Compare commits
122 Commits
v0.28.1-al
...
v0.30.0-al
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5431a70c0c | ||
|
|
65666905e6 | ||
|
|
9703083288 | ||
|
|
c079cafe4d | ||
|
|
421adbe5dd | ||
|
|
35e790cb0d | ||
|
|
2b7ec5cb7f | ||
|
|
d291042e69 | ||
|
|
5c1c97d5a2 | ||
|
|
f51d1ccc07 | ||
|
|
d137456ca1 | ||
|
|
881fe3eff1 | ||
|
|
99ac005b06 | ||
|
|
f132fa1715 | ||
|
|
369b18eef2 | ||
|
|
2ce958e38c | ||
|
|
217c57cd60 | ||
|
|
3ae6bf1d6f | ||
|
|
ec30390446 | ||
|
|
56d5e05762 | ||
|
|
b76ad3b02e | ||
|
|
afd1ce179b | ||
|
|
8f282473b2 | ||
|
|
36e2d1bda1 | ||
|
|
9d5aba69e9 | ||
|
|
1b0d6b473b | ||
|
|
c8339494a8 | ||
|
|
3ef1face9a | ||
|
|
f4cbab4101 | ||
|
|
ae6a8501ee | ||
|
|
1bb41e7165 | ||
|
|
4d7704fba5 | ||
|
|
e4e95bcecf | ||
|
|
a41c97b413 | ||
|
|
46902f5587 | ||
|
|
e7085e3a3e | ||
|
|
9f90f7495f | ||
|
|
260af68a34 | ||
|
|
e461b724a6 | ||
|
|
02c8c9373e | ||
|
|
f40fa787d1 | ||
|
|
7b875091e1 | ||
|
|
17316ec5d0 | ||
|
|
eaa403856e | ||
|
|
e5e2316451 | ||
|
|
98df774d7f | ||
|
|
0a47ebd94b | ||
|
|
abe2f27328 | ||
|
|
fcde9cb9c3 | ||
|
|
2ef3583eff | ||
|
|
a6b579223f | ||
|
|
f1d0a18375 | ||
|
|
55fb2d2e56 | ||
|
|
836634734f | ||
|
|
860e48b0f0 | ||
|
|
04e1b32cd2 | ||
|
|
28732176e1 | ||
|
|
6515097434 | ||
|
|
4c4851ca3d | ||
|
|
4f5503dbf6 | ||
|
|
611086eba4 | ||
|
|
514d273875 | ||
|
|
60cc15bbb0 | ||
|
|
a52ee82fc7 | ||
|
|
381ec75329 | ||
|
|
f6f7794dd7 | ||
|
|
453a308b46 | ||
|
|
9fd1beedb2 | ||
|
|
8db7a9a44a | ||
|
|
b7e488507d | ||
|
|
4728f0e68b | ||
|
|
6db16d67fc | ||
|
|
cc7497532a | ||
|
|
d72968d86b | ||
|
|
7bdccce3d5 | ||
|
|
3df685e114 | ||
|
|
4069aa8274 | ||
|
|
e5a7609588 | ||
|
|
69e0cd2fc4 | ||
|
|
ab6b1feaec | ||
|
|
3a43274881 | ||
|
|
dc8d0587ec | ||
|
|
23c0d52c29 | ||
|
|
c32baa7cd8 | ||
|
|
1153a9ebaf | ||
|
|
2805dddf05 | ||
|
|
baf047f556 | ||
|
|
6745a10508 | ||
|
|
7799f4ff5b | ||
|
|
edcdc8a814 | ||
|
|
5ad623c29b | ||
|
|
bc10af5931 | ||
|
|
784f67a912 | ||
|
|
f4880b40cc | ||
|
|
67c0ea243b | ||
|
|
b9653ba05a | ||
|
|
9875d9facc | ||
|
|
b88717b65f | ||
|
|
5635b930c7 | ||
|
|
870bc6a64a | ||
|
|
da821b431e | ||
|
|
68886d1787 | ||
|
|
0f48239778 | ||
|
|
b13e2f9473 | ||
|
|
c777beb658 | ||
|
|
20c88aaa5b | ||
|
|
e02947be61 | ||
|
|
3a90e2a761 | ||
|
|
65da535745 | ||
|
|
9ed85fd1dd | ||
|
|
aed60b9839 | ||
|
|
3631b34f53 | ||
|
|
0d5f3c091f | ||
|
|
ed51c4b342 | ||
|
|
23516bce76 | ||
|
|
6d1bd99544 | ||
|
|
2fb0b8a741 | ||
|
|
0256269a7f | ||
|
|
fdd5d8c092 | ||
|
|
8b624f5952 | ||
|
|
57d8b742e5 | ||
|
|
d5477b50d5 |
2
.cargo/config.toml
Normal file
2
.cargo/config.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[alias]
|
||||
xtask = "run --package xtask --"
|
||||
9
.github/CODEOWNERS
vendored
9
.github/CODEOWNERS
vendored
@@ -1,8 +1,11 @@
|
||||
# See https://help.github.com/articles/about-codeowners/
|
||||
# See <https://help.github.com/articles/about-codeowners/>
|
||||
|
||||
# for more info about CODEOWNERS file
|
||||
|
||||
# It uses the same pattern rule for gitignore file
|
||||
# https://git-scm.com/docs/gitignore#_pattern_format
|
||||
|
||||
# <https://git-scm.com/docs/gitignore#_pattern_format>
|
||||
|
||||
# Maintainers
|
||||
* @ratatui-org/maintainers
|
||||
|
||||
* @ratatui/maintainers
|
||||
|
||||
2
.github/FUNDING.yml
vendored
Normal file
2
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
github: ratatui
|
||||
open_collective: ratatui
|
||||
51
.github/workflows/bench_track_fork_pr.yml
vendored
51
.github/workflows/bench_track_fork_pr.yml
vendored
@@ -18,43 +18,22 @@ jobs:
|
||||
PR_EVENT: event.json
|
||||
steps:
|
||||
- name: Download Benchmark Results
|
||||
uses: actions/github-script@v7
|
||||
uses: dawidd6/action-download-artifact@v6
|
||||
with:
|
||||
script: |
|
||||
async function downloadArtifact(artifactName) {
|
||||
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: context.payload.workflow_run.id,
|
||||
});
|
||||
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
|
||||
return artifact.name == artifactName
|
||||
})[0];
|
||||
if (!matchArtifact) {
|
||||
core.setFailed(`Failed to find artifact: ${artifactName}`);
|
||||
}
|
||||
let download = await github.rest.actions.downloadArtifact({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
artifact_id: matchArtifact.id,
|
||||
archive_format: 'zip',
|
||||
});
|
||||
let fs = require('fs');
|
||||
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/${artifactName}.zip`, Buffer.from(download.data));
|
||||
}
|
||||
await downloadArtifact(process.env.BENCHMARK_RESULTS);
|
||||
await downloadArtifact(process.env.PR_EVENT);
|
||||
- name: Unzip Benchmark Results
|
||||
run: |
|
||||
unzip $BENCHMARK_RESULTS.zip
|
||||
unzip $PR_EVENT.zip
|
||||
name: ${{ env.BENCHMARK_RESULTS }}
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
- name: Download PR Event
|
||||
uses: dawidd6/action-download-artifact@v6
|
||||
with:
|
||||
name: ${{ env.PR_EVENT }}
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
- name: Export PR Event Data
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
let fs = require('fs');
|
||||
let prEvent = JSON.parse(fs.readFileSync(process.env.PR_EVENT, {encoding: 'utf8'}));
|
||||
core.exportVariable("PR_HEAD", `${prEvent.number}/merge`);
|
||||
core.exportVariable("PR_HEAD", prEvent.pull_request.head.ref);
|
||||
core.exportVariable("PR_BASE", prEvent.pull_request.base.ref);
|
||||
core.exportVariable("PR_BASE_SHA", prEvent.pull_request.base.sha);
|
||||
core.exportVariable("PR_NUMBER", prEvent.number);
|
||||
@@ -64,12 +43,14 @@ jobs:
|
||||
bencher run \
|
||||
--project ratatui-org \
|
||||
--token '${{ secrets.BENCHER_API_TOKEN }}' \
|
||||
--branch '${{ env.PR_HEAD }}' \
|
||||
--branch-start-point '${{ env.PR_BASE }}' \
|
||||
--branch-start-point-hash '${{ env.PR_BASE_SHA }}' \
|
||||
--branch "$PR_HEAD" \
|
||||
--start-point "$PR_BASE" \
|
||||
--start-point-hash "$PR_BASE_SHA" \
|
||||
--start-point-clone-thresholds \
|
||||
--start-point-reset \
|
||||
--testbed ubuntu-latest \
|
||||
--adapter rust_criterion \
|
||||
--err \
|
||||
--github-actions '${{ secrets.GITHUB_TOKEN }}' \
|
||||
--ci-number '${{ env.PR_NUMBER }}' \
|
||||
--file "$BENCHMARK_RESULTS"
|
||||
--ci-number "$PR_NUMBER" \
|
||||
--file "$BENCHMARK_RESULTS"
|
||||
170
.github/workflows/ci.yml
vendored
170
.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
|
||||
@@ -17,82 +16,92 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
# don't install husky hooks during CI as they are only needed for for pre-push
|
||||
CARGO_HUSKY_DONT_INSTALL_HOOKS: true
|
||||
|
||||
# lint, clippy and coveraget jobs are intentionally early in the workflow to catch simple
|
||||
# formatting, 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.
|
||||
# lint, clippy and coverage jobs are intentionally early in the workflow to catch simple formatting,
|
||||
# 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
|
||||
- name: Install Rust nightly
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
with:
|
||||
components: rustfmt
|
||||
- run: cargo +nightly fmt --all --check
|
||||
- uses: dtolnay/rust-toolchain@nightly
|
||||
with: { components: rustfmt }
|
||||
- run: cargo xtask lint-formatting
|
||||
|
||||
typos:
|
||||
# Check for typos in the codebase.
|
||||
# See <https://github.com/crate-ci/typos/>
|
||||
lint-typos:
|
||||
name: Check Typos
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: crate-ci/typos@master
|
||||
|
||||
# Check for any disallowed dependencies in the codebase due to license / security issues.
|
||||
# See <https://github.com/EmbarkStudios/cargo-deny>
|
||||
dependencies:
|
||||
name: Check Dependencies
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: EmbarkStudios/cargo-deny-action@v2
|
||||
|
||||
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
|
||||
- name: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: clippy
|
||||
- name: Install cargo-make
|
||||
uses: taiki-e/install-action@cargo-make
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: cargo make clippy
|
||||
- uses: bnjbvr/cargo-machete@v0.7.0
|
||||
|
||||
markdownlint:
|
||||
# Run cargo clippy.
|
||||
lint-clippy:
|
||||
name: Check Clippy
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Lint markdown
|
||||
uses: DavidAnson/markdownlint-cli2-action@v16
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with: { components: clippy }
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: cargo xtask lint-clippy
|
||||
|
||||
# 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@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
|
||||
- name: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: llvm-tools
|
||||
- name: Install cargo-llvm-cov and cargo-make
|
||||
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
|
||||
- name: Generate coverage
|
||||
run: cargo make coverage
|
||||
- name: Upload to codecov.io
|
||||
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:
|
||||
@@ -101,69 +110,76 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Rust {{ matrix.toolchain }}
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ matrix.toolchain }}
|
||||
- name: Install cargo-make
|
||||
uses: taiki-e/install-action@cargo-make
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: cargo make check
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
- run: cargo xtask check
|
||||
|
||||
lint-docs:
|
||||
# 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
|
||||
- name: Install Rust nightly
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
- name: Install cargo-make
|
||||
uses: taiki-e/install-action@cargo-make
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: cargo make lint-docs
|
||||
- uses: taiki-e/install-action@cargo-rdme
|
||||
- run: cargo xtask check-readme
|
||||
|
||||
test-doc:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
# 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
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: Install cargo-make
|
||||
uses: taiki-e/install-action@cargo-make
|
||||
- uses: dtolnay/rust-toolchain@nightly
|
||||
- uses: dtolnay/install@cargo-docs-rs
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: Test docs
|
||||
run: cargo make test-doc
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
- run: cargo xtask lint-docs
|
||||
|
||||
test:
|
||||
# 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: Swatinem/rust-cache@v2
|
||||
- run: cargo xtask test-docs
|
||||
|
||||
# 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
|
||||
- name: Install Rust ${{ matrix.toolchain }}}
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ matrix.toolchain }}
|
||||
- name: Install cargo-make
|
||||
uses: taiki-e/install-action@cargo-make
|
||||
- name: Install cargo-nextest
|
||||
uses: taiki-e/install-action@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,4 +1,4 @@
|
||||
name: Continuous Deployment
|
||||
name: Release alpha version
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -6,9 +6,6 @@ on:
|
||||
# At 00:00 on Saturday
|
||||
# https://crontab.guru/#0_0_*_*_6
|
||||
- cron: "0 0 * * 6"
|
||||
push:
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
|
||||
defaults:
|
||||
run:
|
||||
@@ -20,7 +17,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
if: ${{ !startsWith(github.event.ref, 'refs/tags/v') }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4
|
||||
@@ -30,14 +26,14 @@ jobs:
|
||||
- name: Calculate the next release
|
||||
run: .github/workflows/calculate-alpha-release.bash
|
||||
|
||||
- name: Publish on crates.io
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: publish
|
||||
args: --allow-dirty --token ${{ secrets.CARGO_TOKEN }}
|
||||
- name: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Publish
|
||||
run: cargo publish --allow-dirty --token ${{ secrets.CARGO_TOKEN }}
|
||||
|
||||
- name: Generate a changelog
|
||||
uses: orhun/git-cliff-action@v3
|
||||
uses: orhun/git-cliff-action@v4
|
||||
with:
|
||||
config: cliff.toml
|
||||
args: --unreleased --tag ${{ env.NEXT_TAG }} --strip header
|
||||
@@ -50,17 +46,3 @@ jobs:
|
||||
tag: ${{ env.NEXT_TAG }}
|
||||
prerelease: true
|
||||
bodyFile: BODY.md
|
||||
|
||||
publish-stable:
|
||||
name: Create a stable release
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ startsWith(github.event.ref, 'refs/tags/v') }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Publish on crates.io
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: publish
|
||||
args: --token ${{ secrets.CARGO_TOKEN }}
|
||||
45
.github/workflows/release-stable.yml
vendored
Normal file
45
.github/workflows/release-stable.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
name: Release stable version
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
|
||||
jobs:
|
||||
publish-stable:
|
||||
name: Create an stable release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Generate a changelog
|
||||
uses: orhun/git-cliff-action@v4
|
||||
with:
|
||||
config: cliff.toml
|
||||
args: --latest --strip header
|
||||
env:
|
||||
OUTPUT: BODY.md
|
||||
|
||||
- name: Publish on GitHub
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
prerelease: false
|
||||
bodyFile: BODY.md
|
||||
|
||||
publish-crate:
|
||||
name: Publish crate
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Publish
|
||||
run: cargo publish --token ${{ secrets.CARGO_TOKEN }}
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,5 +1,4 @@
|
||||
target
|
||||
Cargo.lock
|
||||
*.log
|
||||
*.rs.rustfmt
|
||||
.gdb_history
|
||||
|
||||
@@ -4,14 +4,23 @@ This document contains a list of breaking changes in each version and some notes
|
||||
between versions. It is compiled manually from the commit history and changelog. We also tag PRs on
|
||||
GitHub with a [breaking change] label.
|
||||
|
||||
[breaking change]: (https://github.com/ratatui-org/ratatui/issues?q=label%3A%22breaking+change%22)
|
||||
[breaking change]: (https://github.com/ratatui/ratatui/issues?q=label%3A%22breaking+change%22)
|
||||
|
||||
## Summary
|
||||
|
||||
This is a quick summary of the sections below:
|
||||
|
||||
- [Unreleased](#unreleased)
|
||||
- The `From` impls for backend types are now replaced with more specific traits
|
||||
- [v0.29.0](#v0290)
|
||||
- `Sparkline::data` takes `IntoIterator<Item = SparklineBar>` instead of `&[u64]` and is no longer const
|
||||
- Removed public fields from `Rect` iterators
|
||||
- `Line` now implements `From<Cow<str>`
|
||||
- `Table::highlight_style` is now `Table::row_highlight_style`
|
||||
- `Tabs::select` now accepts `Into<Option<usize>>`
|
||||
- `Color::from_hsl` is now behind the `palette` feature
|
||||
- [v0.28.0](#v0280)
|
||||
⁻ `Backend::size` returns `Size` instead of `Rect`
|
||||
- `Backend::size` returns `Size` instead of `Rect`
|
||||
- `Backend` trait migrates to `get/set_cursor_position`
|
||||
- Ratatui now requires Crossterm 0.28.0
|
||||
- `Axis::labels` now accepts `IntoIterator<Into<Line>>`
|
||||
@@ -65,18 +74,200 @@ This is a quick summary of the sections below:
|
||||
- MSRV is now 1.63.0
|
||||
- `List` no longer ignores empty strings
|
||||
|
||||
## v0.28.0
|
||||
## Unreleased (0.30.0)
|
||||
|
||||
### `WidgetRef` no longer has a blanket implementation of Widget
|
||||
|
||||
Previously there was a blanket implementation of Widget for WidgetRef. This has been reversed to
|
||||
instead be a blanket implementation of WidgetRef for all &W where W: Widget. Any widgets that
|
||||
previously implemented WidgetRef directly should now instead implement Widget for a reference to the
|
||||
type.
|
||||
|
||||
```diff
|
||||
-impl WidgetRef for Foo {
|
||||
- fn render_ref(&self, area: Rect, buf: &mut Buffer)
|
||||
+impl Widget for &Foo {
|
||||
+ fn render(self, area: Rect, buf: &mut Buffer)
|
||||
}
|
||||
```
|
||||
|
||||
### The `From` impls for backend types are now replaced with more specific traits [#1464]
|
||||
|
||||
[#1464]: https://github.com/ratatui/ratatui/pull/1464
|
||||
|
||||
Crossterm gains `ratatui::backend::crossterm::{FromCrossterm, IntoCrossterm}`
|
||||
Termwiz gains `ratatui::backend::termwiz::{FromTermwiz, IntoTermwiz}`
|
||||
|
||||
This is necessary in order to avoid the orphan rule when implementing `From` for crossterm types
|
||||
once the crossterm types are moved to a separate crate.
|
||||
|
||||
```diff
|
||||
+ use ratatui::backend::crossterm::{FromCrossterm, IntoCrossterm};
|
||||
|
||||
let crossterm_color = crossterm::style::Color::Black;
|
||||
- let ratatui_color = crossterm_color.into();
|
||||
- let ratatui_color = ratatui::style::Color::from(crossterm_color);
|
||||
+ let ratatui_color = ratatui::style::Color::from_crossterm(crossterm_color);
|
||||
- let crossterm_color = ratatui_color.into();
|
||||
- let crossterm_color = crossterm::style::Color::from(ratatui_color);
|
||||
+ let crossterm_color = ratatui_color.into_crossterm();
|
||||
|
||||
let crossterm_attribute = crossterm::style::types::Attribute::Bold;
|
||||
- let ratatui_modifier = crossterm_attribute.into();
|
||||
- let ratatui_modifier = ratatui::style::Modifier::from(crossterm_attribute);
|
||||
+ let ratatui_modifier = ratatui::style::Modifier::from_crossterm(crossterm_attribute);
|
||||
- let crossterm_attribute = ratatui_modifier.into();
|
||||
- let crossterm_attribute = crossterm::style::types::Attribute::from(ratatui_modifier);
|
||||
+ let crossterm_attribute = ratatui_modifier.into_crossterm();
|
||||
```
|
||||
|
||||
Similar conversions for `ContentStyle` -> `Style` and `Attributes` -> `Modifier` exist for
|
||||
Crossterm and the various Termion and Termwiz types as well.
|
||||
|
||||
### `Bar::label()` and `BarGroup::label()` now accepts `Into<Line<'a>>`. ([#1471])
|
||||
|
||||
[#1471]: https://github.com/ratatui/ratatui/pull/1471
|
||||
|
||||
Previously `Bar::label()` and `BarGroup::label()` accepted `Line<'a>`, but they now accepts `Into<Line<'a>>`.
|
||||
|
||||
for `Bar::label()`:
|
||||
|
||||
```diff
|
||||
- Bar::default().label("foo".into());
|
||||
+ Bar::default().label("foo");
|
||||
```
|
||||
|
||||
for `BarGroup::label()`:
|
||||
|
||||
```diff
|
||||
- BarGroup::default().label("bar".into());
|
||||
+ BarGroup::default().label("bar");
|
||||
```
|
||||
|
||||
### `Bar::text_value` now accepts `Into<String>` ([#1471])
|
||||
|
||||
Previously `Bar::text_value` accepted `String`, but now it accepts `Into<String>`.
|
||||
|
||||
for `Bar::text_value()`:
|
||||
|
||||
```diff
|
||||
- Bar::default().text_value("foobar".into());
|
||||
+ Bar::default().text_value("foobar");
|
||||
```
|
||||
|
||||
## [v0.29.0](https://github.com/ratatui/ratatui/releases/tag/v0.29.0)
|
||||
|
||||
### `Sparkline::data` takes `IntoIterator<Item = SparklineBar>` instead of `&[u64]` and is no longer const ([#1326])
|
||||
|
||||
[#1326]: https://github.com/ratatui/ratatui/pull/1326
|
||||
|
||||
The `Sparkline::data` method has been modified to accept `IntoIterator<Item = SparklineBar>`
|
||||
instead of `&[u64]`.
|
||||
|
||||
`SparklineBar` is a struct that contains an `Option<u64>` value, which represents an possible
|
||||
_absent_ value, as distinct from a `0` value. This change allows the `Sparkline` to style
|
||||
data points differently, depending on whether they are present or absent.
|
||||
|
||||
`SparklineBar` also contains an `Option<Style>` that will be used to apply a style the bar in
|
||||
addition to any other styling applied to the `Sparkline`.
|
||||
|
||||
Several `From` implementations have been added to `SparklineBar` to support existing callers who
|
||||
provide `&[u64]` and other types that can be converted to `SparklineBar`, such as `Option<u64>`.
|
||||
|
||||
If you encounter any type inference issues, you may need to provide an explicit type for the data
|
||||
passed to `Sparkline::data`. For example, if you are passing a single value, you may need to use
|
||||
`into()` to convert it to form that can be used as a `SparklineBar`:
|
||||
|
||||
```diff
|
||||
let value = 1u8;
|
||||
- Sparkline::default().data(&[value.into()]);
|
||||
+ Sparkline::default().data(&[u64::from(value)]);
|
||||
```
|
||||
|
||||
As a consequence of this change, the `data` method is no longer a `const fn`.
|
||||
|
||||
### `Color::from_hsl` is now behind the `palette` feature and accepts `palette::Hsl` ([#1418])
|
||||
|
||||
[#1418]: https://github.com/ratatui/ratatui/pull/1418
|
||||
|
||||
Previously `Color::from_hsl` accepted components as individual f64 parameters. It now accepts a
|
||||
single `palette::Hsl` value and is gated behind a `palette` feature flag.
|
||||
|
||||
```diff
|
||||
- Color::from_hsl(360.0, 100.0, 100.0)
|
||||
+ Color::from_hsl(Hsl::new(360.0, 100.0, 100.0))
|
||||
```
|
||||
|
||||
### Removed public fields from `Rect` iterators ([#1358], [#1424])
|
||||
|
||||
[#1358]: https://github.com/ratatui/ratatui/pull/1358
|
||||
[#1424]: https://github.com/ratatui/ratatui/pull/1424
|
||||
|
||||
The `pub` modifier has been removed from fields on the `Columns`,`Rows`, and `Positions` iterators.
|
||||
These fields were not intended to be public and should not have been accessed directly.
|
||||
|
||||
### `Rect::area()` now returns u32 instead of u16 ([#1378])
|
||||
|
||||
[#1378]: https://github.com/ratatui/ratatui/pull/1378
|
||||
|
||||
This is likely to impact anything which relies on `Rect::area` maxing out at u16::MAX. It can now
|
||||
return up to u16::MAX * u16::MAX (2^32 - 2^17 + 1).
|
||||
|
||||
### `Line` now implements `From<Cow<str>` ([#1373])
|
||||
|
||||
[#1373]: https://github.com/ratatui/ratatui/pull/1373
|
||||
|
||||
As this adds an extra conversion, ambiguous inferred expressions may no longer compile.
|
||||
|
||||
```rust
|
||||
// given:
|
||||
struct Foo { ... }
|
||||
impl From<Foo> for String { ... }
|
||||
impl From<Foo> for Cow<str> { ... }
|
||||
|
||||
let foo = Foo { ... };
|
||||
let line = Line::from(foo); // now fails due to now ambiguous inferred type
|
||||
// replace with e.g.
|
||||
let line = Line::from(String::from(foo));
|
||||
```
|
||||
|
||||
### `Tabs::select()` now accepts `Into<Option<usize>>` ([#1413])
|
||||
|
||||
[#1413]: https://github.com/ratatui/ratatui/pull/1413
|
||||
|
||||
Previously `Tabs::select()` accepted `usize`, but it now accepts `Into<Option<usize>>`. This breaks
|
||||
any code already using parameter type inference:
|
||||
|
||||
```diff
|
||||
let selected = 1u8;
|
||||
- let tabs = Tabs::new(["A", "B"]).select(selected.into())
|
||||
+ let tabs = Tabs::new(["A", "B"]).select(selected as usize)
|
||||
```
|
||||
|
||||
### `Table::highlight_style` is now `Table::row_highlight_style` ([#1331])
|
||||
|
||||
[#1331]: https://github.com/ratatui/ratatui/pull/1331
|
||||
|
||||
The `Table::highlight_style` is now deprecated in favor of `Table::row_highlight_style`.
|
||||
|
||||
Also, the serialized output of the `TableState` will now include the "selected_column" field.
|
||||
Software that manually parse the serialized the output (with anything other than the `Serialize`
|
||||
implementation on `TableState`) may have to be refactored if the "selected_column" field is not
|
||||
accounted for. This does not affect users who rely on the `Deserialize`, or `Serialize`
|
||||
implementation on the state.
|
||||
|
||||
## [v0.28.0](https://github.com/ratatui/ratatui/releases/tag/v0.28.0)
|
||||
|
||||
### `Backend::size` returns `Size` instead of `Rect` ([#1254])
|
||||
|
||||
[#1254]: https://github.com/ratatui-org/ratatui/pull/1254
|
||||
[#1254]: https://github.com/ratatui/ratatui/pull/1254
|
||||
|
||||
The `Backend::size` method returns a `Size` instead of a `Rect`.
|
||||
There is no need for the position here as it was always 0,0.
|
||||
|
||||
### `Backend` trait migrates to `get/set_cursor_position` ([#1284])
|
||||
|
||||
[#1284]: https://github.com/ratatui-org/ratatui/pull/1284
|
||||
[#1284]: https://github.com/ratatui/ratatui/pull/1284
|
||||
|
||||
If you just use the types implementing the `Backend` trait, you will see deprecation hints but
|
||||
nothing is a breaking change for you.
|
||||
@@ -87,7 +278,7 @@ and a default implementation for them exists.
|
||||
|
||||
### Ratatui now requires Crossterm 0.28.0 ([#1278])
|
||||
|
||||
[#1278]: https://github.com/ratatui-org/ratatui/pull/1278
|
||||
[#1278]: https://github.com/ratatui/ratatui/pull/1278
|
||||
|
||||
Crossterm is updated to version 0.28.0, which is a semver incompatible version with the previous
|
||||
version (0.27.0). Ratatui re-exports the version of crossterm that it is compatible with under
|
||||
@@ -95,8 +286,8 @@ version (0.27.0). Ratatui re-exports the version of crossterm that it is compati
|
||||
|
||||
### `Axis::labels()` now accepts `IntoIterator<Into<Line>>` ([#1273] and [#1283])
|
||||
|
||||
[#1273]: https://github.com/ratatui-org/ratatui/pull/1173
|
||||
[#1283]: https://github.com/ratatui-org/ratatui/pull/1283
|
||||
[#1273]: https://github.com/ratatui/ratatui/pull/1173
|
||||
[#1283]: https://github.com/ratatui/ratatui/pull/1283
|
||||
|
||||
Previously Axis::labels accepted `Vec<Span>`. Any code that uses conversion methods that infer the
|
||||
type will need to be rewritten as the compiler cannot infer the correct type.
|
||||
@@ -108,7 +299,7 @@ type will need to be rewritten as the compiler cannot infer the correct type.
|
||||
|
||||
### `Layout::init_cache` no longer returns bool and takes a `NonZeroUsize` instead of `usize` ([#1245])
|
||||
|
||||
[#1245]: https://github.com/ratatui-org/ratatui/pull/1245
|
||||
[#1245]: https://github.com/ratatui/ratatui/pull/1245
|
||||
|
||||
```diff
|
||||
- let is_initialized = Layout::init_cache(100);
|
||||
@@ -117,7 +308,7 @@ type will need to be rewritten as the compiler cannot infer the correct type.
|
||||
|
||||
### `ratatui::terminal` module is now private ([#1160])
|
||||
|
||||
[#1160]: https://github.com/ratatui-org/ratatui/pull/1160
|
||||
[#1160]: https://github.com/ratatui/ratatui/pull/1160
|
||||
|
||||
The `terminal` module is now private and can not be used directly. The types under this module are
|
||||
exported from the root of the crate. This reduces clashes with other modules in the backends that
|
||||
@@ -130,21 +321,21 @@ are also named terminal, and confusion about module exports for newer Rust users
|
||||
|
||||
### `ToText` no longer has a lifetime ([#1234])
|
||||
|
||||
[#1234]: https://github.com/ratatui-org/ratatui/pull/1234
|
||||
[#1234]: https://github.com/ratatui/ratatui/pull/1234
|
||||
|
||||
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-org/ratatui/pull/1293
|
||||
[#1293]: https://github.com/ratatui/ratatui/pull/1293
|
||||
|
||||
`Frame::size` is renamed to `Frame::area` as its the more correct name.
|
||||
`Frame::size` is renamed to `Frame::area` as it's the more correct name.
|
||||
|
||||
## [v0.27.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.27.0)
|
||||
## [v0.27.0](https://github.com/ratatui/ratatui/releases/tag/v0.27.0)
|
||||
|
||||
### List no clamps the selected index to list ([#1159])
|
||||
|
||||
[#1149]: https://github.com/ratatui-org/ratatui/pull/1149
|
||||
[#1149]: https://github.com/ratatui/ratatui/pull/1149
|
||||
|
||||
The `List` widget now clamps the selected index to the bounds of the list when navigating with
|
||||
`first`, `last`, `previous`, and `next`, as well as when setting the index directly with `select`.
|
||||
@@ -205,11 +396,11 @@ A change is only necessary if you were matching on all variants of the `MouseEve
|
||||
wildcard. In this case, you need to either handle the two new variants, `MouseLeft` and
|
||||
`MouseRight`, or add a wildcard.
|
||||
|
||||
[#1106]: https://github.com/ratatui-org/ratatui/pull/1106
|
||||
[#1106]: https://github.com/ratatui/ratatui/pull/1106
|
||||
|
||||
### `Rect::inner` takes `Margin` directly instead of reference ([#1008])
|
||||
|
||||
[#1008]: https://github.com/ratatui-org/ratatui/pull/1008
|
||||
[#1008]: https://github.com/ratatui/ratatui/pull/1008
|
||||
|
||||
`Margin` needs to be passed without reference now.
|
||||
|
||||
@@ -223,7 +414,7 @@ wildcard. In this case, you need to either handle the two new variants, `MouseLe
|
||||
|
||||
### `Buffer::filled` takes `Cell` directly instead of reference ([#1148])
|
||||
|
||||
[#1148]: https://github.com/ratatui-org/ratatui/pull/1148
|
||||
[#1148]: https://github.com/ratatui/ratatui/pull/1148
|
||||
|
||||
`Buffer::filled` moves the `Cell` instead of taking a reference.
|
||||
|
||||
@@ -234,14 +425,14 @@ wildcard. In this case, you need to either handle the two new variants, `MouseLe
|
||||
|
||||
### `Stylize::bg()` now accepts `Into<Color>` ([#1103])
|
||||
|
||||
[#1103]: https://github.com/ratatui-org/ratatui/pull/1103
|
||||
[#1103]: https://github.com/ratatui/ratatui/pull/1103
|
||||
|
||||
Previously, `Stylize::bg()` accepted `Color` but now accepts `Into<Color>`. This allows more
|
||||
flexible types from calling scopes, though it can break some type inference in the calling scope.
|
||||
|
||||
### Remove deprecated `List::start_corner` and `layout::Corner` ([#759])
|
||||
|
||||
[#759]: https://github.com/ratatui-org/ratatui/pull/759
|
||||
[#759]: https://github.com/ratatui/ratatui/pull/759
|
||||
|
||||
`List::start_corner` was deprecated in v0.25. Use `List::direction` and `ListDirection` instead.
|
||||
|
||||
@@ -264,7 +455,7 @@ flexible types from calling scopes, though it can break some type inference in t
|
||||
|
||||
### `LineGauge::gauge_style` is deprecated ([#565])
|
||||
|
||||
[#565]: https://github.com/ratatui-org/ratatui/pull/1148
|
||||
[#565]: https://github.com/ratatui/ratatui/pull/1148
|
||||
|
||||
`LineGauge::gauge_style` is deprecated and replaced with `LineGauge::filled_style` and `LineGauge::unfilled_style`:
|
||||
|
||||
@@ -275,11 +466,11 @@ let gauge = LineGauge::default()
|
||||
+ .unfilled_style(Style::default().fg(Color::White));
|
||||
```
|
||||
|
||||
## [v0.26.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.26.0)
|
||||
## [v0.26.0](https://github.com/ratatui/ratatui/releases/tag/v0.26.0)
|
||||
|
||||
### `Flex::Start` is the new default flex mode for `Layout` ([#881])
|
||||
|
||||
[#881]: https://github.com/ratatui-org/ratatui/pull/881
|
||||
[#881]: https://github.com/ratatui/ratatui/pull/881
|
||||
|
||||
Previously, constraints would stretch to fill all available space, violating constraints if
|
||||
necessary.
|
||||
@@ -300,7 +491,7 @@ existing layouts with `Flex::Start`. However, to get old behavior, use `Flex::Le
|
||||
|
||||
### `Table::new()` now accepts `IntoIterator<Item: Into<Row<'a>>>` ([#774])
|
||||
|
||||
[#774]: https://github.com/ratatui-org/ratatui/pull/774
|
||||
[#774]: https://github.com/ratatui/ratatui/pull/774
|
||||
|
||||
Previously, `Table::new()` accepted `IntoIterator<Item=Row<'a>>`. The argument change to
|
||||
`IntoIterator<Item: Into<Row<'a>>>`, This allows more flexible types from calling scopes, though it
|
||||
@@ -317,7 +508,7 @@ This can be resolved either by providing an explicit type (e.g. `Vec::<Row>::new
|
||||
|
||||
### `Tabs::new()` now accepts `IntoIterator<Item: Into<Line<'a>>>` ([#776])
|
||||
|
||||
[#776]: https://github.com/ratatui-org/ratatui/pull/776
|
||||
[#776]: https://github.com/ratatui/ratatui/pull/776
|
||||
|
||||
Previously, `Tabs::new()` accepted `Vec<T>` where `T: Into<Line<'a>>`. This allows more flexible
|
||||
types from calling scopes, though it can break some type inference in the calling scope.
|
||||
@@ -333,7 +524,7 @@ by removing the call to `.collect()`.
|
||||
|
||||
### Table::default() now sets segment_size to None and column_spacing to ([#751])
|
||||
|
||||
[#751]: https://github.com/ratatui-org/ratatui/pull/751
|
||||
[#751]: https://github.com/ratatui/ratatui/pull/751
|
||||
|
||||
The default() implementation of Table now sets the column_spacing field to 1 and the segment_size
|
||||
field to `SegmentSize::None`. This will affect the rendering of a small amount of apps.
|
||||
@@ -343,7 +534,7 @@ To use the previous default values, call `table.segment_size(Default::default())
|
||||
|
||||
### `patch_style` & `reset_style` now consumes and returns `Self` ([#754])
|
||||
|
||||
[#754]: https://github.com/ratatui-org/ratatui/pull/754
|
||||
[#754]: https://github.com/ratatui/ratatui/pull/754
|
||||
|
||||
Previously, `patch_style` and `reset_style` in `Text`, `Line` and `Span` were using a mutable
|
||||
reference to `Self`. To be more consistent with the rest of `ratatui`, which is using fluent
|
||||
@@ -369,7 +560,7 @@ The following example shows how to migrate for `Line`, but the same applies for
|
||||
|
||||
### `Block` style methods cannot be used in a const context ([#720])
|
||||
|
||||
[#720]: https://github.com/ratatui-org/ratatui/pull/720
|
||||
[#720]: https://github.com/ratatui/ratatui/pull/720
|
||||
|
||||
Previously the `style()`, `border_style()` and `title_style()` methods could be used to create a
|
||||
`Block` in a constant context. These now accept `Into<Style>` instead of `Style`. These methods no
|
||||
@@ -377,7 +568,7 @@ longer can be called from a constant context.
|
||||
|
||||
### `Line` now has a `style` field that applies to the entire line ([#708])
|
||||
|
||||
[#708]: https://github.com/ratatui-org/ratatui/pull/708
|
||||
[#708]: https://github.com/ratatui/ratatui/pull/708
|
||||
|
||||
Previously the style of a `Line` was stored in the `Span`s that make up the line. Now the `Line`
|
||||
itself has a `style` field, which can be set with the `Line::styled` method. Any code that creates
|
||||
@@ -401,11 +592,11 @@ the `Span::style` field.
|
||||
.alignment(Alignment::Left);
|
||||
```
|
||||
|
||||
## [v0.25.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.25.0)
|
||||
## [v0.25.0](https://github.com/ratatui/ratatui/releases/tag/v0.25.0)
|
||||
|
||||
### Removed `Axis::title_style` and `Buffer::set_background` ([#691])
|
||||
|
||||
[#691]: https://github.com/ratatui-org/ratatui/pull/691
|
||||
[#691]: https://github.com/ratatui/ratatui/pull/691
|
||||
|
||||
These items were deprecated since 0.10.
|
||||
|
||||
@@ -419,7 +610,7 @@ These items were deprecated since 0.10.
|
||||
|
||||
### `List::new()` now accepts `IntoIterator<Item = Into<ListItem<'a>>>` ([#672])
|
||||
|
||||
[#672]: https://github.com/ratatui-org/ratatui/pull/672
|
||||
[#672]: https://github.com/ratatui/ratatui/pull/672
|
||||
|
||||
Previously `List::new()` took `Into<Vec<ListItem<'a>>>`. This change will throw a compilation
|
||||
error for `IntoIterator`s with an indeterminate item (e.g. empty vecs).
|
||||
@@ -434,7 +625,7 @@ E.g.
|
||||
|
||||
### The default `Tabs::highlight_style` is now `Style::new().reversed()` ([#635])
|
||||
|
||||
[#635]: https://github.com/ratatui-org/ratatui/pull/635
|
||||
[#635]: https://github.com/ratatui/ratatui/pull/635
|
||||
|
||||
Previously the default highlight style for tabs was `Style::default()`, which meant that a `Tabs`
|
||||
widget in the default configuration would not show any indication of the selected tab.
|
||||
@@ -446,7 +637,7 @@ widget in the default configuration would not show any indication of the selecte
|
||||
|
||||
### `Table::new()` now requires specifying the widths of the columns ([#664])
|
||||
|
||||
[#664]: https://github.com/ratatui-org/ratatui/pull/664
|
||||
[#664]: https://github.com/ratatui/ratatui/pull/664
|
||||
|
||||
Previously `Table`s could be constructed without `widths`. In almost all cases this is an error.
|
||||
A new `widths` parameter is now mandatory on `Table::new()`. Existing code of the form:
|
||||
@@ -472,7 +663,7 @@ or complex, it may be convenient to replace `Table::new` with `Table::default().
|
||||
|
||||
### `Table::widths()` now accepts `IntoIterator<Item = AsRef<Constraint>>` ([#663])
|
||||
|
||||
[#663]: https://github.com/ratatui-org/ratatui/pull/663
|
||||
[#663]: https://github.com/ratatui/ratatui/pull/663
|
||||
|
||||
Previously `Table::widths()` took a slice (`&'a [Constraint]`). This change will introduce clippy
|
||||
`needless_borrow` warnings for places where slices are passed to this method. To fix these, remove
|
||||
@@ -488,7 +679,7 @@ E.g.
|
||||
|
||||
### Layout::new() now accepts direction and constraint parameters ([#557])
|
||||
|
||||
[#557]: https://github.com/ratatui-org/ratatui/pull/557
|
||||
[#557]: https://github.com/ratatui/ratatui/pull/557
|
||||
|
||||
Previously layout new took no parameters. Existing code should either use `Layout::default()` or
|
||||
the new constructor.
|
||||
@@ -505,18 +696,18 @@ let layout = layout::default()
|
||||
let layout = layout::new(Direction::Vertical, [Constraint::Min(1), Constraint::Max(2)]);
|
||||
```
|
||||
|
||||
## [v0.24.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.24.0)
|
||||
## [v0.24.0](https://github.com/ratatui/ratatui/releases/tag/v0.24.0)
|
||||
|
||||
### `ScrollbarState` field type changed from `u16` to `usize` ([#456])
|
||||
|
||||
[#456]: https://github.com/ratatui-org/ratatui/pull/456
|
||||
[#456]: https://github.com/ratatui/ratatui/pull/456
|
||||
|
||||
In order to support larger content lengths, the `position`, `content_length` and
|
||||
`viewport_content_length` methods on `ScrollbarState` now take `usize` instead of `u16`
|
||||
|
||||
### `BorderType::line_symbols` renamed to `border_symbols` ([#529])
|
||||
|
||||
[#529]: https://github.com/ratatui-org/ratatui/issues/529
|
||||
[#529]: https://github.com/ratatui/ratatui/issues/529
|
||||
|
||||
Applications can now set custom borders on a `Block` by calling `border_set()`. The
|
||||
`BorderType::line_symbols()` is renamed to `border_symbols()` and now returns a new struct
|
||||
@@ -530,7 +721,7 @@ Applications can now set custom borders on a `Block` by calling `border_set()`.
|
||||
|
||||
### Generic `Backend` parameter removed from `Frame` ([#530])
|
||||
|
||||
[#530]: https://github.com/ratatui-org/ratatui/issues/530
|
||||
[#530]: https://github.com/ratatui/ratatui/issues/530
|
||||
|
||||
`Frame` is no longer generic over Backend. Code that accepted `Frame<Backend>` will now need to
|
||||
accept `Frame`. To migrate existing code, remove any generic parameters from code that uses an
|
||||
@@ -544,7 +735,7 @@ instance of a Frame. E.g.:
|
||||
|
||||
### `Stylize` shorthands now consume rather than borrow `String` ([#466])
|
||||
|
||||
[#466]: https://github.com/ratatui-org/ratatui/issues/466
|
||||
[#466]: https://github.com/ratatui/ratatui/issues/466
|
||||
|
||||
In order to support using `Stylize` shorthands (e.g. `"foo".red()`) on temporary `String` values, a
|
||||
new implementation of `Stylize` was added that returns a `Span<'static>`. This causes the value to
|
||||
@@ -562,7 +753,7 @@ longer compile. E.g.
|
||||
|
||||
### Deprecated `Spans` type removed (replaced with `Line`) ([#426])
|
||||
|
||||
[#426]: https://github.com/ratatui-org/ratatui/issues/426
|
||||
[#426]: https://github.com/ratatui/ratatui/issues/426
|
||||
|
||||
`Spans` was replaced with `Line` in 0.21.0. `Buffer::set_spans` was replaced with
|
||||
`Buffer::set_line`.
|
||||
@@ -575,11 +766,11 @@ longer compile. E.g.
|
||||
+ buffer.set_line(0, 0, line, 10);
|
||||
```
|
||||
|
||||
## [v0.23.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.23.0)
|
||||
## [v0.23.0](https://github.com/ratatui/ratatui/releases/tag/v0.23.0)
|
||||
|
||||
### `Scrollbar::track_symbol()` now takes an `Option<&str>` instead of `&str` ([#360])
|
||||
|
||||
[#360]: https://github.com/ratatui-org/ratatui/issues/360
|
||||
[#360]: https://github.com/ratatui/ratatui/issues/360
|
||||
|
||||
The track symbol of `Scrollbar` is now optional, this method now takes an optional value.
|
||||
|
||||
@@ -591,7 +782,7 @@ The track symbol of `Scrollbar` is now optional, this method now takes an option
|
||||
|
||||
### `Scrollbar` symbols moved to `symbols::scrollbar` and `widgets::scrollbar` module is private ([#330])
|
||||
|
||||
[#330]: https://github.com/ratatui-org/ratatui/issues/330
|
||||
[#330]: https://github.com/ratatui/ratatui/issues/330
|
||||
|
||||
The symbols for defining scrollbars have been moved to the `symbols` module from the
|
||||
`widgets::scrollbar` module which is no longer public. To update your code update any imports to the
|
||||
@@ -605,31 +796,31 @@ new module locations. E.g.:
|
||||
|
||||
### MSRV updated to 1.67 ([#361])
|
||||
|
||||
[#361]: https://github.com/ratatui-org/ratatui/issues/361
|
||||
[#361]: https://github.com/ratatui/ratatui/issues/361
|
||||
|
||||
The MSRV of ratatui is now 1.67 due to an MSRV update in a dependency (`time`).
|
||||
|
||||
## [v0.22.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.22.0)
|
||||
## [v0.22.0](https://github.com/ratatui/ratatui/releases/tag/v0.22.0)
|
||||
|
||||
### `bitflags` updated to 2.3 ([#205])
|
||||
|
||||
[#205]: https://github.com/ratatui-org/ratatui/issues/205
|
||||
[#205]: https://github.com/ratatui/ratatui/issues/205
|
||||
|
||||
The `serde` representation of `bitflags` has changed. Any existing serialized types that have
|
||||
Borders or Modifiers will need to be re-serialized. This is documented in the [`bitflags`
|
||||
changelog](https://github.com/bitflags/bitflags/blob/main/CHANGELOG.md#200-rc2)..
|
||||
|
||||
## [v0.21.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.21.0)
|
||||
## [v0.21.0](https://github.com/ratatui/ratatui/releases/tag/v0.21.0)
|
||||
|
||||
### MSRV is 1.65.0 ([#171])
|
||||
|
||||
[#171]: https://github.com/ratatui-org/ratatui/issues/171
|
||||
[#171]: https://github.com/ratatui/ratatui/issues/171
|
||||
|
||||
The minimum supported rust version is now 1.65.0.
|
||||
|
||||
### `Terminal::with_options()` stabilized to allow configuring the viewport ([#114])
|
||||
|
||||
[#114]: https://github.com/ratatui-org/ratatui/issues/114
|
||||
[#114]: https://github.com/ratatui/ratatui/issues/114
|
||||
|
||||
In order to support inline viewports, the unstable method `Terminal::with_options()` was stabilized
|
||||
and `ViewPort` was changed from a struct to an enum.
|
||||
@@ -646,7 +837,7 @@ let terminal = Terminal::with_options(backend, TerminalOptions {
|
||||
|
||||
### Code that binds `Into<Text<'a>>` now requires type annotations ([#168])
|
||||
|
||||
[#168]: https://github.com/ratatui-org/ratatui/issues/168
|
||||
[#168]: https://github.com/ratatui/ratatui/issues/168
|
||||
|
||||
A new type `Masked` was introduced that implements `From<Text<'a>>`. This causes any code that
|
||||
previously did not need to use type annotations to fail to compile. To fix this, annotate or call
|
||||
@@ -660,7 +851,7 @@ previously did not need to use type annotations to fail to compile. To fix this,
|
||||
|
||||
### `Marker::Block` now renders as a block rather than a bar character ([#133])
|
||||
|
||||
[#133]: https://github.com/ratatui-org/ratatui/issues/133
|
||||
[#133]: https://github.com/ratatui/ratatui/issues/133
|
||||
|
||||
Code using the `Block` marker that previously rendered using a half block character (`'▀'``) now
|
||||
renders using the full block character (`'█'`). A new marker variant`Bar` is introduced to replace
|
||||
@@ -672,20 +863,20 @@ the existing code.
|
||||
+ let canvas = Canvas::default().marker(Marker::Bar);
|
||||
```
|
||||
|
||||
## [v0.20.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.20.0)
|
||||
## [v0.20.0](https://github.com/ratatui/ratatui/releases/tag/v0.20.0)
|
||||
|
||||
v0.20.0 was the first release of Ratatui - versions prior to this were release as tui-rs. See the
|
||||
[Changelog](./CHANGELOG.md) for more details.
|
||||
|
||||
### MSRV is update to 1.63.0 ([#80])
|
||||
|
||||
[#80]: https://github.com/ratatui-org/ratatui/issues/80
|
||||
[#80]: https://github.com/ratatui/ratatui/issues/80
|
||||
|
||||
The minimum supported rust version is 1.63.0
|
||||
|
||||
### List no longer ignores empty string in items ([#42])
|
||||
|
||||
[#42]: https://github.com/ratatui-org/ratatui/issues/42
|
||||
[#42]: https://github.com/ratatui/ratatui/issues/42
|
||||
|
||||
The following code now renders 3 items instead of 2. Code which relies on the previous behavior will
|
||||
need to manually filter empty items prior to display.
|
||||
|
||||
3430
CHANGELOG.md
3430
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ creating a new issue before making the change, or starting a discussion on
|
||||
|
||||
## Reporting issues
|
||||
|
||||
Before reporting an issue on the [issue tracker](https://github.com/ratatui-org/ratatui/issues),
|
||||
Before reporting an issue on the [issue tracker](https://github.com/ratatui/ratatui/issues),
|
||||
please check that it has not already been reported by searching for some related keywords. Please
|
||||
also check [`tui-rs` issues](https://github.com/fdehau/tui-rs/issues/) and link any related issues
|
||||
found.
|
||||
@@ -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,22 +71,22 @@ 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-org/ratatui.git
|
||||
git clone https://github.com/ratatui/ratatui.git
|
||||
cd ratatui
|
||||
cargo make build
|
||||
cargo xtask build
|
||||
```
|
||||
|
||||
### Tests
|
||||
|
||||
The [test coverage](https://app.codecov.io/gh/ratatui-org/ratatui) of the crate is reasonably
|
||||
The [test coverage](https://app.codecov.io/gh/ratatui/ratatui) of the crate is reasonably
|
||||
good, but this can always be improved. Focus on keeping the tests simple and obvious and write unit
|
||||
tests for all new or modified code. Beside the usual doc and unit tests, one of the most valuable
|
||||
test you can write for Ratatui is a test against the `TestBackend`. It allows you to assert the
|
||||
@@ -171,7 +171,7 @@ time to update. However, if a deprecation is blocking for us to implement a new
|
||||
|
||||
We don't currently use any unsafe code in Ratatui, and would like to keep it that way. However, there
|
||||
may be specific cases that this becomes necessary in order to avoid slowness. Please see [this
|
||||
discussion](https://github.com/ratatui-org/ratatui/discussions/66) for more about the decision.
|
||||
discussion](https://github.com/ratatui/ratatui/discussions/66) for more about the decision.
|
||||
|
||||
## Continuous Integration
|
||||
|
||||
@@ -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`
|
||||
@@ -196,7 +196,7 @@ it is useful to refer to when contributing code, documentation, or issues with R
|
||||
|
||||
We imported all the PRs from the original repository, implemented many of the smaller ones, and
|
||||
made notes on the leftovers. These are marked as draft PRs and labelled as [imported from
|
||||
tui](https://github.com/ratatui-org/ratatui/pulls?q=is%3Apr+is%3Aopen+label%3A%22imported+from+tui%22).
|
||||
tui](https://github.com/ratatui/ratatui/pulls?q=is%3Apr+is%3Aopen+label%3A%22imported+from+tui%22).
|
||||
We have documented the current state of those PRs, and anyone is welcome to pick them up and
|
||||
continue the work on them.
|
||||
|
||||
|
||||
3953
Cargo.lock
generated
Normal file
3953
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
394
Cargo.toml
394
Cargo.toml
@@ -1,369 +1,55 @@
|
||||
[package]
|
||||
name = "ratatui"
|
||||
version = "0.28.0" # crate version
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["ratatui", "ratatui-*", "xtask"]
|
||||
default-members = [
|
||||
"ratatui",
|
||||
"ratatui-core",
|
||||
"ratatui-crossterm",
|
||||
# this is not included as it doesn't compile on windows
|
||||
# "ratatui-termion",
|
||||
"ratatui-termwiz",
|
||||
"ratatui-widgets",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
authors = ["Florian Dehau <work@fdehau.com>", "The Ratatui Developers"]
|
||||
description = "A library that's all about cooking up terminal user interfaces"
|
||||
documentation = "https://docs.rs/ratatui/latest/ratatui/"
|
||||
repository = "https://github.com/ratatui-org/ratatui"
|
||||
repository = "https://github.com/ratatui/ratatui"
|
||||
homepage = "https://ratatui.rs"
|
||||
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"
|
||||
|
||||
[badges]
|
||||
|
||||
[dependencies]
|
||||
bitflags = "2.3"
|
||||
cassowary = "0.3"
|
||||
compact_str = "0.8.0"
|
||||
crossterm = { version = "0.28.1", optional = true }
|
||||
document-features = { version = "0.2.7", optional = true }
|
||||
instability = "0.3.1"
|
||||
itertools = "0.13"
|
||||
lru = "0.12.0"
|
||||
paste = "1.0.2"
|
||||
palette = { version = "0.7.6", optional = true }
|
||||
serde = { version = "1", optional = true, features = ["derive"] }
|
||||
strum = { version = "0.26", features = ["derive"] }
|
||||
strum_macros = { version = "0.26.3" }
|
||||
termion = { version = "4.0.0", optional = true }
|
||||
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"
|
||||
|
||||
[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"
|
||||
octocrab = "0.39.0"
|
||||
pretty_assertions = "1.4.0"
|
||||
rand = "0.8.5"
|
||||
rand_chacha = "0.3.1"
|
||||
rstest = "0.22.0"
|
||||
serde_json = "1.0.109"
|
||||
tokio = { version = "1.39.2", features = [
|
||||
"rt",
|
||||
"macros",
|
||||
"time",
|
||||
"rt-multi-thread",
|
||||
] }
|
||||
tracing = "0.1.40"
|
||||
tracing-appender = "0.2.3"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
|
||||
[lints.rust]
|
||||
unsafe_code = "forbid"
|
||||
|
||||
[lints.clippy]
|
||||
cargo = { level = "warn", priority = -1 }
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
cast_possible_truncation = "allow"
|
||||
cast_possible_wrap = "allow"
|
||||
cast_precision_loss = "allow"
|
||||
cast_sign_loss = "allow"
|
||||
missing_errors_doc = "allow"
|
||||
missing_panics_doc = "allow"
|
||||
module_name_repetitions = "allow"
|
||||
must_use_candidate = "allow"
|
||||
|
||||
# we often split up a module into multiple files with the main type in a file named after the
|
||||
# module, so we want to allow this pattern
|
||||
module_inception = "allow"
|
||||
|
||||
# nursery or restricted
|
||||
as_underscore = "warn"
|
||||
deref_by_slicing = "warn"
|
||||
else_if_without_else = "warn"
|
||||
empty_line_after_doc_comments = "warn"
|
||||
equatable_if_let = "warn"
|
||||
fn_to_numeric_cast_any = "warn"
|
||||
format_push_string = "warn"
|
||||
map_err_ignore = "warn"
|
||||
missing_const_for_fn = "warn"
|
||||
mixed_read_write_in_expression = "warn"
|
||||
mod_module_files = "warn"
|
||||
needless_pass_by_ref_mut = "warn"
|
||||
needless_raw_strings = "warn"
|
||||
or_fun_call = "warn"
|
||||
redundant_type_annotations = "warn"
|
||||
rest_pat_in_fully_bound_structs = "warn"
|
||||
string_lit_chars_any = "warn"
|
||||
string_slice = "warn"
|
||||
string_to_string = "warn"
|
||||
unnecessary_self_imports = "warn"
|
||||
use_self = "warn"
|
||||
|
||||
[features]
|
||||
#! The crate provides a set of optional features that can be enabled in your `cargo.toml` file.
|
||||
#!
|
||||
## By default, we enable the crossterm backend as this is a reasonable choice for most applications
|
||||
## as it is supported on Linux/Mac/Windows systems. We also enable the `underline-color` feature
|
||||
## which allows you to set the underline color of text.
|
||||
default = ["crossterm", "underline-color"]
|
||||
#! Generally an application will only use one backend, so you should only enable one of the following features:
|
||||
## enables the [`CrosstermBackend`](backend::CrosstermBackend) backend and adds a dependency on [`crossterm`].
|
||||
crossterm = ["dep:crossterm"]
|
||||
## enables the [`TermionBackend`](backend::TermionBackend) backend and adds a dependency on [`termion`].
|
||||
termion = ["dep:termion"]
|
||||
## enables the [`TermwizBackend`](backend::TermwizBackend) backend and adds a dependency on [`termwiz`].
|
||||
termwiz = ["dep:termwiz"]
|
||||
|
||||
#! The following optional features are available for all backends:
|
||||
## enables serialization and deserialization of style and color types using the [`serde`] crate.
|
||||
## This is useful if you want to save themes to a file.
|
||||
serde = ["dep:serde", "bitflags/serde", "compact_str/serde"]
|
||||
|
||||
## enables the [`border!`] macro.
|
||||
macros = []
|
||||
|
||||
## enables conversions from colors in the [`palette`] crate to [`Color`](crate::style::Color).
|
||||
palette = ["dep:palette"]
|
||||
|
||||
## enables all widgets.
|
||||
all-widgets = ["widget-calendar"]
|
||||
|
||||
#! Widgets that add dependencies are gated behind feature flags to prevent unused transitive
|
||||
#! dependencies. The available features are:
|
||||
## enables the [`calendar`](widgets::calendar) widget module and adds a dependency on [`time`].
|
||||
widget-calendar = ["dep:time"]
|
||||
|
||||
#! The following optional features are only available for some backends:
|
||||
|
||||
## enables the backend code that sets the underline color.
|
||||
## Underline color is only supported by the [`CrosstermBackend`](backend::CrosstermBackend) backend,
|
||||
## and is not supported on Windows 7.
|
||||
underline-color = ["dep:crossterm"]
|
||||
|
||||
#! The following features are unstable and may change in the future:
|
||||
|
||||
## Enable all unstable features.
|
||||
unstable = ["unstable-rendered-line-info", "unstable-widget-ref"]
|
||||
|
||||
## 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-org/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"]
|
||||
[workspace.dependencies]
|
||||
bitflags = "2.6.0"
|
||||
color-eyre = "0.6.3"
|
||||
crossterm = "0.28.1"
|
||||
document-features = "0.2.7"
|
||||
indoc = "2.0.5"
|
||||
instability = "0.3.3"
|
||||
itertools = "0.13.0"
|
||||
pretty_assertions = "1.4.1"
|
||||
ratatui = { path = "ratatui", version = "0.30.0-alpha.0" }
|
||||
ratatui-core = { path = "ratatui-core", version = "0.1.0-alpha.0" }
|
||||
ratatui-crossterm = { path = "ratatui-crossterm", version = "0.1.0-alpha.0" }
|
||||
ratatui-termion = { path = "ratatui-termion", version = "0.1.0-alpha.0" }
|
||||
ratatui-termwiz = { path = "ratatui-termwiz", version = "0.1.0-alpha.0" }
|
||||
ratatui-widgets = { path = "ratatui-widgets", version = "0.3.0-alpha.0" }
|
||||
rstest = "0.23.0"
|
||||
serde = { version = "1.0.215", features = ["derive"] }
|
||||
serde_json = "1.0.133"
|
||||
strum = { version = "0.26.3", features = ["derive"] }
|
||||
termwiz = { version = "0.22.0" }
|
||||
unicode-segmentation = "1.12.0"
|
||||
# See <https://github.com/ratatui/ratatui/issues/1271> for information about why we pin unicode-width
|
||||
unicode-width = "=0.2.0"
|
||||
termion = "4.0.0"
|
||||
|
||||
# Improve benchmark consistency
|
||||
[profile.bench]
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[lib]
|
||||
bench = false
|
||||
|
||||
[[bench]]
|
||||
name = "main"
|
||||
harness = false
|
||||
|
||||
[[example]]
|
||||
name = "async"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "barchart"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "barchart-grouped"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "block"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "calendar"
|
||||
required-features = ["crossterm", "widget-calendar"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "canvas"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "chart"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "colors"
|
||||
required-features = ["crossterm"]
|
||||
# this example is a bit verbose, so we don't want to include it in the docs
|
||||
doc-scrape-examples = false
|
||||
|
||||
[[example]]
|
||||
name = "colors_rgb"
|
||||
required-features = ["crossterm", "palette"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "constraint-explorer"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "constraints"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = false
|
||||
|
||||
[[example]]
|
||||
name = "custom_widget"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "demo"
|
||||
# this runs for all of the terminal backends, so it can't be built using --all-features or scraped
|
||||
doc-scrape-examples = false
|
||||
|
||||
[[example]]
|
||||
name = "demo2"
|
||||
required-features = ["crossterm", "palette", "widget-calendar"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "docsrs"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = false
|
||||
|
||||
[[example]]
|
||||
name = "flex"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "gauge"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "hello_world"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "inline"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "layout"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "line_gauge"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "hyperlink"
|
||||
required-features = ["crossterm", "unstable-widget-ref"]
|
||||
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
|
||||
|
||||
[[test]]
|
||||
name = "state_serde"
|
||||
required-features = ["serde"]
|
||||
|
||||
@@ -6,10 +6,10 @@ This file documents current and past maintainers.
|
||||
- [joshka](https://github.com/joshka)
|
||||
- [kdheepak](https://github.com/kdheepak)
|
||||
- [Valentin271](https://github.com/Valentin271)
|
||||
- [EdJoPaTo](https://github.com/EdJoPaTo)
|
||||
|
||||
## Past Maintainers
|
||||
|
||||
- [fdehau](https://github.com/fdehau)
|
||||
- [mindoodoo](https://github.com/mindoodoo)
|
||||
- [sayanarijit](https://github.com/sayanarijit)
|
||||
- [EdJoPaTo](https://github.com/EdJoPaTo)
|
||||
|
||||
180
Makefile.toml
180
Makefile.toml
@@ -1,180 +0,0 @@
|
||||
# configuration for https://github.com/sagiegurari/cargo-make
|
||||
|
||||
[config]
|
||||
skip_core_tasks = true
|
||||
|
||||
[env]
|
||||
# all features except the backend ones
|
||||
ALL_FEATURES = "all-widgets,macros,serde"
|
||||
|
||||
[env.ALL_FEATURES_FLAG]
|
||||
# Windows does not support building termion, so this avoids the build failure by providing two
|
||||
# sets of flags, one for Windows and one for other platforms.
|
||||
source = "${CARGO_MAKE_RUST_TARGET_OS}"
|
||||
default_value = "--features=all-widgets,macros,serde,crossterm,termion,termwiz,underline-color,unstable"
|
||||
mapping = { "windows" = "--features=all-widgets,macros,serde,crossterm,termwiz,underline-color,unstable" }
|
||||
|
||||
[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", "lint-markdown"]
|
||||
|
||||
[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",
|
||||
"--no-default-features",
|
||||
"${ALL_FEATURES_FLAG}",
|
||||
"--",
|
||||
"-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",
|
||||
"--no-default-features",
|
||||
"${ALL_FEATURES_FLAG}",
|
||||
]
|
||||
|
||||
[tasks.build]
|
||||
description = "Compile the project"
|
||||
command = "cargo"
|
||||
args = [
|
||||
"build",
|
||||
"--all-targets",
|
||||
"--no-default-features",
|
||||
"${ALL_FEATURES_FLAG}",
|
||||
]
|
||||
|
||||
[tasks.clippy]
|
||||
description = "Run Clippy for linting"
|
||||
command = "cargo"
|
||||
args = [
|
||||
"clippy",
|
||||
"--all-targets",
|
||||
"--tests",
|
||||
"--benches",
|
||||
"--no-default-features",
|
||||
"${ALL_FEATURES_FLAG}",
|
||||
"--",
|
||||
"-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",
|
||||
"--no-default-features",
|
||||
"${ALL_FEATURES_FLAG}",
|
||||
]
|
||||
|
||||
[tasks.test-doc]
|
||||
description = "Run documentation tests"
|
||||
command = "cargo"
|
||||
args = ["test", "--doc", "--no-default-features", "${ALL_FEATURES_FLAG}"]
|
||||
|
||||
[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",
|
||||
"${ALL_FEATURES},${@}",
|
||||
]
|
||||
|
||||
[tasks.coverage]
|
||||
description = "Generate code coverage report"
|
||||
command = "cargo"
|
||||
args = [
|
||||
"llvm-cov",
|
||||
"--lcov",
|
||||
"--output-path",
|
||||
"target/lcov.info",
|
||||
"--no-default-features",
|
||||
"${ALL_FEATURES_FLAG}",
|
||||
]
|
||||
|
||||
[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
|
||||
'''
|
||||
363
README.md
363
README.md
@@ -2,16 +2,19 @@
|
||||
<summary>Table of Contents</summary>
|
||||
|
||||
- [Ratatui](#ratatui)
|
||||
- [Installation](#installation)
|
||||
- [Quick Start](#quickstart)
|
||||
- [Other documentation](#other-documentation)
|
||||
- [Introduction](#introduction)
|
||||
- [Other Documentation](#other-documentation)
|
||||
- [Quickstart](#quickstart)
|
||||
- [Initialize and restore the terminal](#initialize-and-restore-the-terminal)
|
||||
- [Drawing the UI](#drawing-the-ui)
|
||||
- [Handling events](#handling-events)
|
||||
- [Layout](#layout)
|
||||
- [Text and styling](#text-and-styling)
|
||||
- [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)
|
||||
- [Third-party libraries, bootstrapping templates and widgets](#third-party-libraries-bootstrapping-templates-and-widgets)
|
||||
- [Apps](#apps)
|
||||
- [Alternatives](#alternatives)
|
||||
- [Acknowledgments](#acknowledgments)
|
||||
@@ -21,7 +24,7 @@
|
||||
|
||||
<!-- cargo-rdme start -->
|
||||
|
||||

|
||||

|
||||
|
||||
<div align="center">
|
||||
|
||||
@@ -41,28 +44,42 @@ Badge]][GitHub Sponsors]<br> [![Discord Badge]][Discord Server] [![Matrix Badge]
|
||||
lightweight library that provides a set of widgets and utilities to build complex Rust TUIs.
|
||||
Ratatui was forked from the [tui-rs] crate in 2023 in order to continue its development.
|
||||
|
||||
## Installation
|
||||
## Quickstart
|
||||
|
||||
Add `ratatui` as a dependency to your cargo.toml:
|
||||
Add `ratatui` and `crossterm` as dependencies to your cargo.toml:
|
||||
|
||||
```shell
|
||||
cargo add ratatui
|
||||
cargo add ratatui crossterm
|
||||
```
|
||||
|
||||
Ratatui uses [Crossterm] by default as it works on most platforms. See the [Installation]
|
||||
section of the [Ratatui Website] for more details on how to use other backends ([Termion] /
|
||||
[Termwiz]).
|
||||
Then you can create a simple "Hello World" application:
|
||||
|
||||
## Introduction
|
||||
```rust
|
||||
use crossterm::event::{self, Event};
|
||||
use ratatui::{text::Text, Frame};
|
||||
|
||||
Ratatui is based on the principle of immediate rendering with intermediate buffers. This means
|
||||
that for each frame, your app must render all widgets that are supposed to be part of the UI.
|
||||
This is in contrast to the retained mode style of rendering where widgets are updated and then
|
||||
automatically redrawn on the next frame. See the [Rendering] section of the [Ratatui Website]
|
||||
for more info.
|
||||
fn main() {
|
||||
let mut terminal = ratatui::init();
|
||||
loop {
|
||||
terminal.draw(draw).expect("failed to draw frame");
|
||||
if matches!(event::read().expect("failed to read event"), Event::Key(_)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
ratatui::restore();
|
||||
}
|
||||
|
||||
You can also watch the [FOSDEM 2024 talk] about Ratatui which gives a brief introduction to
|
||||
terminal user interfaces and showcases the features of Ratatui, along with a hello world demo.
|
||||
fn draw(frame: &mut Frame) {
|
||||
let text = Text::raw("Hello World!");
|
||||
frame.render_widget(text, frame.area());
|
||||
}
|
||||
```
|
||||
|
||||
The full code for this example which contains a little more detail is in the [Examples]
|
||||
directory. For more guidance on different ways to structure your application see the
|
||||
[Application Patterns] and [Hello World tutorial] sections in the [Ratatui Website] and the
|
||||
various [Examples]. There are also several starter templates available in the [templates]
|
||||
repository.
|
||||
|
||||
## Other documentation
|
||||
|
||||
@@ -74,46 +91,82 @@ terminal user interfaces and showcases the features of Ratatui, along with a hel
|
||||
- [Changelog] - generated by [git-cliff] utilizing [Conventional Commits].
|
||||
- [Breaking Changes] - a list of breaking changes in the library.
|
||||
|
||||
## Quickstart
|
||||
You can also watch the [FOSDEM 2024 talk] about Ratatui which gives a brief introduction to
|
||||
terminal user interfaces and showcases the features of Ratatui, along with a hello world demo.
|
||||
|
||||
The following example demonstrates the minimal amount of code necessary to setup a terminal and
|
||||
render "Hello World!". The full code for this example which contains a little more detail is in
|
||||
the [Examples] directory. For more guidance on different ways to structure your application see
|
||||
the [Application Patterns] and [Hello World tutorial] sections in the [Ratatui Website] and the
|
||||
various [Examples]. There are also several starter templates available in the [templates]
|
||||
repository.
|
||||
## Introduction
|
||||
|
||||
Ratatui is based on the principle of immediate rendering with intermediate buffers. This means
|
||||
that for each frame, your app must render all widgets that are supposed to be part of the UI.
|
||||
This is in contrast to the retained mode style of rendering where widgets are updated and then
|
||||
automatically redrawn on the next frame. See the [Rendering] section of the [Ratatui Website]
|
||||
for more info.
|
||||
|
||||
Ratatui uses [Crossterm] by default as it works on most platforms. See the [Installation]
|
||||
section of the [Ratatui Website] for more details on how to use other backends ([Termion] /
|
||||
[Termwiz]).
|
||||
|
||||
Every application built with `ratatui` needs to implement the following steps:
|
||||
|
||||
- Initialize the terminal
|
||||
- A main loop to:
|
||||
- Handle input events
|
||||
- Draw the UI
|
||||
- A main loop that:
|
||||
- Draws the UI
|
||||
- Handles input events
|
||||
- Restore the terminal state
|
||||
|
||||
The library contains a [`prelude`] module that re-exports the most commonly used traits and
|
||||
types for convenience. Most examples in the documentation will use this instead of showing the
|
||||
full path of each type.
|
||||
|
||||
### Initialize and restore the terminal
|
||||
|
||||
The [`Terminal`] type is the main entry point for any Ratatui application. It is a light
|
||||
abstraction over a choice of [`Backend`] implementations that provides functionality to draw
|
||||
each frame, clear the screen, hide the cursor, etc. It is parametrized over any type that
|
||||
implements the [`Backend`] trait which has implementations for [Crossterm], [Termion] and
|
||||
[Termwiz].
|
||||
The [`Terminal`] type is the main entry point for any Ratatui application. It is generic over a
|
||||
a choice of [`Backend`] implementations that each provide functionality to draw frames, clear
|
||||
the screen, hide the cursor, etc. There are backend implementations for [Crossterm], [Termion]
|
||||
and [Termwiz].
|
||||
|
||||
Most applications should enter the Alternate Screen when starting and leave it when exiting and
|
||||
also enable raw mode to disable line buffering and enable reading key events. See the [`backend`
|
||||
module] and the [Backends] section of the [Ratatui Website] for more info.
|
||||
The simplest way to initialize the terminal is to use the [`init`] function which returns a
|
||||
[`DefaultTerminal`] instance with the default options, enters the Alternate Screen and Raw mode
|
||||
and sets up a panic hook that restores the terminal in case of panic. This instance can then be
|
||||
used to draw frames and interact with the terminal state. (The [`DefaultTerminal`] instance is a
|
||||
type alias for a terminal with the [`crossterm`] backend.) The [`restore`] function restores the
|
||||
terminal to its original state.
|
||||
|
||||
```rust
|
||||
fn main() -> std::io::Result<()> {
|
||||
let mut terminal = ratatui::init();
|
||||
let result = run(&mut terminal);
|
||||
ratatui::restore();
|
||||
result
|
||||
}
|
||||
```
|
||||
|
||||
See the [`backend` module] and the [Backends] section of the [Ratatui Website] for more info on
|
||||
the alternate screen and raw mode.
|
||||
|
||||
### Drawing the UI
|
||||
|
||||
The drawing logic is delegated to a closure that takes a [`Frame`] instance as argument. The
|
||||
[`Frame`] provides the size of the area to draw to and allows the app to render any [`Widget`]
|
||||
using the provided [`render_widget`] method. After this closure returns, a diff is performed and
|
||||
only the changes are drawn to the terminal. See the [Widgets] section of the [Ratatui Website]
|
||||
for more info.
|
||||
Drawing the UI is done by calling the [`Terminal::draw`] method on the terminal instance. This
|
||||
method takes a closure that is called with a [`Frame`] instance. The [`Frame`] provides the size
|
||||
of the area to draw to and allows the app to render any [`Widget`] using the provided
|
||||
[`render_widget`] method. After this closure returns, a diff is performed and only the changes
|
||||
are drawn to the terminal. See the [Widgets] section of the [Ratatui Website] for more info.
|
||||
|
||||
The closure passed to the [`Terminal::draw`] method should handle the rendering of a full frame.
|
||||
|
||||
```rust
|
||||
use ratatui::{widgets::Paragraph, Frame};
|
||||
|
||||
fn run(terminal: &mut ratatui::DefaultTerminal) -> std::io::Result<()> {
|
||||
loop {
|
||||
terminal.draw(|frame| draw(frame))?;
|
||||
if handle_events()? {
|
||||
break Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(frame: &mut Frame) {
|
||||
let text = Paragraph::new("Hello World!");
|
||||
frame.render_widget(text, frame.area());
|
||||
}
|
||||
```
|
||||
|
||||
### Handling events
|
||||
|
||||
@@ -122,63 +175,23 @@ calling backend library methods directly. See the [Handling Events] section of t
|
||||
Website] for more info. For example, if you are using [Crossterm], you can use the
|
||||
[`crossterm::event`] module to handle events.
|
||||
|
||||
### Example
|
||||
|
||||
```rust
|
||||
use std::io::{self, stdout};
|
||||
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind};
|
||||
|
||||
use ratatui::{
|
||||
backend::CrosstermBackend,
|
||||
crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
terminal::{
|
||||
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
|
||||
fn handle_events() -> std::io::Result<bool> {
|
||||
match event::read()? {
|
||||
Event::Key(key) if key.kind == KeyEventKind::Press => match key.code {
|
||||
KeyCode::Char('q') => return Ok(true),
|
||||
// handle other key events
|
||||
_ => {}
|
||||
},
|
||||
ExecutableCommand,
|
||||
},
|
||||
widgets::{Block, Paragraph},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
fn main() -> io::Result<()> {
|
||||
enable_raw_mode()?;
|
||||
stdout().execute(EnterAlternateScreen)?;
|
||||
let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
|
||||
|
||||
let mut should_quit = false;
|
||||
while !should_quit {
|
||||
terminal.draw(ui)?;
|
||||
should_quit = handle_events()?;
|
||||
}
|
||||
|
||||
disable_raw_mode()?;
|
||||
stdout().execute(LeaveAlternateScreen)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_events() -> io::Result<bool> {
|
||||
if event::poll(std::time::Duration::from_millis(50))? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == event::KeyEventKind::Press && key.code == KeyCode::Char('q') {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
// handle other events
|
||||
_ => {}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn ui(frame: &mut Frame) {
|
||||
frame.render_widget(
|
||||
Paragraph::new("Hello World!").block(Block::bordered().title("Greeting")),
|
||||
frame.size(),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Running this example produces the following output:
|
||||
|
||||
![docsrs-hello]
|
||||
|
||||
## Layout
|
||||
|
||||
The library comes with a basic yet useful layout management object called [`Layout`] which
|
||||
@@ -193,16 +206,13 @@ use ratatui::{
|
||||
Frame,
|
||||
};
|
||||
|
||||
fn ui(frame: &mut Frame) {
|
||||
let [title_area, main_area, status_area] = Layout::vertical([
|
||||
Constraint::Length(1),
|
||||
Constraint::Min(0),
|
||||
Constraint::Length(1),
|
||||
])
|
||||
.areas(frame.size());
|
||||
let [left_area, right_area] =
|
||||
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
.areas(main_area);
|
||||
fn draw(frame: &mut Frame) {
|
||||
use Constraint::{Fill, Length, Min};
|
||||
|
||||
let vertical = Layout::vertical([Length(1), Min(0), Length(1)]);
|
||||
let [title_area, main_area, status_area] = vertical.areas(frame.area());
|
||||
let horizontal = Layout::horizontal([Fill(1); 2]);
|
||||
let [left_area, right_area] = horizontal.areas(main_area);
|
||||
|
||||
frame.render_widget(Block::bordered().title("Title Bar"), title_area);
|
||||
frame.render_widget(Block::bordered().title("Status Bar"), status_area);
|
||||
@@ -213,7 +223,13 @@ fn ui(frame: &mut Frame) {
|
||||
|
||||
Running this example produces the following output:
|
||||
|
||||
![docsrs-layout]
|
||||
```text
|
||||
Title Bar───────────────────────────────────
|
||||
┌Left────────────────┐┌Right───────────────┐
|
||||
│ ││ │
|
||||
└────────────────────┘└────────────────────┘
|
||||
Status Bar──────────────────────────────────
|
||||
```
|
||||
|
||||
## Text and styling
|
||||
|
||||
@@ -236,8 +252,8 @@ use ratatui::{
|
||||
Frame,
|
||||
};
|
||||
|
||||
fn ui(frame: &mut Frame) {
|
||||
let areas = Layout::vertical([Constraint::Length(1); 4]).split(frame.size());
|
||||
fn draw(frame: &mut Frame) {
|
||||
let areas = Layout::vertical([Constraint::Length(1); 4]).split(frame.area());
|
||||
|
||||
let line = Line::from(vec![
|
||||
Span::raw("Hello "),
|
||||
@@ -266,10 +282,6 @@ fn ui(frame: &mut Frame) {
|
||||
}
|
||||
```
|
||||
|
||||
Running this example produces the following output:
|
||||
|
||||
![docsrs-styling]
|
||||
|
||||
[Ratatui Website]: https://ratatui.rs/
|
||||
[Installation]: https://ratatui.rs/installation/
|
||||
[Rendering]: https://ratatui.rs/concepts/rendering/
|
||||
@@ -280,21 +292,18 @@ Running this example produces the following output:
|
||||
[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-org/templates/
|
||||
[Examples]: https://github.com/ratatui-org/ratatui/tree/main/examples/README.md
|
||||
[Report a bug]: https://github.com/ratatui-org/ratatui/issues/new?labels=bug&projects=&template=bug_report.md
|
||||
[Request a Feature]: https://github.com/ratatui-org/ratatui/issues/new?labels=enhancement&projects=&template=feature_request.md
|
||||
[Create a Pull Request]: https://github.com/ratatui-org/ratatui/compare
|
||||
[templates]: https://github.com/ratatui/templates/
|
||||
[Examples]: https://github.com/ratatui/ratatui/tree/main/ratatui/examples/README.md
|
||||
[Report a bug]: https://github.com/ratatui/ratatui/issues/new?labels=bug&projects=&template=bug_report.md
|
||||
[Request a Feature]: https://github.com/ratatui/ratatui/issues/new?labels=enhancement&projects=&template=feature_request.md
|
||||
[Create a Pull Request]: https://github.com/ratatui/ratatui/compare
|
||||
[git-cliff]: https://git-cliff.org
|
||||
[Conventional Commits]: https://www.conventionalcommits.org
|
||||
[API Docs]: https://docs.rs/ratatui
|
||||
[Changelog]: https://github.com/ratatui-org/ratatui/blob/main/CHANGELOG.md
|
||||
[Contributing]: https://github.com/ratatui-org/ratatui/blob/main/CONTRIBUTING.md
|
||||
[Breaking Changes]: https://github.com/ratatui-org/ratatui/blob/main/BREAKING-CHANGES.md
|
||||
[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-org/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-hello.png?raw=true
|
||||
[docsrs-layout]: https://github.com/ratatui-org/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-layout.png?raw=true
|
||||
[docsrs-styling]: https://github.com/ratatui-org/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-styling.png?raw=true
|
||||
[`Frame`]: terminal::Frame
|
||||
[`render_widget`]: terminal::Frame::render_widget
|
||||
[`Widget`]: widgets::Widget
|
||||
@@ -313,15 +322,15 @@ Running this example produces the following output:
|
||||
[Termion]: https://crates.io/crates/termion
|
||||
[Termwiz]: https://crates.io/crates/termwiz
|
||||
[tui-rs]: https://crates.io/crates/tui
|
||||
[GitHub Sponsors]: https://github.com/sponsors/ratatui-org
|
||||
[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-org/ratatui/ci.yml?style=flat-square&logo=github
|
||||
[CI Workflow]: https://github.com/ratatui-org/ratatui/actions/workflows/ci.yml
|
||||
[Codecov Badge]: https://img.shields.io/codecov/c/github/ratatui-org/ratatui?logo=codecov&style=flat-square&token=BAQ8SOKEST&color=C43AC3&logoColor=C43AC3
|
||||
[Codecov]: https://app.codecov.io/gh/ratatui-org/ratatui
|
||||
[Deps.rs Badge]: https://deps.rs/repo/github/ratatui-org/ratatui/status.svg?style=flat-square
|
||||
[Deps.rs]: https://deps.rs/repo/github/ratatui-org/ratatui
|
||||
[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
|
||||
@@ -329,100 +338,42 @@ Running this example produces the following output:
|
||||
[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-org?logo=github&style=flat-square&color=1370D3
|
||||
[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 🚀
|
||||
## Contributing
|
||||
|
||||
In order to organize ourselves, we currently use a [Discord server](https://discord.gg/pMCEU9hNEj),
|
||||
feel free to join and come chat! There is also a [Matrix](https://matrix.org/) bridge available at
|
||||
[#ratatui:matrix.org](https://matrix.to/#/#ratatui:matrix.org).
|
||||
|
||||
While we do utilize Discord for coordinating, it's not essential for contributing. We have recently
|
||||
launched the [Ratatui Forum][Forum], and our primary open-source workflow is centered around GitHub.
|
||||
For bugs and features, we rely on GitHub. Please [Report a bug], [Request a Feature] or [Create a
|
||||
Pull Request].
|
||||
We have also recently launched the [Ratatui Forum][Forum], For bugs and features, we rely on GitHub.
|
||||
Please [Report a bug], [Request a Feature] or [Create a Pull Request].
|
||||
|
||||
Please make sure you read the updated [contributing](./CONTRIBUTING.md) guidelines, especially if
|
||||
you are interested in working on a PR or issue opened in the previous repository.
|
||||
Please make sure you read the [contributing](./CONTRIBUTING.md) guidelines, especially if you are
|
||||
interested in working on a PR or issue opened in the previous repository.
|
||||
|
||||
## Widgets
|
||||
## Built with Ratatui
|
||||
|
||||
### Built in
|
||||
|
||||
The library comes with the following
|
||||
[widgets](https://docs.rs/ratatui/latest/ratatui/widgets/index.html):
|
||||
|
||||
- [BarChart](https://docs.rs/ratatui/latest/ratatui/widgets/struct.BarChart.html)
|
||||
- [Block](https://docs.rs/ratatui/latest/ratatui/widgets/block/struct.Block.html)
|
||||
- [Calendar](https://docs.rs/ratatui/latest/ratatui/widgets/calendar/index.html)
|
||||
- [Canvas](https://docs.rs/ratatui/latest/ratatui/widgets/canvas/struct.Canvas.html) which allows
|
||||
rendering [points, lines, shapes and a world
|
||||
map](https://docs.rs/ratatui/latest/ratatui/widgets/canvas/index.html)
|
||||
- [Chart](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Chart.html)
|
||||
- [Clear](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Clear.html)
|
||||
- [Gauge](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Gauge.html)
|
||||
- [List](https://docs.rs/ratatui/latest/ratatui/widgets/struct.List.html)
|
||||
- [Paragraph](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Paragraph.html)
|
||||
- [Scrollbar](https://docs.rs/ratatui/latest/ratatui/widgets/scrollbar/struct.Scrollbar.html)
|
||||
- [Sparkline](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Sparkline.html)
|
||||
- [Table](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Table.html)
|
||||
- [Tabs](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Tabs.html)
|
||||
|
||||
Each widget has an associated example which can be found in the [Examples] folder. Run each example
|
||||
with cargo (e.g. to run the gauge example `cargo run --example gauge`), and quit by pressing `q`.
|
||||
|
||||
You can also run all examples by running `cargo make run-examples` (requires `cargo-make` that can
|
||||
be installed with `cargo install cargo-make`).
|
||||
|
||||
### Third-party libraries, bootstrapping templates and widgets
|
||||
|
||||
- [ansi-to-tui](https://github.com/uttarayan21/ansi-to-tui) — Convert ansi colored text to
|
||||
`ratatui::text::Text`
|
||||
- [color-to-tui](https://github.com/uttarayan21/color-to-tui) — Parse hex colors to
|
||||
`ratatui::style::Color`
|
||||
- [templates](https://github.com/ratatui-org/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-org/awesome-ratatui) for a curated list of
|
||||
awesome apps/libraries built with `ratatui`!
|
||||
Ratatui has a number of built-in [widgets](https://docs.rs/ratatui/latest/ratatui/widgets/), as well
|
||||
as many contributed by external contributors. Check out the [Showcase](https://ratatui.rs/showcase/)
|
||||
section of the website, or the [awesome-ratatui](https://github.com/ratatui/awesome-ratatui) repo
|
||||
for a curated list of awesome apps/libraries built with `ratatui`!
|
||||
|
||||
## Alternatives
|
||||
|
||||
You might want to checkout [Cursive](https://github.com/gyscos/Cursive) for an alternative solution
|
||||
You might want to checkout [Cursive](https://github.com/gyscos/Cursive) or
|
||||
[iocraft](https://github.com/ccbrown/iocraft/) for an alternative solutions
|
||||
to build text user interfaces in Rust.
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
None of this could be possible without [**Florian Dehau**](https://github.com/fdehau) who originally
|
||||
created [tui-rs] which inspired many Rust TUIs.
|
||||
|
||||
Special thanks to [**Pavel Fomchenkov**](https://github.com/nawok) for his work in designing **an
|
||||
awesome logo** for the ratatui project and ratatui-org organization.
|
||||
awesome logo** for the ratatui project and ratatui organization.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
19
RELEASE.md
19
RELEASE.md
@@ -1,5 +1,12 @@
|
||||
# Creating a Release
|
||||
|
||||
Our release strategy is:
|
||||
|
||||
> Release major versions with detailed summaries when necessary, while releasing minor versions
|
||||
> weekly or as needed without extensive announcements.
|
||||
>
|
||||
> Versioning scheme being `0.x.y`, where `x` is the major version and `y` is the minor version.
|
||||
|
||||
[crates.io](https://crates.io/crates/ratatui) releases are automated via [GitHub
|
||||
actions](.github/workflows/cd.yml) and triggered by pushing a tag.
|
||||
|
||||
@@ -12,7 +19,7 @@ actions](.github/workflows/cd.yml) and triggered by pushing a tag.
|
||||
```
|
||||
|
||||
1. Switch branches to the images branch and copy demo2.gif to examples/, commit, and push.
|
||||
1. Grab the permalink from <https://github.com/ratatui-org/ratatui/blob/images/examples/demo2.gif> and
|
||||
1. Grab the permalink from <https://github.com/ratatui/ratatui/blob/images/examples/demo2.gif> and
|
||||
append `?raw=true` to redirect to the actual image url. Then update the link in the main README.
|
||||
Avoid adding the gif to the git repo as binary files tend to bloat repositories.
|
||||
|
||||
@@ -21,16 +28,16 @@ actions](.github/workflows/cd.yml) and triggered by pushing a tag.
|
||||
can be used for generating the entries.
|
||||
1. Ensure that any breaking changes are documented in [BREAKING-CHANGES.md](./BREAKING-CHANGES.md)
|
||||
1. Commit and push the changes.
|
||||
1. Create a new tag: `git tag -a v[X.Y.Z]`
|
||||
1. Create a new tag: `git tag -a v[0.x.y]`
|
||||
1. Push the tag: `git push --tags`
|
||||
1. Wait for [Continuous Deployment](https://github.com/ratatui-org/ratatui/actions) workflow to
|
||||
1. Wait for [Continuous Deployment](https://github.com/ratatui/ratatui/actions) workflow to
|
||||
finish.
|
||||
|
||||
## Alpha Releases
|
||||
|
||||
Alpha releases are automatically released every Saturday via [cd.yml](./.github/workflows/cd.yml)
|
||||
and can be manually be created when necessary by triggering the [Continuous
|
||||
Deployment](https://github.com/ratatui-org/ratatui/actions/workflows/cd.yml) workflow.
|
||||
Deployment](https://github.com/ratatui/ratatui/actions/workflows/cd.yml) workflow.
|
||||
|
||||
We automatically release an alpha release with a patch level bump + alpha.num weekly (and when we
|
||||
need to manually). E.g. the last release was 0.22.0, and the most recent alpha release is
|
||||
@@ -40,5 +47,5 @@ These releases will have whatever happened to be in main at the time of release,
|
||||
for apps that need to get releases from crates.io, but may contain more bugs and be generally less
|
||||
tested than normal releases.
|
||||
|
||||
See [#147](https://github.com/ratatui-org/ratatui/issues/147) and
|
||||
[#359](https://github.com/ratatui-org/ratatui/pull/359) for more info on the alpha release process.
|
||||
See [#147](https://github.com/ratatui/ratatui/issues/147) and
|
||||
[#359](https://github.com/ratatui/ratatui/pull/359) for more info on the alpha release process.
|
||||
|
||||
@@ -6,4 +6,4 @@ We only support the latest version of this crate.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
To report secuirity vulnerability, please use the form at <https://github.com/ratatui-org/ratatui/security/advisories/new>
|
||||
To report secuirity vulnerability, please use the form at <https://github.com/ratatui/ratatui/security/advisories/new>
|
||||
|
||||
97
bacon.toml
97
bacon.toml
@@ -8,58 +8,66 @@
|
||||
default_job = "check"
|
||||
|
||||
[jobs.check]
|
||||
command = ["cargo", "check", "--all-features", "--color", "always"]
|
||||
command = ["cargo", "check", "--all-features"]
|
||||
need_stdout = false
|
||||
|
||||
[jobs.check-all]
|
||||
command = ["cargo", "check", "--all-targets", "--all-features", "--color", "always"]
|
||||
command = ["cargo", "check", "--all-targets", "--all-features"]
|
||||
need_stdout = false
|
||||
|
||||
[jobs.check-crossterm]
|
||||
command = ["cargo", "check", "--color", "always", "--all-targets", "--no-default-features", "--features", "crossterm"]
|
||||
command = [
|
||||
"cargo",
|
||||
"check",
|
||||
"--all-targets",
|
||||
"--no-default-features",
|
||||
"--features",
|
||||
"crossterm",
|
||||
]
|
||||
need_stdout = false
|
||||
|
||||
[jobs.check-termion]
|
||||
command = ["cargo", "check", "--color", "always", "--all-targets", "--no-default-features", "--features", "termion"]
|
||||
command = [
|
||||
"cargo",
|
||||
"check",
|
||||
"--all-targets",
|
||||
"--no-default-features",
|
||||
"--features",
|
||||
"termion",
|
||||
]
|
||||
need_stdout = false
|
||||
|
||||
[jobs.check-termwiz]
|
||||
command = ["cargo", "check", "--color", "always", "--all-targets", "--no-default-features", "--features", "termwiz"]
|
||||
command = [
|
||||
"cargo",
|
||||
"check",
|
||||
"--all-targets",
|
||||
"--no-default-features",
|
||||
"--features",
|
||||
"termwiz",
|
||||
]
|
||||
need_stdout = false
|
||||
|
||||
[jobs.clippy]
|
||||
command = [
|
||||
"cargo", "clippy",
|
||||
"--all-targets",
|
||||
"--color", "always",
|
||||
]
|
||||
command = ["cargo", "clippy", "--all-targets"]
|
||||
need_stdout = false
|
||||
|
||||
[jobs.test]
|
||||
command = [
|
||||
"cargo", "test",
|
||||
"--all-features",
|
||||
"--color", "always",
|
||||
"--", "--color", "always", # see https://github.com/Canop/bacon/issues/124
|
||||
]
|
||||
command = ["cargo", "test", "--all-features"]
|
||||
need_stdout = true
|
||||
|
||||
[jobs.test-unit]
|
||||
command = [
|
||||
"cargo", "test",
|
||||
"--lib",
|
||||
"--all-features",
|
||||
"--color", "always",
|
||||
"--", "--color", "always", # see https://github.com/Canop/bacon/issues/124
|
||||
]
|
||||
command = ["cargo", "test", "--lib", "--all-features"]
|
||||
need_stdout = true
|
||||
|
||||
[jobs.doc]
|
||||
command = [
|
||||
"cargo", "+nightly", "doc",
|
||||
"-Zunstable-options", "-Zrustdoc-scrape-examples",
|
||||
"cargo",
|
||||
"+nightly",
|
||||
"doc",
|
||||
"-Zunstable-options",
|
||||
"-Zrustdoc-scrape-examples",
|
||||
"--all-features",
|
||||
"--color", "always",
|
||||
"--no-deps",
|
||||
]
|
||||
env.RUSTDOCFLAGS = "--cfg docsrs"
|
||||
@@ -69,10 +77,12 @@ need_stdout = false
|
||||
# to the previous job
|
||||
[jobs.doc-open]
|
||||
command = [
|
||||
"cargo", "+nightly", "doc",
|
||||
"-Zunstable-options", "-Zrustdoc-scrape-examples",
|
||||
"cargo",
|
||||
"+nightly",
|
||||
"doc",
|
||||
"-Zunstable-options",
|
||||
"-Zrustdoc-scrape-examples",
|
||||
"--all-features",
|
||||
"--color", "always",
|
||||
"--no-deps",
|
||||
"--open",
|
||||
]
|
||||
@@ -82,19 +92,34 @@ on_success = "job:doc" # so that we don't open the browser at each change
|
||||
|
||||
[jobs.coverage]
|
||||
command = [
|
||||
"cargo", "llvm-cov",
|
||||
"--lcov", "--output-path", "target/lcov.info",
|
||||
"cargo",
|
||||
"llvm-cov",
|
||||
"--lcov",
|
||||
"--output-path",
|
||||
"target/lcov.info",
|
||||
"--all-features",
|
||||
"--color", "always",
|
||||
]
|
||||
|
||||
[jobs.coverage-unit-tests-only]
|
||||
command = [
|
||||
"cargo", "llvm-cov",
|
||||
"--lcov", "--output-path", "target/lcov.info",
|
||||
"cargo",
|
||||
"llvm-cov",
|
||||
"--lcov",
|
||||
"--output-path",
|
||||
"target/lcov.info",
|
||||
"--lib",
|
||||
"--all-features",
|
||||
"--color", "always",
|
||||
]
|
||||
|
||||
[jobs.hack]
|
||||
command = [
|
||||
"cargo",
|
||||
"hack",
|
||||
"test",
|
||||
"--lib",
|
||||
"--each-feature",
|
||||
# "--all-targets",
|
||||
"--workspace",
|
||||
]
|
||||
|
||||
# You may define here keybindings that would be specific to
|
||||
@@ -102,7 +127,7 @@ command = [
|
||||
# Shortcuts to internal functions (scrolling, toggling, etc.)
|
||||
# should go in your personal global prefs.toml file instead.
|
||||
[keybindings]
|
||||
# alt-m = "job:my-job"
|
||||
ctrl-h = "job:hack"
|
||||
ctrl-c = "job:check-crossterm"
|
||||
ctrl-t = "job:check-termion"
|
||||
ctrl-w = "job:check-termwiz"
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
use criterion::{black_box, criterion_group, BenchmarkId, Criterion};
|
||||
use ratatui::layout::Rect;
|
||||
|
||||
fn rect_rows_benchmark(c: &mut Criterion) {
|
||||
let rect_sizes = vec![
|
||||
Rect::new(0, 0, 1, 16),
|
||||
Rect::new(0, 0, 1, 1024),
|
||||
Rect::new(0, 0, 1, 65535),
|
||||
];
|
||||
let mut group = c.benchmark_group("rect_rows");
|
||||
for rect in rect_sizes {
|
||||
group.bench_with_input(BenchmarkId::new("rows", rect.height), &rect, |b, rect| {
|
||||
b.iter(|| {
|
||||
for row in rect.rows() {
|
||||
// Perform any necessary operations on each row
|
||||
black_box(row);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(benches, rect_rows_benchmark);
|
||||
20
cliff.toml
20
cliff.toml
@@ -2,7 +2,7 @@
|
||||
# https://git-cliff.org/docs/configuration
|
||||
|
||||
[remote.github]
|
||||
owner = "ratatui-org"
|
||||
owner = "ratatui"
|
||||
repo = "ratatui"
|
||||
|
||||
[changelog]
|
||||
@@ -24,25 +24,15 @@ body = """
|
||||
{%- if not version %}
|
||||
## [unreleased]
|
||||
{% else -%}
|
||||
## [{{ version }}](https://github.com/ratatui-org/ratatui/releases/tag/{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||
## [{{ version }}](https://github.com/ratatui/ratatui/releases/tag/{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||
{% endif -%}
|
||||
|
||||
{% macro commit(commit) -%}
|
||||
- [{{ commit.id | truncate(length=7, end="") }}]({{ "https://github.com/ratatui-org/ratatui/commit/" ~ commit.id }}) \
|
||||
- [{{ commit.id | truncate(length=7, end="") }}]({{ "https://github.com/ratatui/ratatui/commit/" ~ commit.id }}) \
|
||||
*({{commit.scope | default(value = "uncategorized") | lower }})* {{ commit.message | upper_first | trim }}\
|
||||
{% if commit.github.username %} by @{{ commit.github.username }}{%- endif -%}\
|
||||
{% if commit.github.pr_number %} in [#{{ commit.github.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.github.pr_number }}){%- endif %}\
|
||||
{% if commit.remote.username %} by @{{ commit.remote.username }}{%- endif -%}\
|
||||
{% if commit.remote.pr_number %} in [#{{ commit.remote.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.remote.pr_number }}){%- endif %}\
|
||||
{%- if commit.breaking %} [**breaking**]{% endif %}
|
||||
{%- if commit.body %}\n\n{{ commit.body | indent(prefix=" > ", first=true, blank=true) }}
|
||||
{%- endif %}
|
||||
{%- for footer in commit.footers %}\n
|
||||
{%- if footer.token != "Signed-off-by" and footer.token != "Co-authored-by" %}
|
||||
>
|
||||
{{ footer.token | indent(prefix=" > ", first=true, blank=true) }}
|
||||
{{- footer.separator }}
|
||||
{{- footer.value| indent(prefix=" > ", first=false, blank=true) }}
|
||||
{%- endif %}
|
||||
{%- endfor %}
|
||||
{% endmacro -%}
|
||||
|
||||
{% for group, commits in commits | group_by(attribute="group") %}
|
||||
|
||||
@@ -3,9 +3,15 @@ avoid-breaking-exported-api = false
|
||||
# https://rust-lang.github.io/rust-clippy/master/index.html#/multiple_crate_versions
|
||||
# ratatui -> bitflags v2.3
|
||||
# termwiz -> wezterm-blob-leases -> mac_address -> nix -> bitflags v1.3.2
|
||||
# crossterm -> all the windows- deps https://github.com/ratatui-org/ratatui/pull/1064#issuecomment-2078848980
|
||||
# (also, memoffset, syn, nix, strsim, windows-sys
|
||||
# crossterm -> all the windows- deps https://github.com/ratatui/ratatui/pull/1064#issuecomment-2078848980
|
||||
allowed-duplicate-crates = [
|
||||
"bitflags",
|
||||
"memoffset",
|
||||
"nix",
|
||||
"strsim",
|
||||
"syn",
|
||||
"windows-sys",
|
||||
"windows-targets",
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
@@ -14,4 +20,5 @@ allowed-duplicate-crates = [
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
@@ -11,6 +11,7 @@ allow = [
|
||||
"MIT",
|
||||
"Unicode-DFS-2016",
|
||||
"WTFPL",
|
||||
"Zlib",
|
||||
]
|
||||
|
||||
[advisories]
|
||||
|
||||
@@ -1,199 +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-org/ratatui
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
use color_eyre::Result;
|
||||
use rand::{thread_rng, Rng};
|
||||
use ratatui::{
|
||||
crossterm::event::{self, Event, KeyCode},
|
||||
layout::{Constraint, Direction, Layout},
|
||||
prelude::{Color, Line, Style, Stylize},
|
||||
widgets::{Bar, BarChart, BarGroup, Block},
|
||||
};
|
||||
|
||||
use self::terminal::Terminal;
|
||||
|
||||
struct App {
|
||||
should_exit: bool,
|
||||
temperatures: Vec<u8>,
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
let mut terminal = terminal::init()?;
|
||||
let app = App::new();
|
||||
app.run(&mut terminal)?;
|
||||
terminal::restore()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn new() -> Self {
|
||||
let mut rng = thread_rng();
|
||||
let temperatures = (0..24).map(|_| rng.gen_range(50..90)).collect();
|
||||
Self {
|
||||
should_exit: false,
|
||||
temperatures,
|
||||
}
|
||||
}
|
||||
|
||||
fn run(mut self, terminal: &mut Terminal) -> Result<()> {
|
||||
while !self.should_exit {
|
||||
self.draw(terminal)?;
|
||||
self.handle_events()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw(&self, terminal: &mut Terminal) -> Result<()> {
|
||||
terminal.draw(|frame| self.render(frame))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_events(&mut self) -> Result<()> {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.code == KeyCode::Char('q') {
|
||||
self.should_exit = true;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(&self, frame: &mut ratatui::Frame) {
|
||||
let [title, vertical, horizontal] = Layout::vertical([
|
||||
Constraint::Length(1),
|
||||
Constraint::Fill(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);
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a vertical bar chart from the temperatures data.
|
||||
fn vertical_barchart(temperatures: &[u8]) -> BarChart {
|
||||
let bars: Vec<Bar> = temperatures
|
||||
.iter()
|
||||
.map(|v| u64::from(*v))
|
||||
.enumerate()
|
||||
.map(|(i, value)| {
|
||||
Bar::default()
|
||||
.value(value)
|
||||
.label(Line::from(format!("{i:>02}:00")))
|
||||
.text_value(format!("{value:>3}°"))
|
||||
.style(temperature_style(value))
|
||||
.value_style(temperature_style(value).reversed())
|
||||
})
|
||||
.collect();
|
||||
let title = Line::from("Weather (Vertical)").centered();
|
||||
BarChart::default()
|
||||
.data(BarGroup::default().bars(&bars))
|
||||
.block(Block::new().title(title))
|
||||
.bar_width(5)
|
||||
}
|
||||
|
||||
/// Create a horizontal bar chart from the temperatures data.
|
||||
fn horizontal_barchart(temperatures: &[u8]) -> BarChart {
|
||||
let bars: Vec<Bar> = temperatures
|
||||
.iter()
|
||||
.map(|v| u64::from(*v))
|
||||
.enumerate()
|
||||
.map(|(i, value)| {
|
||||
let style = temperature_style(value);
|
||||
Bar::default()
|
||||
.value(value)
|
||||
.label(Line::from(format!("{i:>02}:00")))
|
||||
.text_value(format!("{value:>3}°"))
|
||||
.style(style)
|
||||
.value_style(style.reversed())
|
||||
})
|
||||
.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)
|
||||
}
|
||||
|
||||
/// create a yellow to red value based on the value (50-90)
|
||||
fn temperature_style(value: u64) -> Style {
|
||||
let green = (255.0 * (1.0 - (value - 50) as f64 / 40.0)) as u8;
|
||||
let color = Color::Rgb(255, green, 0);
|
||||
Style::new().fg(color)
|
||||
}
|
||||
|
||||
/// Contains functions common to all examples
|
||||
mod terminal {
|
||||
use std::{
|
||||
io::{self, stdout, Stdout},
|
||||
panic,
|
||||
};
|
||||
|
||||
use ratatui::{
|
||||
backend::CrosstermBackend,
|
||||
crossterm::{
|
||||
execute,
|
||||
terminal::{
|
||||
disable_raw_mode, enable_raw_mode, Clear, ClearType, EnterAlternateScreen,
|
||||
LeaveAlternateScreen,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// A type alias to simplify the usage of the terminal and make it easier to change the backend
|
||||
// or choice of writer.
|
||||
pub type Terminal = ratatui::Terminal<CrosstermBackend<Stdout>>;
|
||||
|
||||
/// Initialize the terminal by enabling raw mode and entering the alternate screen.
|
||||
///
|
||||
/// This function should be called before the program starts to ensure that the terminal is in
|
||||
/// the correct state for the application.
|
||||
pub fn init() -> io::Result<Terminal> {
|
||||
install_panic_hook();
|
||||
enable_raw_mode()?;
|
||||
execute!(stdout(), EnterAlternateScreen)?;
|
||||
let backend = CrosstermBackend::new(stdout());
|
||||
let terminal = Terminal::new(backend)?;
|
||||
Ok(terminal)
|
||||
}
|
||||
|
||||
/// Restore the terminal by leaving the alternate screen and disabling raw mode.
|
||||
///
|
||||
/// This function should be called before the program exits to ensure that the terminal is
|
||||
/// restored to its original state.
|
||||
pub fn restore() -> io::Result<()> {
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
stdout(),
|
||||
LeaveAlternateScreen,
|
||||
Clear(ClearType::FromCursorDown),
|
||||
)
|
||||
}
|
||||
|
||||
/// Install a panic hook that restores the terminal before printing the panic.
|
||||
///
|
||||
/// This prevents error messages from being messed up by the terminal state.
|
||||
fn install_panic_hook() {
|
||||
let panic_hook = panic::take_hook();
|
||||
panic::set_hook(Box::new(move |panic_info| {
|
||||
let _ = restore();
|
||||
panic_hook(panic_info);
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -1,258 +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-org/ratatui
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
use std::{
|
||||
error::Error,
|
||||
io::{stdout, Stdout},
|
||||
ops::ControlFlow,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use itertools::Itertools;
|
||||
use ratatui::{
|
||||
backend::CrosstermBackend,
|
||||
crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
},
|
||||
layout::{Alignment, Constraint, Layout, Rect},
|
||||
style::{Style, Stylize},
|
||||
text::Line,
|
||||
widgets::{
|
||||
block::{Position, Title},
|
||||
Block, BorderType, Borders, Padding, Paragraph, Wrap,
|
||||
},
|
||||
Frame,
|
||||
};
|
||||
|
||||
// These type aliases are used to make the code more readable by reducing repetition of the generic
|
||||
// types. They are not necessary for the functionality of the code.
|
||||
type Terminal = ratatui::Terminal<CrosstermBackend<Stdout>>;
|
||||
type Result<T> = std::result::Result<T, Box<dyn Error>>;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let mut terminal = setup_terminal()?;
|
||||
let result = run(&mut terminal);
|
||||
restore_terminal(terminal)?;
|
||||
|
||||
if let Err(err) = result {
|
||||
eprintln!("{err:?}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn setup_terminal() -> Result<Terminal> {
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = stdout();
|
||||
execute!(stdout, EnterAlternateScreen)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let terminal = Terminal::new(backend)?;
|
||||
Ok(terminal)
|
||||
}
|
||||
|
||||
fn restore_terminal(mut terminal: Terminal) -> Result<()> {
|
||||
disable_raw_mode()?;
|
||||
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run(terminal: &mut Terminal) -> Result<()> {
|
||||
loop {
|
||||
terminal.draw(ui)?;
|
||||
if handle_events()?.is_break() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_events() -> Result<ControlFlow<()>> {
|
||||
if event::poll(Duration::from_millis(100))? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.code == KeyCode::Char('q') {
|
||||
return Ok(ControlFlow::Break(()));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(ControlFlow::Continue(()))
|
||||
}
|
||||
|
||||
fn ui(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_vec();
|
||||
(title_area, main_areas)
|
||||
}
|
||||
|
||||
fn render_title(frame: &mut Frame, area: Rect) {
|
||||
frame.render_widget(
|
||||
Paragraph::new("Block example. Press q to quit")
|
||||
.dark_gray()
|
||||
.alignment(Alignment::Center),
|
||||
area,
|
||||
);
|
||||
}
|
||||
|
||||
fn placeholder_paragraph() -> Paragraph<'static> {
|
||||
let text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.";
|
||||
Paragraph::new(text.dark_gray()).wrap(Wrap { trim: true })
|
||||
}
|
||||
|
||||
fn render_borders(paragraph: &Paragraph, border: Borders, frame: &mut Frame, area: Rect) {
|
||||
let block = Block::new()
|
||||
.borders(border)
|
||||
.title(format!("Borders::{border:#?}"));
|
||||
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);
|
||||
}
|
||||
|
||||
// Note: this currently renders incorrectly, see https://github.com/ratatui-org/ratatui/issues/349
|
||||
fn render_styled_title(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
|
||||
let block = Block::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,18 +0,0 @@
|
||||
use color_eyre::{config::HookBuilder, Result};
|
||||
|
||||
use crate::term;
|
||||
|
||||
pub fn init_hooks() -> Result<()> {
|
||||
let (panic, error) = HookBuilder::default().into_hooks();
|
||||
let panic = panic.into_panic_hook();
|
||||
let error = error.into_eyre_hook();
|
||||
color_eyre::eyre::set_hook(Box::new(move |e| {
|
||||
let _ = term::restore();
|
||||
error(e)
|
||||
}))?;
|
||||
std::panic::set_hook(Box::new(move |info| {
|
||||
let _ = term::restore();
|
||||
panic(info);
|
||||
}));
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
//! # [Ratatui] Demo2 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-org/ratatui
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
#![allow(
|
||||
clippy::missing_errors_doc,
|
||||
clippy::module_name_repetitions,
|
||||
clippy::must_use_candidate
|
||||
)]
|
||||
|
||||
mod app;
|
||||
mod colors;
|
||||
mod destroy;
|
||||
mod errors;
|
||||
mod tabs;
|
||||
mod term;
|
||||
mod theme;
|
||||
|
||||
use color_eyre::Result;
|
||||
|
||||
pub use self::{
|
||||
colors::{color_from_oklab, RgbSwatch},
|
||||
theme::THEME,
|
||||
};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
errors::init_hooks()?;
|
||||
let terminal = &mut term::init()?;
|
||||
app::run(terminal)?;
|
||||
term::restore()?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
use std::{
|
||||
io::{self, stdout},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use color_eyre::{eyre::WrapErr, Result};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
crossterm::{
|
||||
event::{self, Event},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
},
|
||||
layout::Rect,
|
||||
Terminal, TerminalOptions, Viewport,
|
||||
};
|
||||
|
||||
pub fn init() -> Result<Terminal<impl Backend>> {
|
||||
// this size is to match the size of the terminal when running the demo
|
||||
// using vhs in a 1280x640 sized window (github social preview size)
|
||||
let options = TerminalOptions {
|
||||
viewport: Viewport::Fixed(Rect::new(0, 0, 81, 18)),
|
||||
};
|
||||
let terminal = Terminal::with_options(CrosstermBackend::new(io::stdout()), options)?;
|
||||
enable_raw_mode().context("enable raw mode")?;
|
||||
stdout()
|
||||
.execute(EnterAlternateScreen)
|
||||
.wrap_err("enter alternate screen")?;
|
||||
Ok(terminal)
|
||||
}
|
||||
|
||||
pub fn restore() -> Result<()> {
|
||||
disable_raw_mode().context("disable raw mode")?;
|
||||
stdout()
|
||||
.execute(LeaveAlternateScreen)
|
||||
.wrap_err("leave alternate screen")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn next_event(timeout: Duration) -> Result<Option<Event>> {
|
||||
if !event::poll(timeout)? {
|
||||
return Ok(None);
|
||||
}
|
||||
let event = event::read()?;
|
||||
Ok(Some(event))
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
//! # [Ratatui] Hello World 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-org/ratatui
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
use std::{
|
||||
io::{self, Stdout},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use color_eyre::{eyre::Context, Result};
|
||||
use ratatui::{
|
||||
backend::CrosstermBackend,
|
||||
crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
},
|
||||
widgets::Paragraph,
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
/// This is a bare minimum example. There are many approaches to running an application loop, so
|
||||
/// this is not meant to be prescriptive. It is only meant to demonstrate the basic setup and
|
||||
/// teardown of a terminal application.
|
||||
///
|
||||
/// This example does not handle events or update the application state. It just draws a greeting
|
||||
/// and exits when the user presses 'q'.
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?; // augment errors / panics with easy to read messages
|
||||
let mut terminal = init_terminal().context("setup failed")?;
|
||||
let result = run(&mut terminal).context("app loop failed");
|
||||
restore_terminal();
|
||||
result
|
||||
}
|
||||
|
||||
/// Setup the terminal. This is where you would enable raw mode, enter the alternate screen, and
|
||||
/// hide the cursor. This example does not handle errors.
|
||||
fn init_terminal() -> Result<Terminal<CrosstermBackend<Stdout>>> {
|
||||
set_panic_hook();
|
||||
let mut stdout = io::stdout();
|
||||
enable_raw_mode().context("failed to enable raw mode")?;
|
||||
execute!(stdout, EnterAlternateScreen).context("unable to enter alternate screen")?;
|
||||
Terminal::new(CrosstermBackend::new(stdout)).context("creating terminal failed")
|
||||
}
|
||||
|
||||
/// Restore the terminal. This is where you disable raw mode, leave the alternate screen, and show
|
||||
/// the cursor.
|
||||
fn restore_terminal() {
|
||||
// There's not a lot we can do if these fail, so we just print an error message.
|
||||
if let Err(err) = disable_raw_mode() {
|
||||
eprintln!("Error disabling raw mode: {err}");
|
||||
}
|
||||
if let Err(err) = execute!(io::stdout(), LeaveAlternateScreen) {
|
||||
eprintln!("Error leaving alternate screen: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Replace the default panic hook with one that restores the terminal before panicking.
|
||||
fn set_panic_hook() {
|
||||
let hook = std::panic::take_hook();
|
||||
std::panic::set_hook(Box::new(move |panic_info| {
|
||||
restore_terminal();
|
||||
hook(panic_info);
|
||||
}));
|
||||
}
|
||||
|
||||
/// Run the application loop. This is where you would handle events and update the application
|
||||
/// state. This example exits when the user presses 'q'. Other styles of application loops are
|
||||
/// possible, for example, you could have multiple application states and switch between them based
|
||||
/// on events, or you could have a single application state and update it based on events.
|
||||
fn run(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
|
||||
loop {
|
||||
terminal.draw(render_app)?;
|
||||
if should_quit()? {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Render the application. This is where you would draw the application UI. This example just
|
||||
/// draws a greeting.
|
||||
fn render_app(frame: &mut Frame) {
|
||||
let greeting = Paragraph::new("Hello World! (press 'q' to quit)");
|
||||
frame.render_widget(greeting, frame.area());
|
||||
}
|
||||
|
||||
/// Check if the user has pressed 'q'. This is where you would handle events. This example just
|
||||
/// checks if the user has pressed 'q' and returns true if they have. It does not handle any other
|
||||
/// events. There is a 250ms timeout on the event poll so that the application can exit in a timely
|
||||
/// manner, and to ensure that the terminal is rendered at least once every 250ms. This allows you
|
||||
/// to do other work in the application loop, such as updating the application state, without
|
||||
/// blocking the event loop for too long.
|
||||
fn should_quit() -> Result<bool> {
|
||||
if event::poll(Duration::from_millis(250)).context("event poll failed")? {
|
||||
if let Event::Key(key) = event::read().context("event read failed")? {
|
||||
return Ok(KeyCode::Char('q') == key.code);
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
//! # [Ratatui] Minimal 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-org/ratatui
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
use ratatui::{
|
||||
backend::CrosstermBackend,
|
||||
crossterm::{
|
||||
event::{self, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
},
|
||||
text::Text,
|
||||
Terminal,
|
||||
};
|
||||
|
||||
/// This is a bare minimum example. There are many approaches to running an application loop, so
|
||||
/// this is not meant to be prescriptive. See the [examples] folder for more complete examples.
|
||||
/// In particular, the [hello-world] example is a good starting point.
|
||||
///
|
||||
/// [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
/// [hello-world]: https://github.com/ratatui-org/ratatui/blob/main/examples/hello_world.rs
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut terminal = Terminal::new(CrosstermBackend::new(std::io::stdout()))?;
|
||||
enable_raw_mode()?;
|
||||
execute!(terminal.backend_mut(), EnterAlternateScreen)?;
|
||||
loop {
|
||||
terminal.draw(|frame| frame.render_widget(Text::raw("Hello World!"), frame.area()))?;
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
disable_raw_mode()?;
|
||||
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
//! # [Ratatui] Panic Hook 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-org/ratatui
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
//! How to use a panic hook to reset the terminal before printing the panic to
|
||||
//! the terminal.
|
||||
//!
|
||||
//! When exiting normally or when handling `Result::Err`, we can reset the
|
||||
//! terminal manually at the end of `main` just before we print the error.
|
||||
//!
|
||||
//! Because a panic interrupts the normal control flow, manually resetting the
|
||||
//! terminal at the end of `main` won't do us any good. Instead, we need to
|
||||
//! make sure to set up a panic hook that first resets the terminal before
|
||||
//! handling the panic. This both reuses the standard panic hook to ensure a
|
||||
//! consistent panic handling UX and properly resets the terminal to not
|
||||
//! distort the output.
|
||||
//!
|
||||
//! That's why this example is set up to show both situations, with and without
|
||||
//! the chained panic hook, to see the difference.
|
||||
|
||||
use std::{error::Error, io};
|
||||
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
},
|
||||
text::Line,
|
||||
widgets::{Block, Paragraph},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
type Result<T> = std::result::Result<T, Box<dyn Error>>;
|
||||
|
||||
#[derive(Default)]
|
||||
struct App {
|
||||
hook_enabled: bool,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn chain_hook(&mut self) {
|
||||
let original_hook = std::panic::take_hook();
|
||||
|
||||
std::panic::set_hook(Box::new(move |panic| {
|
||||
reset_terminal().unwrap();
|
||||
original_hook(panic);
|
||||
}));
|
||||
|
||||
self.hook_enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let mut terminal = init_terminal()?;
|
||||
|
||||
let mut app = App::default();
|
||||
let res = run_tui(&mut terminal, &mut app);
|
||||
|
||||
reset_terminal()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Initializes the terminal.
|
||||
fn init_terminal() -> Result<Terminal<CrosstermBackend<io::Stdout>>> {
|
||||
crossterm::execute!(io::stdout(), EnterAlternateScreen)?;
|
||||
enable_raw_mode()?;
|
||||
|
||||
let backend = CrosstermBackend::new(io::stdout());
|
||||
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
terminal.hide_cursor()?;
|
||||
|
||||
Ok(terminal)
|
||||
}
|
||||
|
||||
/// Resets the terminal.
|
||||
fn reset_terminal() -> Result<()> {
|
||||
disable_raw_mode()?;
|
||||
crossterm::execute!(io::stdout(), LeaveAlternateScreen)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Runs the TUI loop.
|
||||
fn run_tui<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) -> io::Result<()> {
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, app))?;
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
KeyCode::Char('p') => {
|
||||
panic!("intentional demo panic");
|
||||
}
|
||||
|
||||
KeyCode::Char('e') => {
|
||||
app.chain_hook();
|
||||
}
|
||||
|
||||
_ => {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Render the TUI.
|
||||
fn ui(f: &mut Frame, app: &App) {
|
||||
let text = vec![
|
||||
if app.hook_enabled {
|
||||
Line::from("HOOK IS CURRENTLY **ENABLED**")
|
||||
} else {
|
||||
Line::from("HOOK IS CURRENTLY **DISABLED**")
|
||||
},
|
||||
Line::from(""),
|
||||
Line::from("press `p` to panic"),
|
||||
Line::from("press `e` to enable the terminal-resetting panic hook"),
|
||||
Line::from("press any other key to quit without panic"),
|
||||
Line::from(""),
|
||||
Line::from("when you panic without the chained hook,"),
|
||||
Line::from("you will likely have to reset your terminal afterwards"),
|
||||
Line::from("with the `reset` command"),
|
||||
Line::from(""),
|
||||
Line::from("with the chained panic hook enabled,"),
|
||||
Line::from("you should see the panic report as you would without ratatui"),
|
||||
Line::from(""),
|
||||
Line::from("try first without the panic handler to see the difference"),
|
||||
];
|
||||
|
||||
let paragraph = Paragraph::new(text)
|
||||
.block(Block::bordered().title("Panic Handler Demo"))
|
||||
.centered();
|
||||
|
||||
f.render_widget(paragraph, f.area());
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
//! # [Ratatui] Popup 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-org/ratatui
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
// See also https://github.com/joshka/tui-popup and
|
||||
// https://github.com/sephiroth74/tui-confirm-dialog
|
||||
|
||||
use std::{error::Error, io};
|
||||
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
},
|
||||
layout::{Constraint, Layout, Rect},
|
||||
style::Stylize,
|
||||
widgets::{Block, Clear, Paragraph, Wrap},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
struct App {
|
||||
show_popup: bool,
|
||||
}
|
||||
|
||||
impl App {
|
||||
const fn new() -> Self {
|
||||
Self { show_popup: false }
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// create app and run it
|
||||
let app = App::new();
|
||||
let res = run_app(&mut terminal, app);
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<()> {
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &app))?;
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Char('p') => app.show_popup = !app.show_popup,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui(f: &mut Frame, app: &App) {
|
||||
let area = f.area();
|
||||
|
||||
let vertical = Layout::vertical([Constraint::Percentage(20), Constraint::Percentage(80)]);
|
||||
let [instructions, content] = vertical.areas(area);
|
||||
|
||||
let text = if app.show_popup {
|
||||
"Press p to close the popup"
|
||||
} else {
|
||||
"Press p to show the popup"
|
||||
};
|
||||
let paragraph = Paragraph::new(text.slow_blink())
|
||||
.centered()
|
||||
.wrap(Wrap { trim: true });
|
||||
f.render_widget(paragraph, instructions);
|
||||
|
||||
let block = Block::bordered().title("Content").on_blue();
|
||||
f.render_widget(block, content);
|
||||
|
||||
if app.show_popup {
|
||||
let block = Block::bordered().title("Popup");
|
||||
let area = centered_rect(60, 20, area);
|
||||
f.render_widget(Clear, area); //this clears out the background
|
||||
f.render_widget(block, area);
|
||||
}
|
||||
}
|
||||
|
||||
/// helper function to create a centered rect using up certain percentage of the available rect `r`
|
||||
fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
|
||||
let popup_layout = Layout::vertical([
|
||||
Constraint::Percentage((100 - percent_y) / 2),
|
||||
Constraint::Percentage(percent_y),
|
||||
Constraint::Percentage((100 - percent_y) / 2),
|
||||
])
|
||||
.split(r);
|
||||
|
||||
Layout::horizontal([
|
||||
Constraint::Percentage((100 - percent_x) / 2),
|
||||
Constraint::Percentage(percent_x),
|
||||
Constraint::Percentage((100 - percent_x) / 2),
|
||||
])
|
||||
.split(popup_layout[1])[1]
|
||||
}
|
||||
@@ -1,85 +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-org/ratatui
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
use std::{
|
||||
io::{self, stdout},
|
||||
thread::sleep,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use indoc::indoc;
|
||||
use itertools::izip;
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
crossterm::terminal::{disable_raw_mode, enable_raw_mode},
|
||||
widgets::Paragraph,
|
||||
Terminal, 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 = init()?;
|
||||
terminal.draw(|frame| frame.render_widget(Paragraph::new(logo()), frame.area()))?;
|
||||
sleep(Duration::from_secs(5));
|
||||
restore()?;
|
||||
println!();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn init() -> io::Result<Terminal<impl Backend>> {
|
||||
enable_raw_mode()?;
|
||||
let options = TerminalOptions {
|
||||
viewport: Viewport::Inline(3),
|
||||
};
|
||||
Terminal::with_options(CrosstermBackend::new(stdout()), options)
|
||||
}
|
||||
|
||||
fn restore() -> io::Result<()> {
|
||||
disable_raw_mode()?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,238 +0,0 @@
|
||||
//! # [Ratatui] Scrollbar 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-org/ratatui
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
#![warn(clippy::pedantic)]
|
||||
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
},
|
||||
layout::{Alignment, Constraint, Layout, Margin},
|
||||
style::{Color, Style, Stylize},
|
||||
symbols::scrollbar,
|
||||
text::{Line, Masked, Span},
|
||||
widgets::{Block, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
struct App {
|
||||
pub vertical_scroll_state: ScrollbarState,
|
||||
pub horizontal_scroll_state: ScrollbarState,
|
||||
pub vertical_scroll: usize,
|
||||
pub horizontal_scroll: usize,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// create app and run it
|
||||
let tick_rate = Duration::from_millis(250);
|
||||
let app = App::default();
|
||||
let res = run_app(&mut terminal, app, tick_rate);
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> io::Result<()> {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &mut app))?;
|
||||
|
||||
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Char('j') | KeyCode::Down => {
|
||||
app.vertical_scroll = app.vertical_scroll.saturating_add(1);
|
||||
app.vertical_scroll_state =
|
||||
app.vertical_scroll_state.position(app.vertical_scroll);
|
||||
}
|
||||
KeyCode::Char('k') | KeyCode::Up => {
|
||||
app.vertical_scroll = app.vertical_scroll.saturating_sub(1);
|
||||
app.vertical_scroll_state =
|
||||
app.vertical_scroll_state.position(app.vertical_scroll);
|
||||
}
|
||||
KeyCode::Char('h') | KeyCode::Left => {
|
||||
app.horizontal_scroll = app.horizontal_scroll.saturating_sub(1);
|
||||
app.horizontal_scroll_state =
|
||||
app.horizontal_scroll_state.position(app.horizontal_scroll);
|
||||
}
|
||||
KeyCode::Char('l') | KeyCode::Right => {
|
||||
app.horizontal_scroll = app.horizontal_scroll.saturating_add(1);
|
||||
app.horizontal_scroll_state =
|
||||
app.horizontal_scroll_state.position(app.horizontal_scroll);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines, clippy::cast_possible_truncation)]
|
||||
fn ui(f: &mut Frame, app: &mut App) {
|
||||
let area = f.area();
|
||||
|
||||
// Words made "loooong" to demonstrate line breaking.
|
||||
let s = "Veeeeeeeeeeeeeeeery loooooooooooooooooong striiiiiiiiiiiiiiiiiiiiiiiiiing. ";
|
||||
let mut long_line = s.repeat(usize::from(area.width) / s.len() + 4);
|
||||
long_line.push('\n');
|
||||
|
||||
let chunks = Layout::vertical([
|
||||
Constraint::Min(1),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
])
|
||||
.split(area);
|
||||
|
||||
let text = vec![
|
||||
Line::from("This is a line "),
|
||||
Line::from("This is a line ".red()),
|
||||
Line::from("This is a line".on_dark_gray()),
|
||||
Line::from("This is a longer line".crossed_out()),
|
||||
Line::from(long_line.clone()),
|
||||
Line::from("This is a line".reset()),
|
||||
Line::from(vec![
|
||||
Span::raw("Masked text: "),
|
||||
Span::styled(Masked::new("password", '*'), Style::new().fg(Color::Red)),
|
||||
]),
|
||||
Line::from("This is a line "),
|
||||
Line::from("This is a line ".red()),
|
||||
Line::from("This is a line".on_dark_gray()),
|
||||
Line::from("This is a longer line".crossed_out()),
|
||||
Line::from(long_line.clone()),
|
||||
Line::from("This is a line".reset()),
|
||||
Line::from(vec![
|
||||
Span::raw("Masked text: "),
|
||||
Span::styled(Masked::new("password", '*'), Style::new().fg(Color::Red)),
|
||||
]),
|
||||
];
|
||||
app.vertical_scroll_state = app.vertical_scroll_state.content_length(text.len());
|
||||
app.horizontal_scroll_state = app.horizontal_scroll_state.content_length(long_line.len());
|
||||
|
||||
let create_block = |title: &'static str| Block::bordered().gray().title(title.bold());
|
||||
|
||||
let title = Block::new()
|
||||
.title_alignment(Alignment::Center)
|
||||
.title("Use h j k l or ◄ ▲ ▼ ► to scroll ".bold());
|
||||
f.render_widget(title, chunks[0]);
|
||||
|
||||
let paragraph = Paragraph::new(text.clone())
|
||||
.gray()
|
||||
.block(create_block("Vertical scrollbar with arrows"))
|
||||
.scroll((app.vertical_scroll as u16, 0));
|
||||
f.render_widget(paragraph, chunks[1]);
|
||||
f.render_stateful_widget(
|
||||
Scrollbar::new(ScrollbarOrientation::VerticalRight)
|
||||
.begin_symbol(Some("↑"))
|
||||
.end_symbol(Some("↓")),
|
||||
chunks[1],
|
||||
&mut app.vertical_scroll_state,
|
||||
);
|
||||
|
||||
let paragraph = Paragraph::new(text.clone())
|
||||
.gray()
|
||||
.block(create_block(
|
||||
"Vertical scrollbar without arrows, without track symbol and mirrored",
|
||||
))
|
||||
.scroll((app.vertical_scroll as u16, 0));
|
||||
f.render_widget(paragraph, chunks[2]);
|
||||
f.render_stateful_widget(
|
||||
Scrollbar::new(ScrollbarOrientation::VerticalLeft)
|
||||
.symbols(scrollbar::VERTICAL)
|
||||
.begin_symbol(None)
|
||||
.track_symbol(None)
|
||||
.end_symbol(None),
|
||||
chunks[2].inner(Margin {
|
||||
vertical: 1,
|
||||
horizontal: 0,
|
||||
}),
|
||||
&mut app.vertical_scroll_state,
|
||||
);
|
||||
|
||||
let paragraph = Paragraph::new(text.clone())
|
||||
.gray()
|
||||
.block(create_block(
|
||||
"Horizontal scrollbar with only begin arrow & custom thumb symbol",
|
||||
))
|
||||
.scroll((0, app.horizontal_scroll as u16));
|
||||
f.render_widget(paragraph, chunks[3]);
|
||||
f.render_stateful_widget(
|
||||
Scrollbar::new(ScrollbarOrientation::HorizontalBottom)
|
||||
.thumb_symbol("🬋")
|
||||
.end_symbol(None),
|
||||
chunks[3].inner(Margin {
|
||||
vertical: 0,
|
||||
horizontal: 1,
|
||||
}),
|
||||
&mut app.horizontal_scroll_state,
|
||||
);
|
||||
|
||||
let paragraph = Paragraph::new(text.clone())
|
||||
.gray()
|
||||
.block(create_block(
|
||||
"Horizontal scrollbar without arrows & custom thumb and track symbol",
|
||||
))
|
||||
.scroll((0, app.horizontal_scroll as u16));
|
||||
f.render_widget(paragraph, chunks[4]);
|
||||
f.render_stateful_widget(
|
||||
Scrollbar::new(ScrollbarOrientation::HorizontalBottom)
|
||||
.thumb_symbol("░")
|
||||
.track_symbol(Some("─")),
|
||||
chunks[4].inner(Margin {
|
||||
vertical: 0,
|
||||
horizontal: 1,
|
||||
}),
|
||||
&mut app.horizontal_scroll_state,
|
||||
);
|
||||
}
|
||||
@@ -1,183 +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-org/ratatui
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use rand::{
|
||||
distributions::{Distribution, Uniform},
|
||||
rngs::ThreadRng,
|
||||
};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
},
|
||||
layout::{Constraint, Layout},
|
||||
style::{Color, Style},
|
||||
widgets::{Block, Borders, Sparkline},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
#[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))
|
||||
}
|
||||
}
|
||||
|
||||
struct App {
|
||||
signal: RandomSignal,
|
||||
data1: Vec<u64>,
|
||||
data2: Vec<u64>,
|
||||
data3: Vec<u64>,
|
||||
}
|
||||
|
||||
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 main() -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// create app and run it
|
||||
let tick_rate = Duration::from_millis(250);
|
||||
let app = App::new();
|
||||
let res = run_app(&mut terminal, app, tick_rate);
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> io::Result<()> {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &app))?;
|
||||
|
||||
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.code == KeyCode::Char('q') {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
app.on_tick();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui(f: &mut Frame, app: &App) {
|
||||
let chunks = Layout::vertical([
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Min(0),
|
||||
])
|
||||
.split(f.area());
|
||||
let sparkline = Sparkline::default()
|
||||
.block(
|
||||
Block::new()
|
||||
.borders(Borders::LEFT | Borders::RIGHT)
|
||||
.title("Data1"),
|
||||
)
|
||||
.data(&app.data1)
|
||||
.style(Style::default().fg(Color::Yellow));
|
||||
f.render_widget(sparkline, chunks[0]);
|
||||
let sparkline = Sparkline::default()
|
||||
.block(
|
||||
Block::new()
|
||||
.borders(Borders::LEFT | Borders::RIGHT)
|
||||
.title("Data2"),
|
||||
)
|
||||
.data(&app.data2)
|
||||
.style(Style::default().bg(Color::Green));
|
||||
f.render_widget(sparkline, chunks[1]);
|
||||
// Multiline
|
||||
let sparkline = Sparkline::default()
|
||||
.block(
|
||||
Block::new()
|
||||
.borders(Borders::LEFT | Borders::RIGHT)
|
||||
.title("Data3"),
|
||||
)
|
||||
.data(&app.data3)
|
||||
.style(Style::default().fg(Color::Red));
|
||||
f.render_widget(sparkline, chunks[2]);
|
||||
}
|
||||
@@ -1,274 +0,0 @@
|
||||
//! # [Ratatui] User Input 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-org/ratatui
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
// A simple example demonstrating how to handle user input. This is a bit out of the scope of
|
||||
// the library as it does not provide any input handling out of the box. However, it may helps
|
||||
// some to get started.
|
||||
//
|
||||
// This is a very simple example:
|
||||
// * An input box always focused. Every character you type is registered here.
|
||||
// * An entered character is inserted at the cursor position.
|
||||
// * Pressing Backspace erases the left character before the cursor position
|
||||
// * Pressing Enter pushes the current input in the history of previous messages. **Note: ** as
|
||||
// this is a relatively simple example unicode characters are unsupported and their use will
|
||||
// result in undefined behaviour.
|
||||
//
|
||||
// See also https://github.com/rhysd/tui-textarea and https://github.com/sayanarijit/tui-input/
|
||||
|
||||
use std::{error::Error, io};
|
||||
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
},
|
||||
layout::{Constraint, Layout, Position},
|
||||
style::{Color, Modifier, Style, Stylize},
|
||||
text::{Line, Span, Text},
|
||||
widgets::{Block, List, ListItem, Paragraph},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
enum InputMode {
|
||||
Normal,
|
||||
Editing,
|
||||
}
|
||||
|
||||
/// App holds the state of the application
|
||||
struct App {
|
||||
/// Current value of the input box
|
||||
input: String,
|
||||
/// Position of cursor in the editor area.
|
||||
character_index: usize,
|
||||
/// Current input mode
|
||||
input_mode: InputMode,
|
||||
/// History of recorded messages
|
||||
messages: Vec<String>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
const fn new() -> Self {
|
||||
Self {
|
||||
input: String::new(),
|
||||
input_mode: InputMode::Normal,
|
||||
messages: Vec::new(),
|
||||
character_index: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn move_cursor_left(&mut self) {
|
||||
let cursor_moved_left = self.character_index.saturating_sub(1);
|
||||
self.character_index = self.clamp_cursor(cursor_moved_left);
|
||||
}
|
||||
|
||||
fn move_cursor_right(&mut self) {
|
||||
let cursor_moved_right = self.character_index.saturating_add(1);
|
||||
self.character_index = self.clamp_cursor(cursor_moved_right);
|
||||
}
|
||||
|
||||
fn enter_char(&mut self, new_char: char) {
|
||||
let index = self.byte_index();
|
||||
self.input.insert(index, new_char);
|
||||
self.move_cursor_right();
|
||||
}
|
||||
|
||||
/// Returns the byte index based on the character position.
|
||||
///
|
||||
/// Since each character in a string can be contain multiple bytes, it's necessary to calculate
|
||||
/// the byte index based on the index of the character.
|
||||
fn byte_index(&self) -> usize {
|
||||
self.input
|
||||
.char_indices()
|
||||
.map(|(i, _)| i)
|
||||
.nth(self.character_index)
|
||||
.unwrap_or(self.input.len())
|
||||
}
|
||||
|
||||
fn delete_char(&mut self) {
|
||||
let is_not_cursor_leftmost = self.character_index != 0;
|
||||
if is_not_cursor_leftmost {
|
||||
// Method "remove" is not used on the saved text for deleting the selected char.
|
||||
// Reason: Using remove on String works on bytes instead of the chars.
|
||||
// Using remove would require special care because of char boundaries.
|
||||
|
||||
let current_index = self.character_index;
|
||||
let from_left_to_current_index = current_index - 1;
|
||||
|
||||
// Getting all characters before the selected character.
|
||||
let before_char_to_delete = self.input.chars().take(from_left_to_current_index);
|
||||
// Getting all characters after selected character.
|
||||
let after_char_to_delete = self.input.chars().skip(current_index);
|
||||
|
||||
// Put all characters together except the selected one.
|
||||
// By leaving the selected one out, it is forgotten and therefore deleted.
|
||||
self.input = before_char_to_delete.chain(after_char_to_delete).collect();
|
||||
self.move_cursor_left();
|
||||
}
|
||||
}
|
||||
|
||||
fn clamp_cursor(&self, new_cursor_pos: usize) -> usize {
|
||||
new_cursor_pos.clamp(0, self.input.chars().count())
|
||||
}
|
||||
|
||||
fn reset_cursor(&mut self) {
|
||||
self.character_index = 0;
|
||||
}
|
||||
|
||||
fn submit_message(&mut self) {
|
||||
self.messages.push(self.input.clone());
|
||||
self.input.clear();
|
||||
self.reset_cursor();
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// create app and run it
|
||||
let app = App::new();
|
||||
let res = run_app(&mut terminal, app);
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<()> {
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &app))?;
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match app.input_mode {
|
||||
InputMode::Normal => match key.code {
|
||||
KeyCode::Char('e') => {
|
||||
app.input_mode = InputMode::Editing;
|
||||
}
|
||||
KeyCode::Char('q') => {
|
||||
return Ok(());
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
InputMode::Editing if key.kind == KeyEventKind::Press => match key.code {
|
||||
KeyCode::Enter => app.submit_message(),
|
||||
KeyCode::Char(to_insert) => {
|
||||
app.enter_char(to_insert);
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
app.delete_char();
|
||||
}
|
||||
KeyCode::Left => {
|
||||
app.move_cursor_left();
|
||||
}
|
||||
KeyCode::Right => {
|
||||
app.move_cursor_right();
|
||||
}
|
||||
KeyCode::Esc => {
|
||||
app.input_mode = InputMode::Normal;
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
InputMode::Editing => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui(f: &mut Frame, app: &App) {
|
||||
let vertical = Layout::vertical([
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(3),
|
||||
Constraint::Min(1),
|
||||
]);
|
||||
let [help_area, input_area, messages_area] = vertical.areas(f.area());
|
||||
|
||||
let (msg, style) = match app.input_mode {
|
||||
InputMode::Normal => (
|
||||
vec![
|
||||
"Press ".into(),
|
||||
"q".bold(),
|
||||
" to exit, ".into(),
|
||||
"e".bold(),
|
||||
" to start editing.".bold(),
|
||||
],
|
||||
Style::default().add_modifier(Modifier::RAPID_BLINK),
|
||||
),
|
||||
InputMode::Editing => (
|
||||
vec![
|
||||
"Press ".into(),
|
||||
"Esc".bold(),
|
||||
" to stop editing, ".into(),
|
||||
"Enter".bold(),
|
||||
" to record the message".into(),
|
||||
],
|
||||
Style::default(),
|
||||
),
|
||||
};
|
||||
let text = Text::from(Line::from(msg)).patch_style(style);
|
||||
let help_message = Paragraph::new(text);
|
||||
f.render_widget(help_message, help_area);
|
||||
|
||||
let input = Paragraph::new(app.input.as_str())
|
||||
.style(match app.input_mode {
|
||||
InputMode::Normal => Style::default(),
|
||||
InputMode::Editing => Style::default().fg(Color::Yellow),
|
||||
})
|
||||
.block(Block::bordered().title("Input"));
|
||||
f.render_widget(input, input_area);
|
||||
match app.input_mode {
|
||||
// Hide the cursor. `Frame` does this by default, so we don't need to do anything here
|
||||
InputMode::Normal => {}
|
||||
|
||||
// Make the cursor visible and ask ratatui to put it at the specified coordinates after
|
||||
// rendering
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
InputMode::Editing => f.set_cursor_position(Position::new(
|
||||
// Draw the cursor at the current position in the input field.
|
||||
// This position is can be controlled via the left and right arrow key
|
||||
input_area.x + app.character_index as u16 + 1,
|
||||
// Move one line down, from the border to the input line
|
||||
input_area.y + 1,
|
||||
)),
|
||||
}
|
||||
|
||||
let messages: Vec<ListItem> = app
|
||||
.messages
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, m)| {
|
||||
let content = Line::from(Span::raw(format!("{i}: {m}")));
|
||||
ListItem::new(content)
|
||||
})
|
||||
.collect();
|
||||
let messages = List::new(messages).block(Block::bordered().title("Messages"));
|
||||
f.render_widget(messages, messages_area);
|
||||
}
|
||||
70
ratatui-core/Cargo.toml
Normal file
70
ratatui-core/Cargo.toml
Normal file
@@ -0,0 +1,70 @@
|
||||
[package]
|
||||
name = "ratatui-core"
|
||||
description = """
|
||||
Core types and traits for the Ratatui Terminal UI library.
|
||||
Widget libraries should use this crate. Applications should use the main Ratatui crate.
|
||||
"""
|
||||
version = "0.1.0-alpha.0"
|
||||
readme = "README.md"
|
||||
authors.workspace = true
|
||||
documentation.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
keywords.workspace = true
|
||||
categories.workspace = true
|
||||
license.workspace = true
|
||||
exclude.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
## enables conversions from colors in the [`palette`] crate to [`Color`](crate::style::Color).
|
||||
palette = ["dep:palette"]
|
||||
|
||||
## enables the backend code that sets the underline color. Underline color is only supported by
|
||||
## the Crossterm backend, and is not supported on Windows 7.
|
||||
underline-color = []
|
||||
|
||||
## Use terminal scrolling regions to make some operations less prone to
|
||||
## flickering. (i.e. Terminal::insert_before).
|
||||
scrolling-regions = []
|
||||
|
||||
## enables serialization and deserialization of style and color types using the [`serde`] crate.
|
||||
## This is useful if you want to save themes to a file.
|
||||
serde = ["dep:serde", "bitflags/serde", "compact_str/serde"]
|
||||
|
||||
[dependencies]
|
||||
bitflags = "2.3"
|
||||
cassowary = "0.3"
|
||||
compact_str = "0.8.0"
|
||||
document-features = { workspace = true, optional = true }
|
||||
indoc.workspace = true
|
||||
itertools.workspace = true
|
||||
lru = "0.12.0"
|
||||
palette = { version = "0.7.6", optional = true }
|
||||
paste = "1.0.2"
|
||||
serde = { workspace = true, optional = true }
|
||||
strum.workspace = true
|
||||
unicode-segmentation.workspace = true
|
||||
unicode-truncate = "2"
|
||||
unicode-width.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions.workspace = true
|
||||
ratatui = { workspace = true, features = ["crossterm", "termwiz"] }
|
||||
rstest.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
[target.'cfg(not(windows))'.dev-dependencies]
|
||||
ratatui = { workspace = true, features = ["termion"] }
|
||||
|
||||
[lints.clippy]
|
||||
# we often split up a module into multiple files with the main type in a file named after the
|
||||
# module, so we want to allow this pattern
|
||||
module_inception = "allow"
|
||||
41
ratatui-core/README.md
Normal file
41
ratatui-core/README.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Ratatui Core
|
||||
|
||||
[](https://crates.io/crates/ratatui-core)
|
||||
[](https://docs.rs/ratatui-core)
|
||||
[](../LICENSE)
|
||||
|
||||
<!-- ⚠️ DO NOT EDIT THIS FILE DIRECTLY, EDIT lib.rs AND THEN RUN `cargo rdme` to update this file. -->
|
||||
<!-- cargo-rdme start -->
|
||||
|
||||
**ratatui-core** is the core library of the [ratatui] project,
|
||||
providing the essential building blocks for creating rich terminal user interfaces in Rust.
|
||||
|
||||
[ratatui]: https://github.com/ratatui/ratatui
|
||||
|
||||
### Why `ratatui-core`?
|
||||
|
||||
The `ratatui-core` crate is split from the main [`ratatui`](https://crates.io/crates/ratatui) crate
|
||||
to offer better stability for widget library authors. Widget libraries should generally depend
|
||||
on `ratatui-core`, benefiting from a stable API and reducing the need for frequent updates.
|
||||
|
||||
Applications, on the other hand, should depend on the main `ratatui` crate, which includes
|
||||
built-in widgets and additional features.
|
||||
|
||||
## Installation
|
||||
|
||||
Add `ratatui-core` to your `Cargo.toml`:
|
||||
|
||||
```shell
|
||||
cargo add ratatui-core
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome contributions from the community! Please see our [CONTRIBUTING](../CONTRIBUTING.md)
|
||||
guide for more details on how to get involved.
|
||||
|
||||
### License
|
||||
|
||||
This project is licensed under the MIT License. See the [LICENSE](../LICENSE) file for details.
|
||||
|
||||
<!-- cargo-rdme end -->
|
||||
@@ -27,7 +27,7 @@
|
||||
//! ```rust,no_run
|
||||
//! use std::io::stdout;
|
||||
//!
|
||||
//! use ratatui::prelude::*;
|
||||
//! use ratatui::{backend::CrosstermBackend, Terminal};
|
||||
//!
|
||||
//! let backend = CrosstermBackend::new(stdout());
|
||||
//! let mut terminal = Terminal::new(backend)?;
|
||||
@@ -56,7 +56,7 @@
|
||||
//! Each backend handles raw mode differently, so the behavior may vary depending on the backend
|
||||
//! being used. Be sure to consult the backend's specific documentation for exact details on how it
|
||||
//! implements raw mode.
|
||||
|
||||
//!
|
||||
//! # Alternate Screen
|
||||
//!
|
||||
//! The alternate screen is a separate buffer that some terminals provide, distinct from the main
|
||||
@@ -90,16 +90,16 @@
|
||||
//! 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-org/ratatui/tree/main/examples/README.md
|
||||
//! [Backend Comparison]:
|
||||
//! https://ratatui.rs/concepts/backends/comparison/
|
||||
//! [Ratatui Website]: https://ratatui-org.github.io/ratatui-book
|
||||
//! [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 strum::{Display, EnumString};
|
||||
@@ -109,21 +109,6 @@ use crate::{
|
||||
layout::{Position, Size},
|
||||
};
|
||||
|
||||
#[cfg(feature = "termion")]
|
||||
mod termion;
|
||||
#[cfg(feature = "termion")]
|
||||
pub use self::termion::TermionBackend;
|
||||
|
||||
#[cfg(feature = "crossterm")]
|
||||
mod crossterm;
|
||||
#[cfg(feature = "crossterm")]
|
||||
pub use self::crossterm::CrosstermBackend;
|
||||
|
||||
#[cfg(feature = "termwiz")]
|
||||
mod termwiz;
|
||||
#[cfg(feature = "termwiz")]
|
||||
pub use self::termwiz::TermwizBackend;
|
||||
|
||||
mod test;
|
||||
pub use self::test::TestBackend;
|
||||
|
||||
@@ -162,7 +147,7 @@ pub struct WindowSize {
|
||||
/// Most applications should not need to interact with the `Backend` trait directly as the
|
||||
/// [`Terminal`] struct provides a higher level interface for interacting with the terminal.
|
||||
///
|
||||
/// [`Terminal`]: crate::terminal::Terminal
|
||||
/// [`Terminal`]: https://docs.rs/ratatui/latest/ratatui/struct.Terminal.html
|
||||
pub trait Backend {
|
||||
/// Draw the given content to the terminal screen.
|
||||
///
|
||||
@@ -187,8 +172,10 @@ pub trait Backend {
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::backend::{Backend, TestBackend};
|
||||
/// # use ratatui::backend::{TestBackend};
|
||||
/// # let mut backend = TestBackend::new(80, 25);
|
||||
/// use ratatui::backend::Backend;
|
||||
///
|
||||
/// backend.hide_cursor()?;
|
||||
/// // do something with hidden cursor
|
||||
/// backend.show_cursor()?;
|
||||
@@ -222,9 +209,10 @@ pub trait Backend {
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::backend::{Backend, TestBackend};
|
||||
/// # use ratatui::layout::Position;
|
||||
/// # use ratatui::backend::{TestBackend};
|
||||
/// # let mut backend = TestBackend::new(80, 25);
|
||||
/// use ratatui::{backend::Backend, layout::Position};
|
||||
///
|
||||
/// backend.set_cursor_position(Position { x: 10, y: 20 })?;
|
||||
/// assert_eq!(backend.get_cursor_position()?, Position { x: 10, y: 20 });
|
||||
/// # std::io::Result::Ok(())
|
||||
@@ -254,8 +242,10 @@ pub trait Backend {
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use ratatui::backend::{Backend, TestBackend};
|
||||
/// # use ratatui::backend::{TestBackend};
|
||||
/// # let mut backend = TestBackend::new(80, 25);
|
||||
/// use ratatui::backend::Backend;
|
||||
///
|
||||
/// backend.clear()?;
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
@@ -270,8 +260,10 @@ pub trait Backend {
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use ratatui::{prelude::*, backend::{TestBackend, ClearType}};
|
||||
/// # use ratatui::{backend::{TestBackend}};
|
||||
/// # let mut backend = TestBackend::new(80, 25);
|
||||
/// use ratatui::backend::{Backend, ClearType};
|
||||
///
|
||||
/// backend.clear_region(ClearType::All)?;
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
@@ -302,8 +294,10 @@ pub trait Backend {
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, backend::TestBackend};
|
||||
/// let backend = TestBackend::new(80, 25);
|
||||
/// # use ratatui::{backend::{TestBackend}};
|
||||
/// # let backend = TestBackend::new(80, 25);
|
||||
/// use ratatui::{backend::Backend, layout::Size};
|
||||
///
|
||||
/// assert_eq!(backend.size()?, Size::new(80, 25));
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
@@ -318,6 +312,64 @@ pub trait Backend {
|
||||
|
||||
/// Flush any buffered content to the terminal screen.
|
||||
fn flush(&mut self) -> io::Result<()>;
|
||||
|
||||
/// Scroll a region of the screen upwards, where a region is specified by a (half-open) range
|
||||
/// of rows.
|
||||
///
|
||||
/// Each row in the region is replaced by the row `line_count` rows below it, except the bottom
|
||||
/// `line_count` rows, which are replaced by empty rows. If `line_count` is equal to or larger
|
||||
/// than the number of rows in the region, then all rows are replaced with empty rows.
|
||||
///
|
||||
/// If the region includes row 0, then `line_count` rows are copied into the bottom of the
|
||||
/// scrollback buffer. These rows are first taken from the old contents of the region, starting
|
||||
/// from the top. If there aren't sufficient rows in the region, then the remainder are empty
|
||||
/// rows.
|
||||
///
|
||||
/// The position of the cursor afterwards is undefined.
|
||||
///
|
||||
/// The behavior is designed to match what ANSI terminals do when scrolling regions are
|
||||
/// established. With ANSI terminals, a scrolling region can be established with the "^[[X;Yr"
|
||||
/// sequence, where X and Y define the lines of the region. The scrolling region can be reset
|
||||
/// to be the whole screen with the "^[[r" sequence.
|
||||
///
|
||||
/// When a scrolling region is established in an ANSI terminal, various operations' behaviors
|
||||
/// are changed in such a way that the scrolling region acts like a "virtual screen". In
|
||||
/// particular, the scrolling sequence "^[[NS", which scrolls lines up by a count of N.
|
||||
///
|
||||
/// On an ANSI terminal, this method will probably translate to something like:
|
||||
/// "^[[X;Yr^[[NS^[[r". That is, set the scrolling region, scroll up, then reset the scrolling
|
||||
/// region.
|
||||
///
|
||||
/// For examples of how this function is expected to work, refer to the tests for
|
||||
/// [`TestBackend::scroll_region_up`].
|
||||
#[cfg(feature = "scrolling-regions")]
|
||||
fn scroll_region_up(&mut self, region: std::ops::Range<u16>, line_count: u16)
|
||||
-> io::Result<()>;
|
||||
|
||||
/// Scroll a region of the screen downwards, where a region is specified by a (half-open) range
|
||||
/// of rows.
|
||||
///
|
||||
/// Each row in the region is replaced by the row `line_count` rows above it, except the top
|
||||
/// `line_count` rows, which are replaced by empty rows. If `line_count` is equal to or larger
|
||||
/// than the number of rows in the region, then all rows are replaced with empty rows.
|
||||
///
|
||||
/// The position of the cursor afterwards is undefined.
|
||||
///
|
||||
/// See the documentation for [`Self::scroll_region_down`] for more information about how this
|
||||
/// is expected to be implemented for ANSI terminals. All of that applies, except the ANSI
|
||||
/// sequence to scroll down is "^[[NT".
|
||||
///
|
||||
/// This function is asymmetrical with regards to the scrollback buffer. The reason is that
|
||||
/// this how terminals seem to implement things.
|
||||
///
|
||||
/// For examples of how this function is expected to work, refer to the tests for
|
||||
/// [`TestBackend::scroll_region_down`].
|
||||
#[cfg(feature = "scrolling-regions")]
|
||||
fn scroll_region_down(
|
||||
&mut self,
|
||||
region: std::ops::Range<u16>,
|
||||
line_count: u16,
|
||||
) -> io::Result<()>;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
use std::{
|
||||
fmt::{self, Write},
|
||||
io,
|
||||
io, iter,
|
||||
};
|
||||
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
@@ -24,7 +24,7 @@ use crate::{
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::{backend::TestBackend, prelude::*};
|
||||
/// use ratatui::backend::{Backend, TestBackend};
|
||||
///
|
||||
/// let mut backend = TestBackend::new(10, 2);
|
||||
/// backend.clear()?;
|
||||
@@ -35,6 +35,7 @@ use crate::{
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct TestBackend {
|
||||
buffer: Buffer,
|
||||
scrollback: Buffer,
|
||||
cursor: bool,
|
||||
pos: (u16, u16),
|
||||
}
|
||||
@@ -73,6 +74,29 @@ impl TestBackend {
|
||||
pub fn new(width: u16, height: u16) -> Self {
|
||||
Self {
|
||||
buffer: Buffer::empty(Rect::new(0, 0, width, height)),
|
||||
scrollback: Buffer::empty(Rect::new(0, 0, width, 0)),
|
||||
cursor: false,
|
||||
pos: (0, 0),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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),
|
||||
}
|
||||
@@ -83,9 +107,29 @@ impl TestBackend {
|
||||
&self.buffer
|
||||
}
|
||||
|
||||
/// Returns a reference to the internal scrollback buffer of the `TestBackend`.
|
||||
///
|
||||
/// The scrollback buffer represents the part of the screen that is currently hidden from view,
|
||||
/// but that could be accessed by scrolling back in the terminal's history. This would normally
|
||||
/// be done using the terminal's scrollbar or an equivalent keyboard shortcut.
|
||||
///
|
||||
/// The scrollback buffer starts out empty. Lines are appended when they scroll off the top of
|
||||
/// the main buffer. This happens when lines are appended to the bottom of the main buffer
|
||||
/// using [`Backend::append_lines`].
|
||||
///
|
||||
/// The scrollback buffer has a maximum height of [`u16::MAX`]. If lines are appended to the
|
||||
/// bottom of the scrollback buffer when it is at its maximum height, a corresponding number of
|
||||
/// lines will be removed from the top.
|
||||
pub const fn scrollback(&self) -> &Buffer {
|
||||
&self.scrollback
|
||||
}
|
||||
|
||||
/// Resizes the `TestBackend` to the specified width and height.
|
||||
pub fn resize(&mut self, width: u16, height: u16) {
|
||||
self.buffer.resize(Rect::new(0, 0, width, height));
|
||||
let scrollback_height = self.scrollback.area.height;
|
||||
self.scrollback
|
||||
.resize(Rect::new(0, 0, width, scrollback_height));
|
||||
}
|
||||
|
||||
/// Asserts that the `TestBackend`'s buffer is equal to the expected buffer.
|
||||
@@ -93,6 +137,7 @@ impl TestBackend {
|
||||
/// This is a shortcut for `assert_eq!(self.buffer(), &expected)`.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// When they are not equal, a panic occurs with a detailed error message showing the
|
||||
/// differences between the expected and actual buffers.
|
||||
#[allow(deprecated)]
|
||||
@@ -102,11 +147,42 @@ impl TestBackend {
|
||||
crate::assert_buffer_eq!(&self.buffer, expected);
|
||||
}
|
||||
|
||||
/// Asserts that the `TestBackend`'s scrollback buffer is equal to the expected buffer.
|
||||
///
|
||||
/// This is a shortcut for `assert_eq!(self.scrollback(), &expected)`.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// When they are not equal, a panic occurs with a detailed error message showing the
|
||||
/// differences between the expected and actual buffers.
|
||||
#[track_caller]
|
||||
pub fn assert_scrollback(&self, expected: &Buffer) {
|
||||
assert_eq!(&self.scrollback, expected);
|
||||
}
|
||||
|
||||
/// Asserts that the `TestBackend`'s scrollback buffer is empty.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// When the scrollback buffer is not equal, a panic occurs with a detailed error message
|
||||
/// showing the differences between the expected and actual buffers.
|
||||
pub fn assert_scrollback_empty(&self) {
|
||||
let expected = Buffer {
|
||||
area: Rect {
|
||||
width: self.scrollback.area.width,
|
||||
..Rect::ZERO
|
||||
},
|
||||
content: vec![],
|
||||
};
|
||||
self.assert_scrollback(&expected);
|
||||
}
|
||||
|
||||
/// Asserts that the `TestBackend`'s buffer is equal to the expected lines.
|
||||
///
|
||||
/// This is a shortcut for `assert_eq!(self.buffer(), &Buffer::with_lines(expected))`.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// When they are not equal, a panic occurs with a detailed error message showing the
|
||||
/// differences between the expected and actual buffers.
|
||||
#[track_caller]
|
||||
@@ -118,11 +194,29 @@ impl TestBackend {
|
||||
self.assert_buffer(&Buffer::with_lines(expected));
|
||||
}
|
||||
|
||||
/// Asserts that the `TestBackend`'s scrollback buffer is equal to the expected lines.
|
||||
///
|
||||
/// This is a shortcut for `assert_eq!(self.scrollback(), &Buffer::with_lines(expected))`.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// When they are not equal, a panic occurs with a detailed error message showing the
|
||||
/// differences between the expected and actual buffers.
|
||||
#[track_caller]
|
||||
pub fn assert_scrollback_lines<'line, Lines>(&self, expected: Lines)
|
||||
where
|
||||
Lines: IntoIterator,
|
||||
Lines::Item: Into<crate::text::Line<'line>>,
|
||||
{
|
||||
self.assert_scrollback(&Buffer::with_lines(expected));
|
||||
}
|
||||
|
||||
/// Asserts that the `TestBackend`'s cursor position is equal to the expected one.
|
||||
///
|
||||
/// This is a shortcut for `assert_eq!(self.get_cursor_position().unwrap(), expected)`.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// When they are not equal, a panic occurs with a detailed error message showing the
|
||||
/// differences between the expected and actual position.
|
||||
#[track_caller]
|
||||
@@ -175,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 => {
|
||||
@@ -215,7 +309,7 @@ impl Backend for TestBackend {
|
||||
/// the cursor y position then that number of empty lines (at most the buffer's height in this
|
||||
/// case but this limit is instead replaced with scrolling in most backend implementations) will
|
||||
/// be added after the current position and the cursor will be moved to the last row.
|
||||
fn append_lines(&mut self, n: u16) -> io::Result<()> {
|
||||
fn append_lines(&mut self, line_count: u16) -> io::Result<()> {
|
||||
let Position { x: cur_x, y: cur_y } = self.get_cursor_position()?;
|
||||
let Rect { width, height, .. } = self.buffer.area;
|
||||
|
||||
@@ -224,19 +318,29 @@ impl Backend for TestBackend {
|
||||
|
||||
let max_y = height.saturating_sub(1);
|
||||
let lines_after_cursor = max_y.saturating_sub(cur_y);
|
||||
if n > lines_after_cursor {
|
||||
let rotate_by = n.saturating_sub(lines_after_cursor).min(max_y);
|
||||
|
||||
if rotate_by == height - 1 {
|
||||
self.clear()?;
|
||||
}
|
||||
if line_count > lines_after_cursor {
|
||||
// We need to insert blank lines at the bottom and scroll the lines from the top into
|
||||
// scrollback.
|
||||
let scroll_by: usize = (line_count - lines_after_cursor).into();
|
||||
let width: usize = self.buffer.area.width.into();
|
||||
let cells_to_scrollback = self.buffer.content.len().min(width * scroll_by);
|
||||
|
||||
self.set_cursor_position(Position { x: 0, y: rotate_by })?;
|
||||
self.clear_region(ClearType::BeforeCursor)?;
|
||||
self.buffer.content.rotate_left((width * rotate_by).into());
|
||||
append_to_scrollback(
|
||||
&mut self.scrollback,
|
||||
self.buffer.content.splice(
|
||||
0..cells_to_scrollback,
|
||||
iter::repeat_with(Default::default).take(cells_to_scrollback),
|
||||
),
|
||||
);
|
||||
self.buffer.content.rotate_left(cells_to_scrollback);
|
||||
append_to_scrollback(
|
||||
&mut self.scrollback,
|
||||
iter::repeat_with(Default::default).take(width * scroll_by - cells_to_scrollback),
|
||||
);
|
||||
}
|
||||
|
||||
let new_cursor_y = cur_y.saturating_add(n).min(max_y);
|
||||
let new_cursor_y = cur_y.saturating_add(line_count).min(max_y);
|
||||
self.set_cursor_position(Position::new(new_cursor_x, new_cursor_y))?;
|
||||
|
||||
Ok(())
|
||||
@@ -261,10 +365,98 @@ 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
|
||||
/// multiple of the buffer's width. If the scrollback buffer ends up larger than 65535 lines tall,
|
||||
/// then lines will be removed from the top to get it down to size.
|
||||
fn append_to_scrollback(scrollback: &mut Buffer, cells: impl IntoIterator<Item = Cell>) {
|
||||
scrollback.content.extend(cells);
|
||||
let width = scrollback.area.width as usize;
|
||||
let new_height = (scrollback.content.len() / width).min(u16::MAX as usize);
|
||||
let keep_from = scrollback
|
||||
.content
|
||||
.len()
|
||||
.saturating_sub(width * u16::MAX as usize);
|
||||
scrollback.content.drain(0..keep_from);
|
||||
scrollback.area.height = new_height as u16;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use itertools::Itertools as _;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
@@ -273,6 +465,7 @@ mod tests {
|
||||
TestBackend::new(10, 2),
|
||||
TestBackend {
|
||||
buffer: Buffer::with_lines([" "; 2]),
|
||||
scrollback: Buffer::empty(Rect::new(0, 0, 10, 0)),
|
||||
cursor: false,
|
||||
pos: (0, 0),
|
||||
}
|
||||
@@ -286,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, " ")]
|
||||
"#,
|
||||
)
|
||||
);
|
||||
@@ -323,6 +516,13 @@ mod tests {
|
||||
backend.assert_buffer_lines(["aaaaaaaaaa"; 2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic = "assertion `left == right` failed"]
|
||||
fn assert_scrollback_panics() {
|
||||
let backend = TestBackend::new(10, 2);
|
||||
backend.assert_scrollback_lines(["aaaaaaaaaa"; 2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display() {
|
||||
let backend = TestBackend::new(10, 2);
|
||||
@@ -385,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",
|
||||
@@ -406,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",
|
||||
@@ -430,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",
|
||||
@@ -454,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",
|
||||
@@ -478,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",
|
||||
@@ -502,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",
|
||||
@@ -536,12 +730,12 @@ mod tests {
|
||||
"dddddddddd",
|
||||
"eeeeeeeeee",
|
||||
]);
|
||||
backend.assert_scrollback_empty();
|
||||
}
|
||||
|
||||
#[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",
|
||||
@@ -557,13 +751,14 @@ mod tests {
|
||||
|
||||
backend.append_lines(1).unwrap();
|
||||
|
||||
backend.buffer = Buffer::with_lines([
|
||||
backend.assert_buffer_lines([
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
"dddddddddd",
|
||||
"eeeeeeeeee",
|
||||
" ",
|
||||
]);
|
||||
backend.assert_scrollback_lines(["aaaaaaaaaa"]);
|
||||
|
||||
// It also moves the cursor to the right, as is common of the behaviour of
|
||||
// terminals in raw-mode
|
||||
@@ -572,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",
|
||||
@@ -597,12 +791,12 @@ mod tests {
|
||||
"dddddddddd",
|
||||
"eeeeeeeeee",
|
||||
]);
|
||||
backend.assert_scrollback_empty();
|
||||
}
|
||||
|
||||
#[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",
|
||||
@@ -624,12 +818,12 @@ mod tests {
|
||||
" ",
|
||||
" ",
|
||||
]);
|
||||
backend.assert_scrollback_lines(["aaaaaaaaaa", "bbbbbbbbbb"]);
|
||||
}
|
||||
|
||||
#[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",
|
||||
@@ -651,12 +845,18 @@ mod tests {
|
||||
" ",
|
||||
" ",
|
||||
]);
|
||||
backend.assert_scrollback_lines([
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
"dddddddddd",
|
||||
"eeeeeeeeee",
|
||||
]);
|
||||
}
|
||||
|
||||
#[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",
|
||||
@@ -676,6 +876,114 @@ mod tests {
|
||||
"eeeeeeeeee",
|
||||
" ",
|
||||
]);
|
||||
backend.assert_scrollback_lines(["aaaaaaaaaa"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_multiple_lines_where_cursor_at_end_appends_more_than_height_lines() {
|
||||
let mut backend = TestBackend::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
"dddddddddd",
|
||||
"eeeeeeeeee",
|
||||
]);
|
||||
|
||||
backend
|
||||
.set_cursor_position(Position { x: 0, y: 4 })
|
||||
.unwrap();
|
||||
|
||||
backend.append_lines(8).unwrap();
|
||||
backend.assert_cursor_position(Position { x: 1, y: 4 });
|
||||
|
||||
backend.assert_buffer_lines([
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
]);
|
||||
backend.assert_scrollback_lines([
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
"dddddddddd",
|
||||
"eeeeeeeeee",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_lines_truncates_beyond_u16_max() -> io::Result<()> {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
|
||||
// Fill the scrollback with 65535 + 10 lines.
|
||||
let row_count = u16::MAX as usize + 10;
|
||||
for row in 0..=row_count {
|
||||
if row > 4 {
|
||||
backend.set_cursor_position(Position { x: 0, y: 4 })?;
|
||||
backend.append_lines(1)?;
|
||||
}
|
||||
let cells = format!("{row:>10}").chars().map(Cell::from).collect_vec();
|
||||
let content = cells
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(column, cell)| (column as u16, 4.min(row) as u16, cell));
|
||||
backend.draw(content)?;
|
||||
}
|
||||
|
||||
// check that the buffer contains the last 5 lines appended
|
||||
backend.assert_buffer_lines([
|
||||
" 65541",
|
||||
" 65542",
|
||||
" 65543",
|
||||
" 65544",
|
||||
" 65545",
|
||||
]);
|
||||
|
||||
// TODO: ideally this should be something like:
|
||||
// let lines = (6..=65545).map(|row| format!("{row:>10}"));
|
||||
// backend.assert_scrollback_lines(lines);
|
||||
// but there's some truncation happening in Buffer::with_lines that needs to be fixed
|
||||
assert_eq!(
|
||||
Buffer {
|
||||
area: Rect::new(0, 0, 10, 5),
|
||||
content: backend.scrollback.content[0..10 * 5].to_vec(),
|
||||
},
|
||||
Buffer::with_lines([
|
||||
" 6",
|
||||
" 7",
|
||||
" 8",
|
||||
" 9",
|
||||
" 10",
|
||||
]),
|
||||
"first 5 lines of scrollback should have been truncated"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
Buffer {
|
||||
area: Rect::new(0, 0, 10, 5),
|
||||
content: backend.scrollback.content[10 * 65530..10 * 65535].to_vec(),
|
||||
},
|
||||
Buffer::with_lines([
|
||||
" 65536",
|
||||
" 65537",
|
||||
" 65538",
|
||||
" 65539",
|
||||
" 65540",
|
||||
]),
|
||||
"last 5 lines of scrollback should have been appended"
|
||||
);
|
||||
|
||||
// These checks come after the content checks as otherwise we won't see the failing content
|
||||
// when these checks fail.
|
||||
// Make sure the scrollback is the right size.
|
||||
assert_eq!(backend.scrollback.area.width, 10);
|
||||
assert_eq!(backend.scrollback.area.height, 65535);
|
||||
assert_eq!(backend.scrollback.content.len(), 10 * 65535);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -689,4 +997,81 @@ mod tests {
|
||||
let mut backend = TestBackend::new(10, 2);
|
||||
backend.flush().unwrap();
|
||||
}
|
||||
|
||||
#[cfg(feature = "scrolling-regions")]
|
||||
mod scrolling_regions {
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
|
||||
const A: &str = "aaaa";
|
||||
const B: &str = "bbbb";
|
||||
const C: &str = "cccc";
|
||||
const D: &str = "dddd";
|
||||
const E: &str = "eeee";
|
||||
const S: &str = " ";
|
||||
|
||||
#[rstest]
|
||||
#[case([A, B, C, D, E], 0..5, 0, [], [A, B, C, D, E])]
|
||||
#[case([A, B, C, D, E], 0..5, 2, [A, B], [C, D, E, S, S])]
|
||||
#[case([A, B, C, D, E], 0..5, 5, [A, B, C, D, E], [S, S, S, S, S])]
|
||||
#[case([A, B, C, D, E], 0..5, 7, [A, B, C, D, E, S, S], [S, S, S, S, S])]
|
||||
#[case([A, B, C, D, E], 0..3, 0, [], [A, B, C, D, E])]
|
||||
#[case([A, B, C, D, E], 0..3, 2, [A, B], [C, S, S, D, E])]
|
||||
#[case([A, B, C, D, E], 0..3, 3, [A, B, C], [S, S, S, D, E])]
|
||||
#[case([A, B, C, D, E], 0..3, 4, [A, B, C, S], [S, S, S, D, E])]
|
||||
#[case([A, B, C, D, E], 1..4, 0, [], [A, B, C, D, E])]
|
||||
#[case([A, B, C, D, E], 1..4, 2, [], [A, D, S, S, E])]
|
||||
#[case([A, B, C, D, E], 1..4, 3, [], [A, S, S, S, E])]
|
||||
#[case([A, B, C, D, E], 1..4, 4, [], [A, S, S, S, E])]
|
||||
#[case([A, B, C, D, E], 0..0, 0, [], [A, B, C, D, E])]
|
||||
#[case([A, B, C, D, E], 0..0, 2, [S, S], [A, B, C, D, E])]
|
||||
#[case([A, B, C, D, E], 2..2, 0, [], [A, B, C, D, E])]
|
||||
#[case([A, B, C, D, E], 2..2, 2, [], [A, B, C, D, E])]
|
||||
fn scroll_region_up<const L: usize, const M: usize, const N: usize>(
|
||||
#[case] initial_screen: [&'static str; L],
|
||||
#[case] range: std::ops::Range<u16>,
|
||||
#[case] scroll_by: u16,
|
||||
#[case] expected_scrollback: [&'static str; M],
|
||||
#[case] expected_buffer: [&'static str; N],
|
||||
) {
|
||||
let mut backend = TestBackend::with_lines(initial_screen);
|
||||
backend.scroll_region_up(range, scroll_by).unwrap();
|
||||
if expected_scrollback.is_empty() {
|
||||
backend.assert_scrollback_empty();
|
||||
} else {
|
||||
backend.assert_scrollback_lines(expected_scrollback);
|
||||
}
|
||||
backend.assert_buffer_lines(expected_buffer);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case([A, B, C, D, E], 0..5, 0, [A, B, C, D, E])]
|
||||
#[case([A, B, C, D, E], 0..5, 2, [S, S, A, B, C])]
|
||||
#[case([A, B, C, D, E], 0..5, 5, [S, S, S, S, S])]
|
||||
#[case([A, B, C, D, E], 0..5, 7, [S, S, S, S, S])]
|
||||
#[case([A, B, C, D, E], 0..3, 0, [A, B, C, D, E])]
|
||||
#[case([A, B, C, D, E], 0..3, 2, [S, S, A, D, E])]
|
||||
#[case([A, B, C, D, E], 0..3, 3, [S, S, S, D, E])]
|
||||
#[case([A, B, C, D, E], 0..3, 4, [S, S, S, D, E])]
|
||||
#[case([A, B, C, D, E], 1..4, 0, [A, B, C, D, E])]
|
||||
#[case([A, B, C, D, E], 1..4, 2, [A, S, S, B, E])]
|
||||
#[case([A, B, C, D, E], 1..4, 3, [A, S, S, S, E])]
|
||||
#[case([A, B, C, D, E], 1..4, 4, [A, S, S, S, E])]
|
||||
#[case([A, B, C, D, E], 0..0, 0, [A, B, C, D, E])]
|
||||
#[case([A, B, C, D, E], 0..0, 2, [A, B, C, D, E])]
|
||||
#[case([A, B, C, D, E], 2..2, 0, [A, B, C, D, E])]
|
||||
#[case([A, B, C, D, E], 2..2, 2, [A, B, C, D, E])]
|
||||
fn scroll_region_down<const M: usize, const N: usize>(
|
||||
#[case] initial_screen: [&'static str; M],
|
||||
#[case] range: std::ops::Range<u16>,
|
||||
#[case] scroll_by: u16,
|
||||
#[case] expected_buffer: [&'static str; N],
|
||||
) {
|
||||
let mut backend = TestBackend::with_lines(initial_screen);
|
||||
backend.scroll_region_down(range, scroll_by).unwrap();
|
||||
backend.assert_scrollback_empty();
|
||||
backend.assert_buffer_lines(expected_buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,11 @@ macro_rules! assert_buffer_eq {
|
||||
#[allow(deprecated)]
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::prelude::*;
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::{Color, Style},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn assert_buffer_eq_does_not_panic_on_equal_buffers() {
|
||||
@@ -6,7 +6,12 @@ use std::{
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{buffer::Cell, layout::Position, prelude::*};
|
||||
use crate::{
|
||||
buffer::Cell,
|
||||
layout::{Position, Rect},
|
||||
style::Style,
|
||||
text::{Line, Span},
|
||||
};
|
||||
|
||||
/// A buffer that maps to the desired content of the terminal after the draw call
|
||||
///
|
||||
@@ -18,7 +23,7 @@ use crate::{buffer::Cell, layout::Position, prelude::*};
|
||||
/// # Examples:
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::{
|
||||
/// use ratatui_core::{
|
||||
/// buffer::{Buffer, Cell},
|
||||
/// layout::{Position, Rect},
|
||||
/// style::{Color, Style},
|
||||
@@ -163,7 +168,11 @@ impl Buffer {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, buffer::Cell, layout::Position};
|
||||
/// use ratatui_core::{
|
||||
/// buffer::{Buffer, Cell},
|
||||
/// layout::{Position, Rect},
|
||||
/// };
|
||||
///
|
||||
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 10));
|
||||
///
|
||||
/// assert_eq!(buffer.cell(Position::new(0, 0)), Some(&Cell::default()));
|
||||
@@ -190,7 +199,11 @@ impl Buffer {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, buffer::Cell, layout::Position};
|
||||
/// use ratatui_core::{
|
||||
/// buffer::{Buffer, Cell},
|
||||
/// layout::{Position, Rect},
|
||||
/// style::{Color, Style},
|
||||
/// };
|
||||
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 10));
|
||||
///
|
||||
/// if let Some(cell) = buffer.cell_mut(Position::new(0, 0)) {
|
||||
@@ -214,7 +227,8 @@ impl Buffer {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::{buffer::Buffer, layout::Rect};
|
||||
///
|
||||
/// let buffer = Buffer::empty(Rect::new(200, 100, 10, 10));
|
||||
/// // Global coordinates to the top corner of this buffer's area
|
||||
/// assert_eq!(buffer.index_of(200, 100), 0);
|
||||
@@ -225,7 +239,8 @@ impl Buffer {
|
||||
/// Panics when given an coordinate that is outside of this Buffer's area.
|
||||
///
|
||||
/// ```should_panic
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::{buffer::Buffer, layout::Rect};
|
||||
///
|
||||
/// let buffer = Buffer::empty(Rect::new(200, 100, 10, 10));
|
||||
/// // Top coordinate is outside of the buffer in global coordinate space, as the Buffer's area
|
||||
/// // starts at (200, 100).
|
||||
@@ -246,7 +261,7 @@ impl Buffer {
|
||||
///
|
||||
/// Returns `None` if the given coordinates are outside of the Buffer's area.
|
||||
///
|
||||
/// Note that this is private because of <https://github.com/ratatui-org/ratatui/issues/1122>
|
||||
/// Note that this is private because of <https://github.com/ratatui/ratatui/issues/1122>
|
||||
#[must_use]
|
||||
const fn index_of_opt(&self, position: Position) -> Option<usize> {
|
||||
let area = self.area;
|
||||
@@ -254,9 +269,10 @@ impl Buffer {
|
||||
return None;
|
||||
}
|
||||
// remove offset
|
||||
let y = position.y - self.area.y;
|
||||
let x = position.x - self.area.x;
|
||||
Some((y * self.area.width + x) as usize)
|
||||
let y = (position.y - self.area.y) as usize;
|
||||
let x = (position.x - self.area.x) as usize;
|
||||
let width = self.area.width as usize;
|
||||
Some(y * width + x)
|
||||
}
|
||||
|
||||
/// Returns the (global) coordinates of a cell given its index
|
||||
@@ -266,7 +282,8 @@ impl Buffer {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::{buffer::Buffer, layout::Rect};
|
||||
///
|
||||
/// let rect = Rect::new(200, 100, 10, 10);
|
||||
/// let buffer = Buffer::empty(rect);
|
||||
/// assert_eq!(buffer.pos_of(0), (200, 100));
|
||||
@@ -278,22 +295,25 @@ impl Buffer {
|
||||
/// Panics when given an index that is outside the Buffer's content.
|
||||
///
|
||||
/// ```should_panic
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::{buffer::Buffer, layout::Rect};
|
||||
///
|
||||
/// let rect = Rect::new(0, 0, 10, 10); // 100 cells in total
|
||||
/// let buffer = Buffer::empty(rect);
|
||||
/// // Index 100 is the 101th cell, which lies outside of the area of this Buffer.
|
||||
/// buffer.pos_of(100); // Panics
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn pos_of(&self, i: usize) -> (u16, u16) {
|
||||
pub fn pos_of(&self, index: usize) -> (u16, u16) {
|
||||
debug_assert!(
|
||||
i < self.content.len(),
|
||||
"Trying to get the coords of a cell outside the buffer: i={i} len={}",
|
||||
index < self.content.len(),
|
||||
"Trying to get the coords of a cell outside the buffer: i={index} len={}",
|
||||
self.content.len()
|
||||
);
|
||||
let x = index % self.area.width as usize + self.area.x as usize;
|
||||
let y = index / self.area.width as usize + self.area.y as usize;
|
||||
(
|
||||
self.area.x + (i as u16) % self.area.width,
|
||||
self.area.y + (i as u16) / self.area.width,
|
||||
u16::try_from(x).expect("x overflow. This should never happen as area.width is u16"),
|
||||
u16::try_from(y).expect("y overflow. This should never happen as area.height is u16"),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -377,6 +397,8 @@ impl Buffer {
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// [`Color`]: crate::style::Color
|
||||
pub fn set_style<S: Into<Style>>(&mut self, area: Rect, style: S) {
|
||||
let style = style.into();
|
||||
let area = self.area.intersection(area);
|
||||
@@ -504,7 +526,11 @@ impl<P: Into<Position>> Index<P> for Buffer {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::{prelude::*, buffer::Cell, layout::Position};
|
||||
/// use ratatui_core::{
|
||||
/// buffer::{Buffer, Cell},
|
||||
/// layout::{Position, Rect},
|
||||
/// };
|
||||
///
|
||||
/// let buf = Buffer::empty(Rect::new(0, 0, 10, 10));
|
||||
/// let cell = &buf[(0, 0)];
|
||||
/// let cell = &buf[Position::new(0, 0)];
|
||||
@@ -530,7 +556,11 @@ impl<P: Into<Position>> IndexMut<P> for Buffer {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::{prelude::*, buffer::Cell, layout::Position};
|
||||
/// use ratatui_core::{
|
||||
/// buffer::{Buffer, Cell},
|
||||
/// layout::{Position, Rect},
|
||||
/// };
|
||||
///
|
||||
/// let mut buf = Buffer::empty(Rect::new(0, 0, 10, 10));
|
||||
/// buf[(0, 0)].set_symbol("A");
|
||||
/// buf[Position::new(0, 0)].set_symbol("B");
|
||||
@@ -622,6 +652,7 @@ mod tests {
|
||||
use rstest::{fixture, rstest};
|
||||
|
||||
use super::*;
|
||||
use crate::style::{Color, Modifier, Stylize};
|
||||
|
||||
#[test]
|
||||
fn debug_empty_buffer() {
|
||||
@@ -1214,11 +1245,12 @@ mod tests {
|
||||
#[case::shrug("🤷", "🤷xxxxx")]
|
||||
// Technically this is a (brown) bear, a zero-width joiner and a snowflake
|
||||
// As it is joined its a single emoji and should therefore have a width of 2.
|
||||
// It's correctly detected as a single grapheme but it's width is 4 for some reason
|
||||
#[case::polarbear("🐻❄️", "🐻❄️xxx")]
|
||||
// Prior to unicode-width 0.2, this was incorrectly detected as width 4 for some reason
|
||||
#[case::polarbear("🐻❄️", "🐻❄️xxxxx")]
|
||||
// Technically this is an eye, a zero-width joiner and a speech bubble
|
||||
// Both eye and speech bubble include a 'display as emoji' variation selector
|
||||
#[case::eye_speechbubble("👁️🗨️", "👁️🗨️xxx")]
|
||||
// Prior to unicode-width 0.2, this was incorrectly detected as width 4 for some reason
|
||||
#[case::eye_speechbubble("👁️🗨️", "👁️🗨️xxxxx")]
|
||||
fn renders_emoji(#[case] input: &str, #[case] expected: &str) {
|
||||
use unicode_width::UnicodeWidthChar;
|
||||
|
||||
@@ -1244,4 +1276,24 @@ mod tests {
|
||||
let expected = Buffer::with_lines([expected]);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
/// Regression test for <https://github.com/ratatui/ratatui/issues/1441>
|
||||
///
|
||||
/// Previously the `pos_of` function would incorrectly cast the index to a u16 value instead of
|
||||
/// using the index as is. This caused incorrect rendering of any buffer with an length > 65535.
|
||||
#[test]
|
||||
fn index_pos_of_u16_max() {
|
||||
let buffer = Buffer::empty(Rect::new(0, 0, 256, 256 + 1));
|
||||
assert_eq!(buffer.index_of(255, 255), 65535);
|
||||
assert_eq!(buffer.pos_of(65535), (255, 255));
|
||||
|
||||
assert_eq!(buffer.index_of(0, 256), 65536);
|
||||
assert_eq!(buffer.pos_of(65536), (0, 256)); // previously (0, 0)
|
||||
|
||||
assert_eq!(buffer.index_of(1, 256), 65537);
|
||||
assert_eq!(buffer.pos_of(65537), (1, 256)); // previously (1, 0)
|
||||
|
||||
assert_eq!(buffer.index_of(255, 256), 65791);
|
||||
assert_eq!(buffer.pos_of(65791), (255, 256)); // previously (255, 0)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
use compact_str::CompactString;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::style::{Color, Modifier, Style};
|
||||
|
||||
/// A buffer cell
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
@@ -13,7 +13,7 @@ pub struct Cell {
|
||||
/// This is a [`CompactString`] which is a wrapper around [`String`] that uses a small inline
|
||||
/// buffer for short strings.
|
||||
///
|
||||
/// See <https://github.com/ratatui-org/ratatui/pull/601> for more information.
|
||||
/// See <https://github.com/ratatui/ratatui/pull/601> for more information.
|
||||
symbol: CompactString,
|
||||
|
||||
/// The foreground color of the cell.
|
||||
@@ -157,6 +157,14 @@ impl Default for Cell {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<char> for Cell {
|
||||
fn from(ch: char) -> Self {
|
||||
let mut cell = Self::EMPTY;
|
||||
cell.set_char(ch);
|
||||
cell
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -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;
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::fmt;
|
||||
|
||||
use itertools::Itertools;
|
||||
use strum::EnumIs;
|
||||
|
||||
/// A constraint that defines the size of a layout element.
|
||||
@@ -27,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]);
|
||||
///
|
||||
@@ -117,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
|
||||
///
|
||||
@@ -220,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);
|
||||
@@ -229,7 +234,7 @@ impl Constraint {
|
||||
where
|
||||
T: IntoIterator<Item = u16>,
|
||||
{
|
||||
lengths.into_iter().map(Self::Length).collect_vec()
|
||||
lengths.into_iter().map(Self::Length).collect()
|
||||
}
|
||||
|
||||
/// Convert an iterator of ratios into a vector of constraints
|
||||
@@ -237,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);
|
||||
@@ -246,10 +252,7 @@ impl Constraint {
|
||||
where
|
||||
T: IntoIterator<Item = (u32, u32)>,
|
||||
{
|
||||
ratios
|
||||
.into_iter()
|
||||
.map(|(n, d)| Self::Ratio(n, d))
|
||||
.collect_vec()
|
||||
ratios.into_iter().map(|(n, d)| Self::Ratio(n, d)).collect()
|
||||
}
|
||||
|
||||
/// Convert an iterator of percentages into a vector of constraints
|
||||
@@ -257,7 +260,8 @@ impl Constraint {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::layout::{Constraint, Layout, Rect};
|
||||
///
|
||||
/// # let area = Rect::default();
|
||||
/// let constraints = Constraint::from_percentages([25, 50, 25]);
|
||||
/// let layout = Layout::default().constraints(constraints).split(area);
|
||||
@@ -266,7 +270,7 @@ impl Constraint {
|
||||
where
|
||||
T: IntoIterator<Item = u16>,
|
||||
{
|
||||
percentages.into_iter().map(Self::Percentage).collect_vec()
|
||||
percentages.into_iter().map(Self::Percentage).collect()
|
||||
}
|
||||
|
||||
/// Convert an iterator of maxes into a vector of constraints
|
||||
@@ -274,7 +278,8 @@ impl Constraint {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::layout::{Constraint, Layout, Rect};
|
||||
///
|
||||
/// # let area = Rect::default();
|
||||
/// let constraints = Constraint::from_maxes([1, 2, 3]);
|
||||
/// let layout = Layout::default().constraints(constraints).split(area);
|
||||
@@ -283,7 +288,7 @@ impl Constraint {
|
||||
where
|
||||
T: IntoIterator<Item = u16>,
|
||||
{
|
||||
maxes.into_iter().map(Self::Max).collect_vec()
|
||||
maxes.into_iter().map(Self::Max).collect()
|
||||
}
|
||||
|
||||
/// Convert an iterator of mins into a vector of constraints
|
||||
@@ -291,7 +296,8 @@ impl Constraint {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::layout::{Constraint, Layout, Rect};
|
||||
///
|
||||
/// # let area = Rect::default();
|
||||
/// let constraints = Constraint::from_mins([1, 2, 3]);
|
||||
/// let layout = Layout::default().constraints(constraints).split(area);
|
||||
@@ -300,7 +306,7 @@ impl Constraint {
|
||||
where
|
||||
T: IntoIterator<Item = u16>,
|
||||
{
|
||||
mins.into_iter().map(Self::Min).collect_vec()
|
||||
mins.into_iter().map(Self::Min).collect()
|
||||
}
|
||||
|
||||
/// Convert an iterator of proportional factors into a vector of constraints
|
||||
@@ -308,7 +314,8 @@ impl Constraint {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::layout::{Constraint, Layout, Rect};
|
||||
///
|
||||
/// # let area = Rect::default();
|
||||
/// let constraints = Constraint::from_fills([1, 2, 3]);
|
||||
/// let layout = Layout::default().constraints(constraints).split(area);
|
||||
@@ -317,10 +324,7 @@ impl Constraint {
|
||||
where
|
||||
T: IntoIterator<Item = u16>,
|
||||
{
|
||||
proportional_factors
|
||||
.into_iter()
|
||||
.map(Self::Fill)
|
||||
.collect_vec()
|
||||
proportional_factors.into_iter().map(Self::Fill).collect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,7 +337,8 @@ impl From<u16> for Constraint {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::layout::{Constraint, Direction, Layout, Rect};
|
||||
///
|
||||
/// # let area = Rect::default();
|
||||
/// let layout = Layout::new(Direction::Vertical, [1, 2, 3]).split(area);
|
||||
/// let layout = Layout::horizontal([1, 2, 3]).split(area);
|
||||
@@ -1,7 +1,7 @@
|
||||
use strum::{Display, EnumIs, EnumString};
|
||||
|
||||
#[allow(unused_imports)]
|
||||
use super::constraint::Constraint;
|
||||
use crate::layout::Constraint;
|
||||
|
||||
/// Defines the options for layout flex justify content in a container.
|
||||
///
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,7 +12,7 @@ use crate::layout::Rect;
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::layout::{Position, Rect};
|
||||
/// use ratatui_core::layout::{Position, Rect};
|
||||
///
|
||||
/// // the following are all equivalent
|
||||
/// let position = Position { x: 1, y: 2 };
|
||||
@@ -4,8 +4,7 @@ use std::{
|
||||
fmt,
|
||||
};
|
||||
|
||||
use super::{Position, Size};
|
||||
use crate::prelude::*;
|
||||
use crate::layout::{Margin, Position, Size};
|
||||
|
||||
mod iter;
|
||||
pub use iter::*;
|
||||
@@ -27,7 +26,7 @@ pub struct Rect {
|
||||
pub height: u16,
|
||||
}
|
||||
|
||||
/// Amounts by which to move a [`Rect`](super::Rect).
|
||||
/// Amounts by which to move a [`Rect`](crate::layout::Rect).
|
||||
///
|
||||
/// Positive numbers move to the right/bottom and negative to the left/top.
|
||||
///
|
||||
@@ -56,32 +55,41 @@ impl Rect {
|
||||
height: 0,
|
||||
};
|
||||
|
||||
/// Creates a new `Rect`, with width and height limited to keep the area under max `u16`. If
|
||||
/// clipped, aspect ratio will be preserved.
|
||||
pub fn new(x: u16, y: u16, width: u16, height: u16) -> Self {
|
||||
let max_area = u16::MAX;
|
||||
let (clipped_width, clipped_height) =
|
||||
if u32::from(width) * u32::from(height) > u32::from(max_area) {
|
||||
let aspect_ratio = f64::from(width) / f64::from(height);
|
||||
let max_area_f = f64::from(max_area);
|
||||
let height_f = (max_area_f / aspect_ratio).sqrt();
|
||||
let width_f = height_f * aspect_ratio;
|
||||
(width_f as u16, height_f as u16)
|
||||
} else {
|
||||
(width, height)
|
||||
};
|
||||
/// Creates a new `Rect`, with width and height limited to keep both bounds within `u16`.
|
||||
///
|
||||
/// If the width or height would cause the right or bottom coordinate to be larger than the
|
||||
/// maximum value of `u16`, the width or height will be clamped to keep the right or bottom
|
||||
/// coordinate within `u16`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui_core::layout::Rect;
|
||||
///
|
||||
/// let rect = Rect::new(1, 2, 3, 4);
|
||||
/// ```
|
||||
pub const fn new(x: u16, y: u16, width: u16, height: u16) -> Self {
|
||||
// these calculations avoid using min so that this function can be const
|
||||
let max_width = u16::MAX - x;
|
||||
let max_height = u16::MAX - y;
|
||||
let width = if width > max_width { max_width } else { width };
|
||||
let height = if height > max_height {
|
||||
max_height
|
||||
} else {
|
||||
height
|
||||
};
|
||||
Self {
|
||||
x,
|
||||
y,
|
||||
width: clipped_width,
|
||||
height: clipped_height,
|
||||
width,
|
||||
height,
|
||||
}
|
||||
}
|
||||
|
||||
/// The area of the `Rect`. If the area is larger than the maximum value of `u16`, it will be
|
||||
/// clamped to `u16::MAX`.
|
||||
pub const fn area(self) -> u16 {
|
||||
self.width.saturating_mul(self.height)
|
||||
pub const fn area(self) -> u32 {
|
||||
(self.width as u32) * (self.height as u32)
|
||||
}
|
||||
|
||||
/// Returns true if the `Rect` has no area.
|
||||
@@ -205,7 +213,8 @@ impl Rect {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, layout::Position};
|
||||
/// use ratatui_core::layout::{Position, Rect};
|
||||
///
|
||||
/// let rect = Rect::new(1, 2, 3, 4);
|
||||
/// assert!(rect.contains(Position { x: 1, y: 2 }));
|
||||
/// ````
|
||||
@@ -234,11 +243,11 @@ impl Rect {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// # fn render(frame: &mut Frame) {
|
||||
/// let area = frame.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 +263,8 @@ impl Rect {
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::{buffer::Buffer, layout::Rect, text::Line, widgets::Widget};
|
||||
///
|
||||
/// fn render(area: Rect, buf: &mut Buffer) {
|
||||
/// for row in area.rows() {
|
||||
/// Line::raw("Hello, world!").render(row, buf);
|
||||
@@ -270,10 +280,11 @@ impl Rect {
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui_core::{buffer::Buffer, layout::Rect, text::Text, widgets::Widget};
|
||||
///
|
||||
/// fn render(area: Rect, buf: &mut Buffer) {
|
||||
/// if let Some(left) = area.columns().next() {
|
||||
/// Block::new().borders(Borders::LEFT).render(left, buf);
|
||||
/// for (i, column) in area.columns().enumerate() {
|
||||
/// Text::from(format!("{}", i)).render(column, buf);
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
@@ -288,7 +299,8 @@ impl Rect {
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::{buffer::Buffer, layout::Rect};
|
||||
///
|
||||
/// fn render(area: Rect, buf: &mut Buffer) {
|
||||
/// for position in area.positions() {
|
||||
/// buf[(position.x, position.y)].set_symbol("x");
|
||||
@@ -304,7 +316,8 @@ impl Rect {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::layout::Rect;
|
||||
///
|
||||
/// let rect = Rect::new(1, 2, 3, 4);
|
||||
/// let position = rect.as_position();
|
||||
/// ````
|
||||
@@ -352,6 +365,7 @@ mod tests {
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
use crate::layout::{Constraint, Layout};
|
||||
|
||||
#[test]
|
||||
fn to_string() {
|
||||
@@ -496,46 +510,28 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn size_truncation() {
|
||||
for width in 256u16..300u16 {
|
||||
for height in 256u16..300u16 {
|
||||
let rect = Rect::new(0, 0, width, height);
|
||||
rect.area(); // Should not panic.
|
||||
assert!(rect.width < width || rect.height < height);
|
||||
// The target dimensions are rounded down so the math will not be too precise
|
||||
// but let's make sure the ratios don't diverge crazily.
|
||||
assert!(
|
||||
(f64::from(rect.width) / f64::from(rect.height)
|
||||
- f64::from(width) / f64::from(height))
|
||||
.abs()
|
||||
< 1.0
|
||||
);
|
||||
assert_eq!(
|
||||
Rect::new(u16::MAX - 100, u16::MAX - 1000, 200, 2000),
|
||||
Rect {
|
||||
x: u16::MAX - 100,
|
||||
y: u16::MAX - 1000,
|
||||
width: 100,
|
||||
height: 1000
|
||||
}
|
||||
}
|
||||
|
||||
// One dimension below 255, one above. Area above max u16.
|
||||
let width = 900;
|
||||
let height = 100;
|
||||
let rect = Rect::new(0, 0, width, height);
|
||||
assert_ne!(rect.width, 900);
|
||||
assert_ne!(rect.height, 100);
|
||||
assert!(rect.width < width || rect.height < height);
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn size_preservation() {
|
||||
for width in 0..256u16 {
|
||||
for height in 0..256u16 {
|
||||
let rect = Rect::new(0, 0, width, height);
|
||||
rect.area(); // Should not panic.
|
||||
assert_eq!(rect.width, width);
|
||||
assert_eq!(rect.height, height);
|
||||
assert_eq!(
|
||||
Rect::new(u16::MAX - 100, u16::MAX - 1000, 100, 1000),
|
||||
Rect {
|
||||
x: u16::MAX - 100,
|
||||
y: u16::MAX - 1000,
|
||||
width: 100,
|
||||
height: 1000
|
||||
}
|
||||
}
|
||||
|
||||
// One dimension below 255, one above. Area below max u16.
|
||||
let rect = Rect::new(0, 0, 300, 100);
|
||||
assert_eq!(rect.width, 300);
|
||||
assert_eq!(rect.height, 100);
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -546,7 +542,7 @@ mod tests {
|
||||
width: 10,
|
||||
height: 10,
|
||||
};
|
||||
const _AREA: u16 = RECT.area();
|
||||
const _AREA: u32 = RECT.area();
|
||||
const _LEFT: u16 = RECT.left();
|
||||
const _RIGHT: u16 = RECT.right();
|
||||
const _TOP: u16 = RECT.top();
|
||||
329
ratatui-core/src/layout/rect/iter.rs
Normal file
329
ratatui-core/src/layout/rect/iter.rs
Normal file
@@ -0,0 +1,329 @@
|
||||
use crate::layout::{Position, Rect};
|
||||
|
||||
/// An iterator over rows within a `Rect`.
|
||||
pub struct Rows {
|
||||
/// The `Rect` associated with the rows.
|
||||
rect: Rect,
|
||||
/// The y coordinate of the row within the `Rect` when iterating forwards.
|
||||
current_row_fwd: u16,
|
||||
/// The y coordinate of the row within the `Rect` when iterating backwards.
|
||||
current_row_back: u16,
|
||||
}
|
||||
|
||||
impl Rows {
|
||||
/// Creates a new `Rows` iterator.
|
||||
pub const fn new(rect: Rect) -> Self {
|
||||
Self {
|
||||
rect,
|
||||
current_row_fwd: rect.y,
|
||||
current_row_back: rect.bottom(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for Rows {
|
||||
type Item = Rect;
|
||||
|
||||
/// Retrieves the next row within the `Rect`.
|
||||
///
|
||||
/// Returns `None` when there are no more rows to iterate through.
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.current_row_fwd >= self.current_row_back {
|
||||
return None;
|
||||
}
|
||||
let row = Rect::new(self.rect.x, self.current_row_fwd, self.rect.width, 1);
|
||||
self.current_row_fwd += 1;
|
||||
Some(row)
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
let start_count = self.current_row_fwd.saturating_sub(self.rect.top());
|
||||
let end_count = self.rect.bottom().saturating_sub(self.current_row_back);
|
||||
let count = self
|
||||
.rect
|
||||
.height
|
||||
.saturating_sub(start_count)
|
||||
.saturating_sub(end_count) as usize;
|
||||
(count, Some(count))
|
||||
}
|
||||
}
|
||||
|
||||
impl DoubleEndedIterator for Rows {
|
||||
/// Retrieves the previous row within the `Rect`.
|
||||
///
|
||||
/// Returns `None` when there are no more rows to iterate through.
|
||||
fn next_back(&mut self) -> Option<Self::Item> {
|
||||
if self.current_row_back <= self.current_row_fwd {
|
||||
return None;
|
||||
}
|
||||
self.current_row_back -= 1;
|
||||
let row = Rect::new(self.rect.x, self.current_row_back, self.rect.width, 1);
|
||||
Some(row)
|
||||
}
|
||||
}
|
||||
|
||||
/// An iterator over columns within a `Rect`.
|
||||
pub struct Columns {
|
||||
/// The `Rect` associated with the columns.
|
||||
rect: Rect,
|
||||
/// The x coordinate of the column within the `Rect` when iterating forwards.
|
||||
current_column_fwd: u16,
|
||||
/// The x coordinate of the column within the `Rect` when iterating backwards.
|
||||
current_column_back: u16,
|
||||
}
|
||||
|
||||
impl Columns {
|
||||
/// Creates a new `Columns` iterator.
|
||||
pub const fn new(rect: Rect) -> Self {
|
||||
Self {
|
||||
rect,
|
||||
current_column_fwd: rect.x,
|
||||
current_column_back: rect.right(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for Columns {
|
||||
type Item = Rect;
|
||||
|
||||
/// Retrieves the next column within the `Rect`.
|
||||
///
|
||||
/// Returns `None` when there are no more columns to iterate through.
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.current_column_fwd >= self.current_column_back {
|
||||
return None;
|
||||
}
|
||||
let column = Rect::new(self.current_column_fwd, self.rect.y, 1, self.rect.height);
|
||||
self.current_column_fwd += 1;
|
||||
Some(column)
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
let start_count = self.current_column_fwd.saturating_sub(self.rect.left());
|
||||
let end_count = self.rect.right().saturating_sub(self.current_column_back);
|
||||
let count = self
|
||||
.rect
|
||||
.width
|
||||
.saturating_sub(start_count)
|
||||
.saturating_sub(end_count) as usize;
|
||||
(count, Some(count))
|
||||
}
|
||||
}
|
||||
|
||||
impl DoubleEndedIterator for Columns {
|
||||
/// Retrieves the previous column within the `Rect`.
|
||||
///
|
||||
/// Returns `None` when there are no more columns to iterate through.
|
||||
fn next_back(&mut self) -> Option<Self::Item> {
|
||||
if self.current_column_back <= self.current_column_fwd {
|
||||
return None;
|
||||
}
|
||||
self.current_column_back -= 1;
|
||||
let column = Rect::new(self.current_column_back, self.rect.y, 1, self.rect.height);
|
||||
Some(column)
|
||||
}
|
||||
}
|
||||
|
||||
/// An iterator over positions within a `Rect`.
|
||||
///
|
||||
/// The iterator will yield all positions within the `Rect` in a row-major order.
|
||||
pub struct Positions {
|
||||
/// The `Rect` associated with the positions.
|
||||
rect: Rect,
|
||||
/// The current position within the `Rect`.
|
||||
current_position: Position,
|
||||
}
|
||||
|
||||
impl Positions {
|
||||
/// Creates a new `Positions` iterator.
|
||||
pub const fn new(rect: Rect) -> Self {
|
||||
Self {
|
||||
rect,
|
||||
current_position: Position::new(rect.x, rect.y),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for Positions {
|
||||
type Item = Position;
|
||||
|
||||
/// Retrieves the next position within the `Rect`.
|
||||
///
|
||||
/// Returns `None` when there are no more positions to iterate through.
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.current_position.y >= self.rect.bottom() {
|
||||
return None;
|
||||
}
|
||||
let position = self.current_position;
|
||||
self.current_position.x += 1;
|
||||
if self.current_position.x >= self.rect.right() {
|
||||
self.current_position.x = self.rect.x;
|
||||
self.current_position.y += 1;
|
||||
}
|
||||
Some(position)
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
let row_count = self.rect.bottom().saturating_sub(self.current_position.y);
|
||||
if row_count == 0 {
|
||||
return (0, Some(0));
|
||||
}
|
||||
let column_count = self.rect.right().saturating_sub(self.current_position.x);
|
||||
// subtract 1 from the row count to account for the current row
|
||||
let count = (row_count - 1)
|
||||
.saturating_mul(self.rect.width)
|
||||
.saturating_add(column_count) as usize;
|
||||
(count, Some(count))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn rows() {
|
||||
let rect = Rect::new(0, 0, 2, 3);
|
||||
let mut rows = Rows::new(rect);
|
||||
assert_eq!(rows.size_hint(), (3, Some(3)));
|
||||
assert_eq!(rows.next(), Some(Rect::new(0, 0, 2, 1)));
|
||||
assert_eq!(rows.size_hint(), (2, Some(2)));
|
||||
assert_eq!(rows.next(), Some(Rect::new(0, 1, 2, 1)));
|
||||
assert_eq!(rows.size_hint(), (1, Some(1)));
|
||||
assert_eq!(rows.next(), Some(Rect::new(0, 2, 2, 1)));
|
||||
assert_eq!(rows.size_hint(), (0, Some(0)));
|
||||
assert_eq!(rows.next(), None);
|
||||
assert_eq!(rows.size_hint(), (0, Some(0)));
|
||||
assert_eq!(rows.next_back(), None);
|
||||
assert_eq!(rows.size_hint(), (0, Some(0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rows_back() {
|
||||
let rect = Rect::new(0, 0, 2, 3);
|
||||
let mut rows = Rows::new(rect);
|
||||
assert_eq!(rows.size_hint(), (3, Some(3)));
|
||||
assert_eq!(rows.next_back(), Some(Rect::new(0, 2, 2, 1)));
|
||||
assert_eq!(rows.size_hint(), (2, Some(2)));
|
||||
assert_eq!(rows.next_back(), Some(Rect::new(0, 1, 2, 1)));
|
||||
assert_eq!(rows.size_hint(), (1, Some(1)));
|
||||
assert_eq!(rows.next_back(), Some(Rect::new(0, 0, 2, 1)));
|
||||
assert_eq!(rows.size_hint(), (0, Some(0)));
|
||||
assert_eq!(rows.next_back(), None);
|
||||
assert_eq!(rows.size_hint(), (0, Some(0)));
|
||||
assert_eq!(rows.next(), None);
|
||||
assert_eq!(rows.size_hint(), (0, Some(0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rows_meet_in_the_middle() {
|
||||
let rect = Rect::new(0, 0, 2, 4);
|
||||
let mut rows = Rows::new(rect);
|
||||
assert_eq!(rows.size_hint(), (4, Some(4)));
|
||||
assert_eq!(rows.next(), Some(Rect::new(0, 0, 2, 1)));
|
||||
assert_eq!(rows.size_hint(), (3, Some(3)));
|
||||
assert_eq!(rows.next_back(), Some(Rect::new(0, 3, 2, 1)));
|
||||
assert_eq!(rows.size_hint(), (2, Some(2)));
|
||||
assert_eq!(rows.next(), Some(Rect::new(0, 1, 2, 1)));
|
||||
assert_eq!(rows.size_hint(), (1, Some(1)));
|
||||
assert_eq!(rows.next_back(), Some(Rect::new(0, 2, 2, 1)));
|
||||
assert_eq!(rows.size_hint(), (0, Some(0)));
|
||||
assert_eq!(rows.next(), None);
|
||||
assert_eq!(rows.size_hint(), (0, Some(0)));
|
||||
assert_eq!(rows.next_back(), None);
|
||||
assert_eq!(rows.size_hint(), (0, Some(0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn columns() {
|
||||
let rect = Rect::new(0, 0, 3, 2);
|
||||
let mut columns = Columns::new(rect);
|
||||
assert_eq!(columns.size_hint(), (3, Some(3)));
|
||||
assert_eq!(columns.next(), Some(Rect::new(0, 0, 1, 2)));
|
||||
assert_eq!(columns.size_hint(), (2, Some(2)));
|
||||
assert_eq!(columns.next(), Some(Rect::new(1, 0, 1, 2)));
|
||||
assert_eq!(columns.size_hint(), (1, Some(1)));
|
||||
assert_eq!(columns.next(), Some(Rect::new(2, 0, 1, 2)));
|
||||
assert_eq!(columns.size_hint(), (0, Some(0)));
|
||||
assert_eq!(columns.next(), None);
|
||||
assert_eq!(columns.size_hint(), (0, Some(0)));
|
||||
assert_eq!(columns.next_back(), None);
|
||||
assert_eq!(columns.size_hint(), (0, Some(0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn columns_back() {
|
||||
let rect = Rect::new(0, 0, 3, 2);
|
||||
let mut columns = Columns::new(rect);
|
||||
assert_eq!(columns.size_hint(), (3, Some(3)));
|
||||
assert_eq!(columns.next_back(), Some(Rect::new(2, 0, 1, 2)));
|
||||
assert_eq!(columns.size_hint(), (2, Some(2)));
|
||||
assert_eq!(columns.next_back(), Some(Rect::new(1, 0, 1, 2)));
|
||||
assert_eq!(columns.size_hint(), (1, Some(1)));
|
||||
assert_eq!(columns.next_back(), Some(Rect::new(0, 0, 1, 2)));
|
||||
assert_eq!(columns.size_hint(), (0, Some(0)));
|
||||
assert_eq!(columns.next_back(), None);
|
||||
assert_eq!(columns.size_hint(), (0, Some(0)));
|
||||
assert_eq!(columns.next(), None);
|
||||
assert_eq!(columns.size_hint(), (0, Some(0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn columns_meet_in_the_middle() {
|
||||
let rect = Rect::new(0, 0, 4, 2);
|
||||
let mut columns = Columns::new(rect);
|
||||
assert_eq!(columns.size_hint(), (4, Some(4)));
|
||||
assert_eq!(columns.next(), Some(Rect::new(0, 0, 1, 2)));
|
||||
assert_eq!(columns.size_hint(), (3, Some(3)));
|
||||
assert_eq!(columns.next_back(), Some(Rect::new(3, 0, 1, 2)));
|
||||
assert_eq!(columns.size_hint(), (2, Some(2)));
|
||||
assert_eq!(columns.next(), Some(Rect::new(1, 0, 1, 2)));
|
||||
assert_eq!(columns.size_hint(), (1, Some(1)));
|
||||
assert_eq!(columns.next_back(), Some(Rect::new(2, 0, 1, 2)));
|
||||
assert_eq!(columns.size_hint(), (0, Some(0)));
|
||||
assert_eq!(columns.next(), None);
|
||||
assert_eq!(columns.size_hint(), (0, Some(0)));
|
||||
assert_eq!(columns.next_back(), None);
|
||||
assert_eq!(columns.size_hint(), (0, Some(0)));
|
||||
}
|
||||
|
||||
/// We allow a total of `65536` columns in the range `(0..=65535)`. In this test we iterate
|
||||
/// forward and skip the first `65534` columns, and expect the next column to be `65535` and
|
||||
/// the subsequent columns to be `None`.
|
||||
#[test]
|
||||
fn columns_max() {
|
||||
let rect = Rect::new(0, 0, u16::MAX, 1);
|
||||
let mut columns = Columns::new(rect).skip(usize::from(u16::MAX - 1));
|
||||
assert_eq!(columns.next(), Some(Rect::new(u16::MAX - 1, 0, 1, 1)));
|
||||
assert_eq!(columns.next(), None);
|
||||
}
|
||||
|
||||
/// We allow a total of `65536` columns in the range `(0..=65535)`. In this test we iterate
|
||||
/// backward and skip the last `65534` columns, and expect the next column to be `0` and the
|
||||
/// subsequent columns to be `None`.
|
||||
#[test]
|
||||
fn columns_min() {
|
||||
let rect = Rect::new(0, 0, u16::MAX, 1);
|
||||
let mut columns = Columns::new(rect).rev().skip(usize::from(u16::MAX - 1));
|
||||
assert_eq!(columns.next(), Some(Rect::new(0, 0, 1, 1)));
|
||||
assert_eq!(columns.next(), None);
|
||||
assert_eq!(columns.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn positions() {
|
||||
let rect = Rect::new(0, 0, 2, 2);
|
||||
let mut positions = Positions::new(rect);
|
||||
assert_eq!(positions.size_hint(), (4, Some(4)));
|
||||
assert_eq!(positions.next(), Some(Position::new(0, 0)));
|
||||
assert_eq!(positions.size_hint(), (3, Some(3)));
|
||||
assert_eq!(positions.next(), Some(Position::new(1, 0)));
|
||||
assert_eq!(positions.size_hint(), (2, Some(2)));
|
||||
assert_eq!(positions.next(), Some(Position::new(0, 1)));
|
||||
assert_eq!(positions.size_hint(), (1, Some(1)));
|
||||
assert_eq!(positions.next(), Some(Position::new(1, 1)));
|
||||
assert_eq!(positions.size_hint(), (0, Some(0)));
|
||||
assert_eq!(positions.next(), None);
|
||||
assert_eq!(positions.size_hint(), (0, Some(0)));
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
#![warn(missing_docs)]
|
||||
use std::fmt;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::layout::Rect;
|
||||
|
||||
/// A simple size struct
|
||||
///
|
||||
47
ratatui-core/src/lib.rs
Normal file
47
ratatui-core/src/lib.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
// show the feature flags in the generated documentation
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![doc(
|
||||
html_logo_url = "https://raw.githubusercontent.com/ratatui/ratatui/main/assets/logo.png",
|
||||
html_favicon_url = "https://raw.githubusercontent.com/ratatui/ratatui/main/assets/favicon.ico"
|
||||
)]
|
||||
//! **ratatui-core** is the core library of the [ratatui] project,
|
||||
//! providing the essential building blocks for creating rich terminal user interfaces in Rust.
|
||||
//!
|
||||
//! [ratatui]: https://github.com/ratatui/ratatui
|
||||
//!
|
||||
//! ## Why `ratatui-core`?
|
||||
//!
|
||||
//! The `ratatui-core` crate is split from the main [`ratatui`](https://crates.io/crates/ratatui) crate
|
||||
//! to offer better stability for widget library authors. Widget libraries should generally depend
|
||||
//! on `ratatui-core`, benefiting from a stable API and reducing the need for frequent updates.
|
||||
//!
|
||||
//! Applications, on the other hand, should depend on the main `ratatui` crate, which includes
|
||||
//! built-in widgets and additional features.
|
||||
//!
|
||||
//! # Installation
|
||||
//!
|
||||
//! Add `ratatui-core` to your `Cargo.toml`:
|
||||
//!
|
||||
//! ```shell
|
||||
//! cargo add ratatui-core
|
||||
//! ```
|
||||
#![cfg_attr(feature = "document-features", doc = "\n## Features")]
|
||||
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
|
||||
//!
|
||||
//! # Contributing
|
||||
//!
|
||||
//! We welcome contributions from the community! Please see our [CONTRIBUTING](../CONTRIBUTING.md)
|
||||
//! guide for more details on how to get involved.
|
||||
//!
|
||||
//! ## License
|
||||
//!
|
||||
//! This project is licensed under the MIT License. See the [LICENSE](../LICENSE) file for details.
|
||||
|
||||
pub mod backend;
|
||||
pub mod buffer;
|
||||
pub mod layout;
|
||||
pub mod style;
|
||||
pub mod symbols;
|
||||
pub mod text;
|
||||
pub mod widgets;
|
||||
@@ -13,7 +13,10 @@
|
||||
//! ## Example
|
||||
//!
|
||||
//! ```
|
||||
//! use ratatui::prelude::*;
|
||||
//! use ratatui_core::{
|
||||
//! style::{Color, Modifier, Style},
|
||||
//! text::Span,
|
||||
//! };
|
||||
//!
|
||||
//! let heading_style = Style::new()
|
||||
//! .fg(Color::Black)
|
||||
@@ -35,13 +38,15 @@
|
||||
//! - [`Span`]s can be styled again, which will merge the styles.
|
||||
//! - Many widget types can be styled directly rather than calling their `style()` method.
|
||||
//!
|
||||
//! See the [`Stylize`] and [`Styled`] traits for more information. These traits are re-exported in
|
||||
//! the [`prelude`] module for convenience.
|
||||
//! See the [`Stylize`] and [`Styled`] traits for more information.
|
||||
//!
|
||||
//! ## Example
|
||||
//!
|
||||
//! ```
|
||||
//! use ratatui::{prelude::*, widgets::*};
|
||||
//! use ratatui_core::{
|
||||
//! style::{Color, Modifier, Style, Stylize},
|
||||
//! text::{Span, Text},
|
||||
//! };
|
||||
//!
|
||||
//! assert_eq!(
|
||||
//! "hello".red().on_blue().bold(),
|
||||
@@ -55,8 +60,8 @@
|
||||
//! );
|
||||
//!
|
||||
//! assert_eq!(
|
||||
//! Paragraph::new("hello").red().on_blue().bold(),
|
||||
//! Paragraph::new("hello").style(
|
||||
//! Text::from("hello").red().on_blue().bold(),
|
||||
//! Text::from("hello").style(
|
||||
//! Style::default()
|
||||
//! .fg(Color::Red)
|
||||
//! .bg(Color::Blue)
|
||||
@@ -65,13 +70,13 @@
|
||||
//! );
|
||||
//! ```
|
||||
//!
|
||||
//! [`prelude`]: crate::prelude
|
||||
//! [`Span`]: crate::text::Span
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use bitflags::bitflags;
|
||||
pub use color::{Color, ParseColorError};
|
||||
use stylize::ColorDebugKind;
|
||||
pub use stylize::{Styled, Stylize};
|
||||
|
||||
mod color;
|
||||
@@ -91,7 +96,7 @@ bitflags! {
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::{prelude::*};
|
||||
/// use ratatui_core::style::Modifier;
|
||||
///
|
||||
/// let m = Modifier::BOLD | Modifier::ITALIC;
|
||||
/// ```
|
||||
@@ -127,7 +132,7 @@ impl fmt::Debug for Modifier {
|
||||
/// Style lets you control the main characteristics of the displayed elements.
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::prelude::*;
|
||||
/// use ratatui_core::style::{Color, Modifier, Style};
|
||||
///
|
||||
/// Style::default()
|
||||
/// .fg(Color::Black)
|
||||
@@ -138,7 +143,8 @@ impl fmt::Debug for Modifier {
|
||||
/// Styles can also be created with a [shorthand notation](crate::style#using-style-shorthands).
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::style::{Style, Stylize};
|
||||
///
|
||||
/// Style::new().black().on_green().italic().bold();
|
||||
/// ```
|
||||
///
|
||||
@@ -148,7 +154,11 @@ impl fmt::Debug for Modifier {
|
||||
/// anywhere that accepts `Into<Style>`.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::{
|
||||
/// style::{Color, Modifier, Style},
|
||||
/// text::Line,
|
||||
/// };
|
||||
///
|
||||
/// Line::styled("hello", Style::new().fg(Color::Red));
|
||||
/// // simplifies to
|
||||
/// Line::styled("hello", Color::Red);
|
||||
@@ -163,7 +173,11 @@ impl fmt::Debug for Modifier {
|
||||
/// just S3.
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::prelude::*;
|
||||
/// use ratatui_core::{
|
||||
/// buffer::Buffer,
|
||||
/// layout::Rect,
|
||||
/// style::{Color, Modifier, Style},
|
||||
/// };
|
||||
///
|
||||
/// let styles = [
|
||||
/// Style::default()
|
||||
@@ -199,7 +213,11 @@ impl fmt::Debug for Modifier {
|
||||
/// reset all properties until that point use [`Style::reset`].
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::prelude::*;
|
||||
/// use ratatui_core::{
|
||||
/// buffer::Buffer,
|
||||
/// layout::Rect,
|
||||
/// style::{Color, Modifier, Style},
|
||||
/// };
|
||||
///
|
||||
/// let styles = [
|
||||
/// Style::default()
|
||||
@@ -223,17 +241,32 @@ impl fmt::Debug for Modifier {
|
||||
/// buffer[(0, 0)].style(),
|
||||
/// );
|
||||
/// ```
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
#[derive(Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Style {
|
||||
/// The foreground color.
|
||||
pub fg: Option<Color>,
|
||||
/// The background color.
|
||||
pub bg: Option<Color>,
|
||||
/// The underline color.
|
||||
#[cfg(feature = "underline-color")]
|
||||
pub underline_color: Option<Color>,
|
||||
/// The modifiers to add.
|
||||
pub add_modifier: Modifier,
|
||||
/// The modifiers to remove.
|
||||
pub sub_modifier: Modifier,
|
||||
}
|
||||
|
||||
/// A custom debug implementation that prints only the fields that are not the default, and unwraps
|
||||
/// the `Option`s.
|
||||
impl fmt::Debug for Style {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str("Style::new()")?;
|
||||
self.fmt_stylize(f)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for Style {
|
||||
type Item = Self;
|
||||
|
||||
@@ -247,6 +280,7 @@ impl Styled for Style {
|
||||
}
|
||||
|
||||
impl Style {
|
||||
/// Returns a `Style` with default properties.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
fg: None,
|
||||
@@ -275,7 +309,8 @@ impl Style {
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::style::{Color, Style};
|
||||
///
|
||||
/// let style = Style::default().fg(Color::Blue);
|
||||
/// let diff = Style::default().fg(Color::Red);
|
||||
/// assert_eq!(style.patch(diff), Style::default().fg(Color::Red));
|
||||
@@ -291,7 +326,8 @@ impl Style {
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::style::{Color, Style};
|
||||
///
|
||||
/// let style = Style::default().bg(Color::Blue);
|
||||
/// let diff = Style::default().bg(Color::Red);
|
||||
/// assert_eq!(style.patch(diff), Style::default().bg(Color::Red));
|
||||
@@ -315,7 +351,8 @@ impl Style {
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::style::{Color, Modifier, Style};
|
||||
///
|
||||
/// let style = Style::default()
|
||||
/// .underline_color(Color::Blue)
|
||||
/// .add_modifier(Modifier::UNDERLINED);
|
||||
@@ -343,7 +380,8 @@ impl Style {
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::style::{Modifier, Style};
|
||||
///
|
||||
/// let style = Style::default().add_modifier(Modifier::BOLD);
|
||||
/// let diff = Style::default().add_modifier(Modifier::ITALIC);
|
||||
/// let patched = style.patch(diff);
|
||||
@@ -364,7 +402,8 @@ impl Style {
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::style::{Modifier, Style};
|
||||
///
|
||||
/// let style = Style::default().add_modifier(Modifier::BOLD | Modifier::ITALIC);
|
||||
/// let diff = Style::default().remove_modifier(Modifier::ITALIC);
|
||||
/// let patched = style.patch(diff);
|
||||
@@ -386,7 +425,8 @@ impl Style {
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::style::{Color, Modifier, Style};
|
||||
///
|
||||
/// let style_1 = Style::default().fg(Color::Yellow);
|
||||
/// let style_2 = Style::default().bg(Color::Red);
|
||||
/// let combined = style_1.patch(style_2);
|
||||
@@ -413,6 +453,54 @@ impl Style {
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Formats the style in a way that can be copy-pasted into code using the style shorthands.
|
||||
///
|
||||
/// This is useful for debugging and for generating code snippets.
|
||||
pub(crate) fn fmt_stylize(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
use fmt::Debug;
|
||||
if let Some(fg) = self.fg {
|
||||
fg.stylize_debug(ColorDebugKind::Foreground).fmt(f)?;
|
||||
}
|
||||
if let Some(bg) = self.bg {
|
||||
bg.stylize_debug(ColorDebugKind::Background).fmt(f)?;
|
||||
}
|
||||
#[cfg(feature = "underline-color")]
|
||||
if let Some(underline_color) = self.underline_color {
|
||||
underline_color
|
||||
.stylize_debug(ColorDebugKind::Underline)
|
||||
.fmt(f)?;
|
||||
}
|
||||
for modifier in self.add_modifier.iter() {
|
||||
match modifier {
|
||||
Modifier::BOLD => f.write_str(".bold()")?,
|
||||
Modifier::DIM => f.write_str(".dim()")?,
|
||||
Modifier::ITALIC => f.write_str(".italic()")?,
|
||||
Modifier::UNDERLINED => f.write_str(".underlined()")?,
|
||||
Modifier::SLOW_BLINK => f.write_str(".slow_blink()")?,
|
||||
Modifier::RAPID_BLINK => f.write_str(".rapid_blink()")?,
|
||||
Modifier::REVERSED => f.write_str(".reversed()")?,
|
||||
Modifier::HIDDEN => f.write_str(".hidden()")?,
|
||||
Modifier::CROSSED_OUT => f.write_str(".crossed_out()")?,
|
||||
_ => f.write_fmt(format_args!(".add_modifier(Modifier::{modifier:?})"))?,
|
||||
}
|
||||
}
|
||||
for modifier in self.sub_modifier.iter() {
|
||||
match modifier {
|
||||
Modifier::BOLD => f.write_str(".not_bold()")?,
|
||||
Modifier::DIM => f.write_str(".not_dim()")?,
|
||||
Modifier::ITALIC => f.write_str(".not_italic()")?,
|
||||
Modifier::UNDERLINED => f.write_str(".not_underlined()")?,
|
||||
Modifier::SLOW_BLINK => f.write_str(".not_slow_blink()")?,
|
||||
Modifier::RAPID_BLINK => f.write_str(".not_rapid_blink()")?,
|
||||
Modifier::REVERSED => f.write_str(".not_reversed()")?,
|
||||
Modifier::HIDDEN => f.write_str(".not_hidden()")?,
|
||||
Modifier::CROSSED_OUT => f.write_str(".not_crossed_out()")?,
|
||||
_ => f.write_fmt(format_args!(".remove_modifier(Modifier::{modifier:?})"))?,
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color> for Style {
|
||||
@@ -423,7 +511,8 @@ impl From<Color> for Style {
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::style::{Color, Style};
|
||||
///
|
||||
/// let style = Style::from(Color::Red);
|
||||
/// ```
|
||||
fn from(color: Color) -> Self {
|
||||
@@ -437,7 +526,8 @@ impl From<(Color, Color)> for Style {
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::style::{Color, Style};
|
||||
///
|
||||
/// // red foreground, blue background
|
||||
/// let style = Style::from((Color::Red, Color::Blue));
|
||||
/// // default foreground, blue background
|
||||
@@ -459,7 +549,8 @@ impl From<Modifier> for Style {
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::style::{Style, Modifier};
|
||||
///
|
||||
/// // add bold and italic
|
||||
/// let style = Style::from(Modifier::BOLD|Modifier::ITALIC);
|
||||
fn from(modifier: Modifier) -> Self {
|
||||
@@ -473,7 +564,8 @@ impl From<(Modifier, Modifier)> for Style {
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::style::{Modifier, Style};
|
||||
///
|
||||
/// // add bold and italic, remove dim
|
||||
/// let style = Style::from((Modifier::BOLD | Modifier::ITALIC, Modifier::DIM));
|
||||
/// ```
|
||||
@@ -492,7 +584,8 @@ impl From<(Color, Modifier)> for Style {
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::style::{Color, Modifier, Style};
|
||||
///
|
||||
/// // red foreground, add bold and italic
|
||||
/// let style = Style::from((Color::Red, Modifier::BOLD | Modifier::ITALIC));
|
||||
/// ```
|
||||
@@ -509,7 +602,8 @@ impl From<(Color, Color, Modifier)> for Style {
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::style::{Color, Modifier, Style};
|
||||
///
|
||||
/// // red foreground, blue background, add bold and italic
|
||||
/// let style = Style::from((Color::Red, Color::Blue, Modifier::BOLD | Modifier::ITALIC));
|
||||
/// ```
|
||||
@@ -525,7 +619,8 @@ impl From<(Color, Color, Modifier, Modifier)> for Style {
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::style::{Color, Modifier, Style};
|
||||
///
|
||||
/// // red foreground, blue background, add bold and italic, remove dim
|
||||
/// let style = Style::from((
|
||||
/// Color::Red,
|
||||
@@ -549,6 +644,20 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[rstest]
|
||||
#[case(Style::new(), "Style::new()")]
|
||||
#[case(Style::new().red(), "Style::new().red()")]
|
||||
#[case(Style::new().on_blue(), "Style::new().on_blue()")]
|
||||
#[case(Style::new().bold(), "Style::new().bold()")]
|
||||
#[case(Style::new().not_italic(), "Style::new().not_italic()")]
|
||||
#[case(
|
||||
Style::new().red().on_blue().bold().italic().not_dim().not_hidden(),
|
||||
"Style::new().red().on_blue().bold().italic().not_dim().not_hidden()"
|
||||
)]
|
||||
fn debug(#[case] style: Style, #[case] expected: &'static str) {
|
||||
assert_eq!(format!("{style:?}"), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn combined_patch_gives_same_result_as_individual_patch() {
|
||||
let styles = [
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
use std::{fmt, str::FromStr};
|
||||
|
||||
use crate::style::stylize::{ColorDebug, ColorDebugKind};
|
||||
|
||||
/// ANSI Color
|
||||
///
|
||||
/// All colors from the [ANSI color table] are supported (though some names are not exactly the
|
||||
@@ -42,7 +44,7 @@ use std::{fmt, str::FromStr};
|
||||
/// ```
|
||||
/// use std::str::FromStr;
|
||||
///
|
||||
/// use ratatui::prelude::*;
|
||||
/// use ratatui_core::style::Color;
|
||||
///
|
||||
/// assert_eq!(Color::from_str("red"), Ok(Color::Red));
|
||||
/// assert_eq!("red".parse(), Ok(Color::Red));
|
||||
@@ -110,14 +112,12 @@ pub enum Color {
|
||||
/// Notably versions of Windows Terminal prior to Windows 10 and macOS Terminal.app do not
|
||||
/// support this.
|
||||
///
|
||||
/// If the terminal does not support true color, code using the [`TermwizBackend`] will
|
||||
/// If the terminal does not support true color, code using the `TermwizBackend` will
|
||||
/// fallback to the default text color. Crossterm and Termion do not have this capability and
|
||||
/// the display will be unpredictable (e.g. Terminal.app may display glitched blinking text).
|
||||
/// See <https://github.com/ratatui-org/ratatui/issues/475> for an example of this problem.
|
||||
/// See <https://github.com/ratatui/ratatui/issues/475> for an example of this problem.
|
||||
///
|
||||
/// See also: <https://en.wikipedia.org/wiki/ANSI_escape_code#24-bit>
|
||||
///
|
||||
/// [`TermwizBackend`]: crate::backend::TermwizBackend
|
||||
Rgb(u8, u8, u8),
|
||||
/// An 8-bit 256 color.
|
||||
///
|
||||
@@ -166,7 +166,9 @@ impl<'de> serde::Deserialize<'de> for Color {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::prelude::*;
|
||||
/// use std::str::FromStr;
|
||||
///
|
||||
/// use ratatui_core::style::Color;
|
||||
///
|
||||
/// #[derive(Debug, serde::Deserialize)]
|
||||
/// struct Theme {
|
||||
@@ -261,7 +263,7 @@ impl std::error::Error for ParseColorError {}
|
||||
/// ```
|
||||
/// use std::str::FromStr;
|
||||
///
|
||||
/// use ratatui::prelude::*;
|
||||
/// use ratatui_core::style::Color;
|
||||
///
|
||||
/// let color: Color = Color::from_str("blue").unwrap();
|
||||
/// assert_eq!(color, Color::Blue);
|
||||
@@ -361,111 +363,114 @@ impl fmt::Display for Color {
|
||||
}
|
||||
|
||||
impl Color {
|
||||
pub(crate) const fn stylize_debug(self, kind: ColorDebugKind) -> ColorDebug {
|
||||
ColorDebug { kind, color: self }
|
||||
}
|
||||
|
||||
/// Converts a HSL representation to a `Color::Rgb` instance.
|
||||
///
|
||||
/// The `from_hsl` function converts the Hue, Saturation and Lightness values to a
|
||||
/// corresponding `Color` RGB equivalent.
|
||||
/// The `from_hsl` function converts the Hue, Saturation and Lightness values to a corresponding
|
||||
/// `Color` RGB equivalent.
|
||||
///
|
||||
/// Hue values should be in the range [0, 360].
|
||||
/// Saturation and L values should be in the range [0, 100].
|
||||
/// Values that are not in the range are clamped to be within the range.
|
||||
/// Hue values should be in the range [-180..180]. Values outside this range are normalized by
|
||||
/// wrapping.
|
||||
///
|
||||
/// Saturation and L values should be in the range [0.0..1.0]. Values outside this range are
|
||||
/// clamped.
|
||||
///
|
||||
/// Clamping to valid ranges happens before conversion to RGB.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::prelude::*;
|
||||
/// use palette::Hsl;
|
||||
/// use ratatui_core::style::Color;
|
||||
///
|
||||
/// let color: Color = Color::from_hsl(360.0, 100.0, 100.0);
|
||||
/// // Minimum Lightness is black
|
||||
/// let color: Color = Color::from_hsl(Hsl::new(0.0, 0.0, 0.0));
|
||||
/// assert_eq!(color, Color::Rgb(0, 0, 0));
|
||||
///
|
||||
/// // Maximum Lightness is white
|
||||
/// let color: Color = Color::from_hsl(Hsl::new(0.0, 0.0, 1.0));
|
||||
/// assert_eq!(color, Color::Rgb(255, 255, 255));
|
||||
///
|
||||
/// let color: Color = Color::from_hsl(0.0, 0.0, 0.0);
|
||||
/// assert_eq!(color, Color::Rgb(0, 0, 0));
|
||||
/// // Minimum Saturation is fully desaturated red = gray
|
||||
/// let color: Color = Color::from_hsl(Hsl::new(0.0, 0.0, 0.5));
|
||||
/// assert_eq!(color, Color::Rgb(128, 128, 128));
|
||||
///
|
||||
/// // Bright red
|
||||
/// let color: Color = Color::from_hsl(Hsl::new(0.0, 1.0, 0.5));
|
||||
/// assert_eq!(color, Color::Rgb(255, 0, 0));
|
||||
///
|
||||
/// // Bright blue
|
||||
/// let color: Color = Color::from_hsl(Hsl::new(-120.0, 1.0, 0.5));
|
||||
/// assert_eq!(color, Color::Rgb(0, 0, 255));
|
||||
/// ```
|
||||
pub fn from_hsl(h: f64, s: f64, l: f64) -> Self {
|
||||
// Clamp input values to valid ranges
|
||||
let h = h.clamp(0.0, 360.0);
|
||||
let s = s.clamp(0.0, 100.0);
|
||||
let l = l.clamp(0.0, 100.0);
|
||||
#[cfg(feature = "palette")]
|
||||
pub fn from_hsl(hsl: palette::Hsl) -> Self {
|
||||
use palette::{Clamp, FromColor, Srgb};
|
||||
let hsl = hsl.clamp();
|
||||
let Srgb {
|
||||
red,
|
||||
green,
|
||||
blue,
|
||||
standard: _,
|
||||
}: Srgb<u8> = Srgb::from_color(hsl).into();
|
||||
|
||||
// Delegate to the function for normalized HSL to RGB conversion
|
||||
normalized_hsl_to_rgb(h / 360.0, s / 100.0, l / 100.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts normalized HSL (Hue, Saturation, Lightness) values to RGB (Red, Green, Blue) color
|
||||
/// representation. H, S, and L values should be in the range [0, 1].
|
||||
///
|
||||
/// Based on <https://github.com/killercup/hsl-rs/blob/b8a30e11afd75f262e0550725333293805f4ead0/src/lib.rs>
|
||||
fn normalized_hsl_to_rgb(hue: f64, saturation: f64, lightness: f64) -> Color {
|
||||
// This function can be made into `const` in the future.
|
||||
// This comment contains the relevant information for making it `const`.
|
||||
//
|
||||
// If it is `const` and made public, users can write the following:
|
||||
//
|
||||
// ```rust
|
||||
// const SLATE_50: Color = normalized_hsl_to_rgb(0.210, 0.40, 0.98);
|
||||
// ```
|
||||
//
|
||||
// For it to be const now, we need `#![feature(const_fn_floating_point_arithmetic)]`
|
||||
// Tracking issue: https://github.com/rust-lang/rust/issues/57241
|
||||
//
|
||||
// We would also need to remove the use of `.round()` in this function, i.e.:
|
||||
//
|
||||
// ```rust
|
||||
// Color::Rgb((r * 255.0) as u8, (g * 255.0) as u8, (b * 255.0) as u8)
|
||||
// ```
|
||||
|
||||
// Initialize RGB components
|
||||
let red: f64;
|
||||
let green: f64;
|
||||
let blue: f64;
|
||||
|
||||
// Check if the color is achromatic (grayscale)
|
||||
if saturation == 0.0 {
|
||||
red = lightness;
|
||||
green = lightness;
|
||||
blue = lightness;
|
||||
} else {
|
||||
// Calculate RGB components for colored cases
|
||||
let q = if lightness < 0.5 {
|
||||
lightness * (1.0 + saturation)
|
||||
} else {
|
||||
lightness + saturation - lightness * saturation
|
||||
};
|
||||
let p = 2.0 * lightness - q;
|
||||
red = hue_to_rgb(p, q, hue + 1.0 / 3.0);
|
||||
green = hue_to_rgb(p, q, hue);
|
||||
blue = hue_to_rgb(p, q, hue - 1.0 / 3.0);
|
||||
Self::Rgb(red, green, blue)
|
||||
}
|
||||
|
||||
// Scale RGB components to the range [0, 255] and create a Color::Rgb instance
|
||||
Color::Rgb(
|
||||
(red * 255.0).round() as u8,
|
||||
(green * 255.0).round() as u8,
|
||||
(blue * 255.0).round() as u8,
|
||||
)
|
||||
}
|
||||
/// Converts a `HSLuv` representation to a `Color::Rgb` instance.
|
||||
///
|
||||
/// The `from_hsluv` function converts the Hue, Saturation and Lightness values to a
|
||||
/// corresponding `Color` RGB equivalent.
|
||||
///
|
||||
/// Hue values should be in the range [-180.0..180.0]. Values outside this range are normalized
|
||||
/// by wrapping.
|
||||
///
|
||||
/// Saturation and L values should be in the range [0.0..100.0]. Values outside this range are
|
||||
/// clamped.
|
||||
///
|
||||
/// Clamping to valid ranges happens before conversion to RGB.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use palette::Hsluv;
|
||||
/// use ratatui_core::style::Color;
|
||||
///
|
||||
/// // Minimum Lightness is black
|
||||
/// let color: Color = Color::from_hsluv(Hsluv::new(0.0, 100.0, 0.0));
|
||||
/// assert_eq!(color, Color::Rgb(0, 0, 0));
|
||||
///
|
||||
/// // Maximum Lightness is white
|
||||
/// let color: Color = Color::from_hsluv(Hsluv::new(0.0, 0.0, 100.0));
|
||||
/// assert_eq!(color, Color::Rgb(255, 255, 255));
|
||||
///
|
||||
/// // Minimum Saturation is fully desaturated red = gray
|
||||
/// let color = Color::from_hsluv(Hsluv::new(0.0, 0.0, 50.0));
|
||||
/// assert_eq!(color, Color::Rgb(119, 119, 119));
|
||||
///
|
||||
/// // Bright Red
|
||||
/// let color = Color::from_hsluv(Hsluv::new(12.18, 100.0, 53.2));
|
||||
/// assert_eq!(color, Color::Rgb(255, 0, 0));
|
||||
///
|
||||
/// // Bright Blue
|
||||
/// let color = Color::from_hsluv(Hsluv::new(-94.13, 100.0, 32.3));
|
||||
/// assert_eq!(color, Color::Rgb(0, 0, 255));
|
||||
/// ```
|
||||
#[cfg(feature = "palette")]
|
||||
pub fn from_hsluv(hsluv: palette::Hsluv) -> Self {
|
||||
use palette::{Clamp, FromColor, Srgb};
|
||||
let hsluv = hsluv.clamp();
|
||||
let Srgb {
|
||||
red,
|
||||
green,
|
||||
blue,
|
||||
standard: _,
|
||||
}: Srgb<u8> = Srgb::from_color(hsluv).into();
|
||||
|
||||
/// Helper function to calculate RGB component for a specific hue value.
|
||||
fn hue_to_rgb(p: f64, q: f64, t: f64) -> f64 {
|
||||
// Adjust the hue value to be within the valid range [0, 1]
|
||||
let mut t = t;
|
||||
if t < 0.0 {
|
||||
t += 1.0;
|
||||
}
|
||||
if t > 1.0 {
|
||||
t -= 1.0;
|
||||
}
|
||||
|
||||
// Calculate the RGB component based on the hue value
|
||||
if t < 1.0 / 6.0 {
|
||||
p + (q - p) * 6.0 * t
|
||||
} else if t < 1.0 / 2.0 {
|
||||
q
|
||||
} else if t < 2.0 / 3.0 {
|
||||
p + (q - p) * (2.0 / 3.0 - t) * 6.0
|
||||
} else {
|
||||
p
|
||||
Self::Rgb(red, green, blue)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -473,36 +478,62 @@ fn hue_to_rgb(p: f64, q: f64, t: f64) -> f64 {
|
||||
mod tests {
|
||||
use std::error::Error;
|
||||
|
||||
#[cfg(feature = "palette")]
|
||||
use palette::{Hsl, Hsluv};
|
||||
#[cfg(feature = "palette")]
|
||||
use rstest::rstest;
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::de::{Deserialize, IntoDeserializer};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_hsl_to_rgb() {
|
||||
// Test with valid HSL values
|
||||
let color = Color::from_hsl(120.0, 50.0, 75.0);
|
||||
assert_eq!(color, Color::Rgb(159, 223, 159));
|
||||
#[cfg(feature = "palette")]
|
||||
#[rstest]
|
||||
#[case::black(Hsl::new(0.0, 0.0, 0.0), Color::Rgb(0, 0, 0))]
|
||||
#[case::white(Hsl::new(0.0, 0.0, 1.0), Color::Rgb(255, 255, 255))]
|
||||
#[case::valid(Hsl::new(120.0, 0.5, 0.75), Color::Rgb(159, 223, 159))]
|
||||
#[case::min_hue(Hsl::new(-180.0, 0.5, 0.75), Color::Rgb(159, 223, 223))]
|
||||
#[case::max_hue(Hsl::new(180.0, 0.5, 0.75), Color::Rgb(159, 223, 223))]
|
||||
#[case::min_saturation(Hsl::new(0.0, 0.0, 0.5), Color::Rgb(128, 128, 128))]
|
||||
#[case::max_saturation(Hsl::new(0.0, 1.0, 0.5), Color::Rgb(255, 0, 0))]
|
||||
#[case::min_lightness(Hsl::new(0.0, 0.5, 0.0), Color::Rgb(0, 0, 0))]
|
||||
#[case::max_lightness(Hsl::new(0.0, 0.5, 1.0), Color::Rgb(255, 255, 255))]
|
||||
#[case::under_hue_wraps(Hsl::new(-240.0, 0.5, 0.75), Color::Rgb(159, 223, 159))]
|
||||
#[case::over_hue_wraps(Hsl::new(480.0, 0.5, 0.75), Color::Rgb(159, 223, 159))]
|
||||
#[case::under_saturation_clamps(Hsl::new(0.0, -0.5, 0.75), Color::Rgb(191, 191, 191))]
|
||||
#[case::over_saturation_clamps(Hsl::new(0.0, 1.2, 0.75), Color::Rgb(255, 128, 128))]
|
||||
#[case::under_lightness_clamps(Hsl::new(0.0, 0.5, -0.20), Color::Rgb(0, 0, 0))]
|
||||
#[case::over_lightness_clamps(Hsl::new(0.0, 0.5, 1.5), Color::Rgb(255, 255, 255))]
|
||||
#[case::under_saturation_lightness_clamps(Hsl::new(0.0, -0.5, -0.20), Color::Rgb(0, 0, 0))]
|
||||
#[case::over_saturation_lightness_clamps(Hsl::new(0.0, 1.2, 1.5), Color::Rgb(255, 255, 255))]
|
||||
fn test_hsl_to_rgb(#[case] hsl: palette::Hsl, #[case] expected: Color) {
|
||||
assert_eq!(Color::from_hsl(hsl), expected);
|
||||
}
|
||||
|
||||
// Test with H value at upper bound
|
||||
let color = Color::from_hsl(360.0, 50.0, 75.0);
|
||||
assert_eq!(color, Color::Rgb(223, 159, 159));
|
||||
|
||||
// Test with H value exceeding the upper bound
|
||||
let color = Color::from_hsl(400.0, 50.0, 75.0);
|
||||
assert_eq!(color, Color::Rgb(223, 159, 159));
|
||||
|
||||
// Test with S and L values exceeding the upper bound
|
||||
let color = Color::from_hsl(240.0, 120.0, 150.0);
|
||||
assert_eq!(color, Color::Rgb(255, 255, 255));
|
||||
|
||||
// Test with H, S, and L values below the lower bound
|
||||
let color = Color::from_hsl(-20.0, -50.0, -20.0);
|
||||
assert_eq!(color, Color::Rgb(0, 0, 0));
|
||||
|
||||
// Test with S and L values below the lower bound
|
||||
let color = Color::from_hsl(60.0, -20.0, -10.0);
|
||||
assert_eq!(color, Color::Rgb(0, 0, 0));
|
||||
#[cfg(feature = "palette")]
|
||||
#[rstest]
|
||||
#[case::black(Hsluv::new(0.0, 0.0, 0.0), Color::Rgb(0, 0, 0))]
|
||||
#[case::white(Hsluv::new(0.0, 0.0, 100.0), Color::Rgb(255, 255, 255))]
|
||||
#[case::valid(Hsluv::new(120.0, 50.0, 75.0), Color::Rgb(147, 198, 129))]
|
||||
#[case::min_hue(Hsluv::new(-180.0, 50.0, 75.0), Color::Rgb(135,196, 188))]
|
||||
#[case::max_hue(Hsluv::new(180.0, 50.0, 75.0), Color::Rgb(135, 196, 188))]
|
||||
#[case::min_saturation(Hsluv::new(0.0, 0.0, 75.0), Color::Rgb(185, 185, 185))]
|
||||
#[case::max_saturation(Hsluv::new(0.0, 100.0, 75.0), Color::Rgb(255, 156, 177))]
|
||||
#[case::min_lightness(Hsluv::new(0.0, 50.0, 0.0), Color::Rgb(0, 0, 0))]
|
||||
#[case::max_lightness(Hsluv::new(0.0, 50.0, 100.0), Color::Rgb(255, 255, 255))]
|
||||
#[case::under_hue_wraps(Hsluv::new(-240.0, 50.0, 75.0), Color::Rgb(147, 198, 129))]
|
||||
#[case::over_hue_wraps(Hsluv::new(480.0, 50.0, 75.0), Color::Rgb(147, 198, 129))]
|
||||
#[case::under_saturation_clamps(Hsluv::new(0.0, -50.0, 75.0), Color::Rgb(185, 185, 185))]
|
||||
#[case::over_saturation_clamps(Hsluv::new(0.0, 150.0, 75.0), Color::Rgb(255, 156, 177))]
|
||||
#[case::under_lightness_clamps(Hsluv::new(0.0, 50.0, -20.0), Color::Rgb(0, 0, 0))]
|
||||
#[case::over_lightness_clamps(Hsluv::new(0.0, 50.0, 150.0), Color::Rgb(255, 255, 255))]
|
||||
#[case::under_saturation_lightness_clamps(Hsluv::new(0.0, -50.0, -20.0), Color::Rgb(0, 0, 0))]
|
||||
#[case::over_saturation_lightness_clamps(
|
||||
Hsluv::new(0.0, 150.0, 150.0),
|
||||
Color::Rgb(255, 255, 255)
|
||||
)]
|
||||
fn test_hsluv_to_rgb(#[case] hsluv: palette::Hsluv, #[case] expected: Color) {
|
||||
assert_eq!(Color::from_hsluv(hsluv), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -403,8 +403,10 @@
|
||||
//! # Example
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use ratatui::prelude::*;
|
||||
//! use ratatui::style::palette::material::{BLUE, RED};
|
||||
//! use ratatui_core::style::{
|
||||
//! palette::material::{BLUE, RED},
|
||||
//! Color,
|
||||
//! };
|
||||
//!
|
||||
//! assert_eq!(RED.c500, Color::Rgb(244, 67, 54));
|
||||
//! assert_eq!(BLUE.c500, Color::Rgb(33, 150, 243));
|
||||
@@ -412,7 +414,7 @@
|
||||
//!
|
||||
//! [`matdesign-color` crate]: https://crates.io/crates/matdesign-color
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::style::Color;
|
||||
|
||||
/// A palette of colors for use in Material design with accent colors
|
||||
///
|
||||
@@ -268,14 +268,16 @@
|
||||
//! # Example
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use ratatui::prelude::*;
|
||||
//! use ratatui::style::palette::tailwind::{BLUE, RED};
|
||||
//! use ratatui_core::style::{
|
||||
//! palette::tailwind::{BLUE, RED},
|
||||
//! Color,
|
||||
//! };
|
||||
//!
|
||||
//! assert_eq!(RED.c500, Color::Rgb(239, 68, 68));
|
||||
//! assert_eq!(BLUE.c500, Color::Rgb(59, 130, 246));
|
||||
//! ```
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::style::Color;
|
||||
|
||||
pub struct Palette {
|
||||
pub c50: Color,
|
||||
@@ -7,7 +7,7 @@ use ::palette::{
|
||||
};
|
||||
use palette::{stimulus::IntoStimulus, Srgb};
|
||||
|
||||
use super::Color;
|
||||
use crate::style::Color;
|
||||
|
||||
/// Convert an [`palette::Srgb`] color to a [`Color`].
|
||||
///
|
||||
@@ -15,7 +15,7 @@ use super::Color;
|
||||
///
|
||||
/// ```
|
||||
/// use palette::Srgb;
|
||||
/// use ratatui::style::Color;
|
||||
/// use ratatui_core::style::Color;
|
||||
///
|
||||
/// let color = Color::from(Srgb::new(1.0f32, 0.0, 0.0));
|
||||
/// assert_eq!(color, Color::Rgb(255, 0, 0));
|
||||
@@ -36,7 +36,7 @@ impl<T: IntoStimulus<u8>> From<Srgb<T>> for Color {
|
||||
///
|
||||
/// ```
|
||||
/// use palette::LinSrgb;
|
||||
/// use ratatui::style::Color;
|
||||
/// use ratatui_core::style::Color;
|
||||
///
|
||||
/// let color = Color::from(LinSrgb::new(1.0f32, 0.0, 0.0));
|
||||
/// assert_eq!(color, Color::Rgb(255, 0, 0));
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::fmt;
|
||||
|
||||
use paste::paste;
|
||||
|
||||
use crate::{
|
||||
@@ -23,6 +25,75 @@ pub trait Styled {
|
||||
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item;
|
||||
}
|
||||
|
||||
/// A helper struct to make it easy to debug using the `Stylize` method names
|
||||
pub(crate) struct ColorDebug {
|
||||
pub kind: ColorDebugKind,
|
||||
pub color: Color,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub(crate) enum ColorDebugKind {
|
||||
Foreground,
|
||||
Background,
|
||||
#[cfg(feature = "underline-color")]
|
||||
Underline,
|
||||
}
|
||||
|
||||
impl fmt::Debug for ColorDebug {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
#[cfg(feature = "underline-color")]
|
||||
let is_underline = self.kind == ColorDebugKind::Underline;
|
||||
#[cfg(not(feature = "underline-color"))]
|
||||
let is_underline = false;
|
||||
if is_underline
|
||||
|| matches!(
|
||||
self.color,
|
||||
Color::Reset | Color::Indexed(_) | Color::Rgb(_, _, _)
|
||||
)
|
||||
{
|
||||
match self.kind {
|
||||
ColorDebugKind::Foreground => write!(f, ".fg(")?,
|
||||
ColorDebugKind::Background => write!(f, ".bg(")?,
|
||||
#[cfg(feature = "underline-color")]
|
||||
ColorDebugKind::Underline => write!(f, ".underline_color(")?,
|
||||
}
|
||||
write!(f, "Color::{:?}", self.color)?;
|
||||
write!(f, ")")?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
match self.kind {
|
||||
ColorDebugKind::Foreground => write!(f, ".")?,
|
||||
ColorDebugKind::Background => write!(f, ".on_")?,
|
||||
// TODO: .underline_color_xxx is not implemented on Stylize yet, but it should be
|
||||
#[cfg(feature = "underline-color")]
|
||||
ColorDebugKind::Underline => {
|
||||
unreachable!("covered by the first part of the if statement")
|
||||
}
|
||||
}
|
||||
match self.color {
|
||||
Color::Black => write!(f, "black")?,
|
||||
Color::Red => write!(f, "red")?,
|
||||
Color::Green => write!(f, "green")?,
|
||||
Color::Yellow => write!(f, "yellow")?,
|
||||
Color::Blue => write!(f, "blue")?,
|
||||
Color::Magenta => write!(f, "magenta")?,
|
||||
Color::Cyan => write!(f, "cyan")?,
|
||||
Color::Gray => write!(f, "gray")?,
|
||||
Color::DarkGray => write!(f, "dark_gray")?,
|
||||
Color::LightRed => write!(f, "light_red")?,
|
||||
Color::LightGreen => write!(f, "light_green")?,
|
||||
Color::LightYellow => write!(f, "light_yellow")?,
|
||||
Color::LightBlue => write!(f, "light_blue")?,
|
||||
Color::LightMagenta => write!(f, "light_magenta")?,
|
||||
Color::LightCyan => write!(f, "light_cyan")?,
|
||||
Color::White => write!(f, "white")?,
|
||||
_ => unreachable!("covered by the first part of the if statement"),
|
||||
}
|
||||
write!(f, "()")
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates two methods for each color, one for setting the foreground color (`red()`, `blue()`,
|
||||
/// etc) and one for setting the background color (`on_red()`, `on_blue()`, etc.). Each method sets
|
||||
/// the color of the style to the corresponding color.
|
||||
@@ -124,8 +195,12 @@ macro_rules! modifier {
|
||||
/// by `not_`). The `reset()` method is also provided to reset the style.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
/// ```ignore
|
||||
/// use ratatui_core::{
|
||||
/// style::{Color, Modifier, Style, Stylize},
|
||||
/// text::Line,
|
||||
/// widgets::{Block, Paragraph},
|
||||
/// };
|
||||
///
|
||||
/// let span = "hello".red().on_blue().bold();
|
||||
/// let line = Line::from(vec![
|
||||
@@ -231,6 +306,7 @@ impl Styled for String {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use itertools::Itertools;
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -346,7 +422,7 @@ mod tests {
|
||||
// issue as above without the `Styled` trait impl for `String`
|
||||
let items = [String::from("a"), String::from("b")];
|
||||
let sss = items.iter().map(|s| format!("{s}{s}").red()).collect_vec();
|
||||
assert_eq!(sss, vec![Span::from("aa").red(), Span::from("bb").red()]);
|
||||
assert_eq!(sss, [Span::from("aa").red(), Span::from("bb").red()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -423,4 +499,77 @@ mod tests {
|
||||
Span::styled("hello", all_modifier_black)
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(Color::Black, ".black()")]
|
||||
#[case(Color::Red, ".red()")]
|
||||
#[case(Color::Green, ".green()")]
|
||||
#[case(Color::Yellow, ".yellow()")]
|
||||
#[case(Color::Blue, ".blue()")]
|
||||
#[case(Color::Magenta, ".magenta()")]
|
||||
#[case(Color::Cyan, ".cyan()")]
|
||||
#[case(Color::Gray, ".gray()")]
|
||||
#[case(Color::DarkGray, ".dark_gray()")]
|
||||
#[case(Color::LightRed, ".light_red()")]
|
||||
#[case(Color::LightGreen, ".light_green()")]
|
||||
#[case(Color::LightYellow, ".light_yellow()")]
|
||||
#[case(Color::LightBlue, ".light_blue()")]
|
||||
#[case(Color::LightMagenta, ".light_magenta()")]
|
||||
#[case(Color::LightCyan, ".light_cyan()")]
|
||||
#[case(Color::White, ".white()")]
|
||||
#[case(Color::Indexed(10), ".fg(Color::Indexed(10))")]
|
||||
#[case(Color::Rgb(255, 0, 0), ".fg(Color::Rgb(255, 0, 0))")]
|
||||
fn stylize_debug_foreground(#[case] color: Color, #[case] expected: &str) {
|
||||
let debug = color.stylize_debug(ColorDebugKind::Foreground);
|
||||
assert_eq!(format!("{debug:?}"), expected);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(Color::Black, ".on_black()")]
|
||||
#[case(Color::Red, ".on_red()")]
|
||||
#[case(Color::Green, ".on_green()")]
|
||||
#[case(Color::Yellow, ".on_yellow()")]
|
||||
#[case(Color::Blue, ".on_blue()")]
|
||||
#[case(Color::Magenta, ".on_magenta()")]
|
||||
#[case(Color::Cyan, ".on_cyan()")]
|
||||
#[case(Color::Gray, ".on_gray()")]
|
||||
#[case(Color::DarkGray, ".on_dark_gray()")]
|
||||
#[case(Color::LightRed, ".on_light_red()")]
|
||||
#[case(Color::LightGreen, ".on_light_green()")]
|
||||
#[case(Color::LightYellow, ".on_light_yellow()")]
|
||||
#[case(Color::LightBlue, ".on_light_blue()")]
|
||||
#[case(Color::LightMagenta, ".on_light_magenta()")]
|
||||
#[case(Color::LightCyan, ".on_light_cyan()")]
|
||||
#[case(Color::White, ".on_white()")]
|
||||
#[case(Color::Indexed(10), ".bg(Color::Indexed(10))")]
|
||||
#[case(Color::Rgb(255, 0, 0), ".bg(Color::Rgb(255, 0, 0))")]
|
||||
fn stylize_debug_background(#[case] color: Color, #[case] expected: &str) {
|
||||
let debug = color.stylize_debug(ColorDebugKind::Background);
|
||||
assert_eq!(format!("{debug:?}"), expected);
|
||||
}
|
||||
|
||||
#[cfg(feature = "underline-color")]
|
||||
#[rstest]
|
||||
#[case(Color::Black, ".underline_color(Color::Black)")]
|
||||
#[case(Color::Red, ".underline_color(Color::Red)")]
|
||||
#[case(Color::Green, ".underline_color(Color::Green)")]
|
||||
#[case(Color::Yellow, ".underline_color(Color::Yellow)")]
|
||||
#[case(Color::Blue, ".underline_color(Color::Blue)")]
|
||||
#[case(Color::Magenta, ".underline_color(Color::Magenta)")]
|
||||
#[case(Color::Cyan, ".underline_color(Color::Cyan)")]
|
||||
#[case(Color::Gray, ".underline_color(Color::Gray)")]
|
||||
#[case(Color::DarkGray, ".underline_color(Color::DarkGray)")]
|
||||
#[case(Color::LightRed, ".underline_color(Color::LightRed)")]
|
||||
#[case(Color::LightGreen, ".underline_color(Color::LightGreen)")]
|
||||
#[case(Color::LightYellow, ".underline_color(Color::LightYellow)")]
|
||||
#[case(Color::LightBlue, ".underline_color(Color::LightBlue)")]
|
||||
#[case(Color::LightMagenta, ".underline_color(Color::LightMagenta)")]
|
||||
#[case(Color::LightCyan, ".underline_color(Color::LightCyan)")]
|
||||
#[case(Color::White, ".underline_color(Color::White)")]
|
||||
#[case(Color::Indexed(10), ".underline_color(Color::Indexed(10))")]
|
||||
#[case(Color::Rgb(255, 0, 0), ".underline_color(Color::Rgb(255, 0, 0))")]
|
||||
fn stylize_debug_underline(#[case] color: Color, #[case] expected: &str) {
|
||||
let debug = color.stylize_debug(ColorDebugKind::Underline);
|
||||
assert_eq!(format!("{debug:?}"), expected);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
//! Symbols and markers for drawing various widgets.
|
||||
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
pub mod border;
|
||||
@@ -155,7 +157,7 @@ pub enum Marker {
|
||||
}
|
||||
|
||||
pub mod scrollbar {
|
||||
use super::{block, line};
|
||||
use crate::symbols::{block, line};
|
||||
|
||||
/// Scrollbar Set
|
||||
/// ```text
|
||||
@@ -203,6 +205,14 @@ pub mod scrollbar {
|
||||
};
|
||||
}
|
||||
|
||||
pub mod shade {
|
||||
pub const EMPTY: &str = " ";
|
||||
pub const LIGHT: &str = "░";
|
||||
pub const MEDIUM: &str = "▒";
|
||||
pub const DARK: &str = "▓";
|
||||
pub const FULL: &str = "█";
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use strum::ParseError;
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::{block, line};
|
||||
use crate::symbols::{block, line};
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub struct Set {
|
||||
@@ -1,25 +1,29 @@
|
||||
//! Primitives for styled text.
|
||||
//!
|
||||
//! A terminal UI is at its root a lot of strings. In order to make it accessible and stylish,
|
||||
//! those strings may be associated to a set of styles. `ratatui` has three ways to represent them:
|
||||
//! A terminal UI is at its root a lot of strings. In order to make it accessible and stylish, those
|
||||
//! strings may be associated to a set of styles. `ratatui` has three ways to represent them:
|
||||
//! - A single line string where all graphemes have the same style is represented by a [`Span`].
|
||||
//! - A single line string where each grapheme may have its own style is represented by [`Line`].
|
||||
//! - A multiple line string where each grapheme may have its own style is represented by a
|
||||
//! [`Text`].
|
||||
//!
|
||||
//! These types form a hierarchy: [`Line`] is a collection of [`Span`] and each line of [`Text`]
|
||||
//! is a [`Line`].
|
||||
//! These types form a hierarchy: [`Line`] is a collection of [`Span`] and each line of [`Text`] is
|
||||
//! a [`Line`].
|
||||
//!
|
||||
//! Keep it mind that a lot of widgets will use those types to advertise what kind of string is
|
||||
//! supported for their properties. Moreover, `ratatui` provides convenient `From` implementations
|
||||
//! so that you can start by using simple `String` or `&str` and then promote them to the previous
|
||||
//! primitives when you need additional styling capabilities.
|
||||
//!
|
||||
//! For example, for the [`crate::widgets::Block`] widget, all the following calls are valid to set
|
||||
//! its `title` property (which is a [`Line`] under the hood):
|
||||
//! For example, for the `Block` widget, all the following calls are valid to set its `title`
|
||||
//! property (which is a [`Line`] under the hood):
|
||||
//!
|
||||
//! ```rust
|
||||
//! use ratatui::{prelude::*, widgets::*};
|
||||
//! ```rust,ignore
|
||||
//! use ratatui_core::{
|
||||
//! style::{Color, Style},
|
||||
//! text::{Line, Span},
|
||||
//! widgets::Block,
|
||||
//! };
|
||||
//!
|
||||
//! // A simple string with no styling.
|
||||
//! // Converted to Line(vec![
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{prelude::*, style::Styled};
|
||||
use crate::style::{Style, Styled};
|
||||
|
||||
const NBSP: &str = "\u{00a0}";
|
||||
const ZWSP: &str = "\u{200b}";
|
||||
@@ -19,6 +19,8 @@ impl<'a> StyledGrapheme<'a> {
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// [`Color`]: crate::style::Color
|
||||
pub fn new<S: Into<Style>>(symbol: &'a str, style: S) -> Self {
|
||||
Self {
|
||||
symbol,
|
||||
@@ -26,7 +28,7 @@ impl<'a> StyledGrapheme<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_whitespace(&self) -> bool {
|
||||
pub fn is_whitespace(&self) -> bool {
|
||||
let symbol = self.symbol;
|
||||
symbol == ZWSP || symbol.chars().all(char::is_whitespace) && symbol != NBSP
|
||||
}
|
||||
@@ -48,6 +50,7 @@ impl<'a> Styled for StyledGrapheme<'a> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::style::Stylize;
|
||||
|
||||
#[test]
|
||||
fn new() {
|
||||
293
src/text/line.rs → ratatui-core/src/text/line.rs
Normal file → Executable file
293
src/text/line.rs → ratatui-core/src/text/line.rs
Normal file → Executable file
@@ -4,7 +4,13 @@ use std::{borrow::Cow, fmt};
|
||||
|
||||
use unicode_truncate::UnicodeTruncateStr;
|
||||
|
||||
use crate::{prelude::*, style::Styled, text::StyledGrapheme};
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Rect},
|
||||
style::{Style, Styled},
|
||||
text::{Span, StyledGrapheme, Text},
|
||||
widgets::Widget,
|
||||
};
|
||||
|
||||
/// A line of text, consisting of one or more [`Span`]s.
|
||||
///
|
||||
@@ -69,7 +75,10 @@ use crate::{prelude::*, style::Styled, text::StyledGrapheme};
|
||||
/// [`Style`].
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::prelude::*;
|
||||
/// use ratatui_core::{
|
||||
/// style::{Color, Modifier, Style, Stylize},
|
||||
/// text::{Line, Span},
|
||||
/// };
|
||||
///
|
||||
/// let style = Style::new().yellow();
|
||||
/// let line = Line::raw("Hello, world!").style(style);
|
||||
@@ -93,7 +102,11 @@ use crate::{prelude::*, style::Styled, text::StyledGrapheme};
|
||||
/// methods of the [`Stylize`] trait.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::{
|
||||
/// style::{Color, Modifier, Style, Stylize},
|
||||
/// text::Line,
|
||||
/// };
|
||||
///
|
||||
/// let line = Line::from("Hello world!").style(Style::new().yellow().italic());
|
||||
/// let line = Line::from("Hello world!").style(Color::Yellow);
|
||||
/// let line = Line::from("Hello world!").style((Color::Yellow, Color::Black));
|
||||
@@ -108,7 +121,8 @@ use crate::{prelude::*, style::Styled, text::StyledGrapheme};
|
||||
/// ignored and the line is truncated.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::{layout::Alignment, text::Line};
|
||||
///
|
||||
/// let line = Line::from("Hello world!").alignment(Alignment::Right);
|
||||
/// let line = Line::from("Hello world!").centered();
|
||||
/// let line = Line::from("Hello world!").left_aligned();
|
||||
@@ -120,26 +134,44 @@ use crate::{prelude::*, style::Styled, text::StyledGrapheme};
|
||||
/// `Line` implements the [`Widget`] trait, which means it can be rendered to a [`Buffer`].
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::{
|
||||
/// buffer::Buffer,
|
||||
/// layout::Rect,
|
||||
/// style::{Style, Stylize},
|
||||
/// text::Line,
|
||||
/// widgets::Widget,
|
||||
/// };
|
||||
///
|
||||
/// # fn render(area: Rect, buf: &mut Buffer) {
|
||||
/// // in another widget's render method
|
||||
/// let line = Line::from("Hello world!").style(Style::new().yellow().italic());
|
||||
/// line.render(area, buf);
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// Or you can use the `render_widget` method on the `Frame` in a `Terminal::draw` closure.
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// # use ratatui::{Frame, layout::Rect, text::Line};
|
||||
/// # fn draw(frame: &mut Frame, area: Rect) {
|
||||
/// // in a terminal.draw closure
|
||||
/// let line = Line::from("Hello world!").style(Style::new().yellow().italic());
|
||||
/// let line = Line::from("Hello world!");
|
||||
/// frame.render_widget(line, area);
|
||||
/// # }
|
||||
/// ```
|
||||
/// ## Rendering Lines with a Paragraph widget
|
||||
///
|
||||
/// Usually apps will use the [`Paragraph`] widget instead of rendering a [`Line`] directly as it
|
||||
/// Usually apps will use the `Paragraph` widget instead of rendering a [`Line`] directly as it
|
||||
/// provides more functionality.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// ```rust,ignore
|
||||
/// use ratatui::{
|
||||
/// buffer::Buffer,
|
||||
/// layout::Rect,
|
||||
/// style::Stylize,
|
||||
/// text::Line,
|
||||
/// widgets::{Paragraph, Widget, Wrap},
|
||||
/// };
|
||||
///
|
||||
/// # fn render(area: Rect, buf: &mut Buffer) {
|
||||
/// let line = Line::from("Hello world!").yellow().italic();
|
||||
/// Paragraph::new(line)
|
||||
@@ -148,17 +180,44 @@ use crate::{prelude::*, style::Styled, text::StyledGrapheme};
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// [`Paragraph`]: crate::widgets::Paragraph
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
/// [`Stylize`]: crate::style::Stylize
|
||||
#[derive(Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Line<'a> {
|
||||
/// The spans that make up this line of text.
|
||||
pub spans: Vec<Span<'a>>,
|
||||
|
||||
/// The style of this line of text.
|
||||
pub style: Style,
|
||||
|
||||
/// The alignment of this line of text.
|
||||
pub alignment: Option<Alignment>,
|
||||
|
||||
/// The spans that make up this line of text.
|
||||
pub spans: Vec<Span<'a>>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for Line<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if self.spans.is_empty() {
|
||||
f.write_str("Line::default()")?;
|
||||
} else if self.spans.len() == 1 && self.spans[0].style == Style::default() {
|
||||
f.write_str(r#"Line::from(""#)?;
|
||||
f.write_str(&self.spans[0].content)?;
|
||||
f.write_str(r#"")"#)?;
|
||||
} else if self.spans.len() == 1 {
|
||||
f.write_str("Line::from(")?;
|
||||
self.spans[0].fmt(f)?;
|
||||
f.write_str(")")?;
|
||||
} else {
|
||||
f.write_str("Line::from_iter(")?;
|
||||
f.debug_list().entries(&self.spans).finish()?;
|
||||
f.write_str(")")?;
|
||||
}
|
||||
self.style.fmt_stylize(f)?;
|
||||
match self.alignment {
|
||||
Some(Alignment::Left) => write!(f, ".left_aligned()"),
|
||||
Some(Alignment::Center) => write!(f, ".centered()"),
|
||||
Some(Alignment::Right) => write!(f, ".right_aligned()"),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn cow_to_spans<'a>(content: impl Into<Cow<'a, str>>) -> Vec<Span<'a>> {
|
||||
@@ -182,8 +241,10 @@ impl<'a> Line<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// # use std::borrow::Cow;
|
||||
/// use std::borrow::Cow;
|
||||
///
|
||||
/// use ratatui_core::text::Line;
|
||||
///
|
||||
/// Line::raw("test content");
|
||||
/// Line::raw(String::from("test content"));
|
||||
/// Line::raw(Cow::from("test content"));
|
||||
@@ -211,13 +272,20 @@ impl<'a> Line<'a> {
|
||||
/// Any newlines in the content are removed.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// # use std::borrow::Cow;
|
||||
/// use std::borrow::Cow;
|
||||
///
|
||||
/// use ratatui_core::{
|
||||
/// style::{Style, Stylize},
|
||||
/// text::Line,
|
||||
/// };
|
||||
///
|
||||
/// let style = Style::new().yellow().italic();
|
||||
/// Line::styled("My text", style);
|
||||
/// Line::styled(String::from("My text"), style);
|
||||
/// Line::styled(Cow::from("test content"), style);
|
||||
/// ```
|
||||
///
|
||||
/// [`Color`]: crate::style::Color
|
||||
pub fn styled<T, S>(content: T, style: S) -> Self
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
@@ -238,7 +306,8 @@ impl<'a> Line<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::{style::Stylize, text::Line};
|
||||
///
|
||||
/// let line = Line::default().spans(vec!["Hello".blue(), " world!".green()]);
|
||||
/// let line = Line::default().spans([1, 2, 3].iter().map(|i| format!("Item {}", i)));
|
||||
/// ```
|
||||
@@ -265,9 +334,15 @@ impl<'a> Line<'a> {
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::{
|
||||
/// style::{Style, Stylize},
|
||||
/// text::Line,
|
||||
/// };
|
||||
///
|
||||
/// let mut line = Line::from("foo").style(Style::new().red());
|
||||
/// ```
|
||||
///
|
||||
/// [`Color`]: crate::style::Color
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.style = style.into();
|
||||
@@ -283,7 +358,8 @@ impl<'a> Line<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::{layout::Alignment, text::Line};
|
||||
///
|
||||
/// let mut line = Line::from("Hi, what's up?");
|
||||
/// assert_eq!(None, line.alignment);
|
||||
/// assert_eq!(
|
||||
@@ -308,7 +384,8 @@ impl<'a> Line<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::text::Line;
|
||||
///
|
||||
/// let line = Line::from("Hi, what's up?").left_aligned();
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
@@ -325,7 +402,8 @@ impl<'a> Line<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::text::Line;
|
||||
///
|
||||
/// let line = Line::from("Hi, what's up?").centered();
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
@@ -342,7 +420,8 @@ impl<'a> Line<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::text::Line;
|
||||
///
|
||||
/// let line = Line::from("Hi, what's up?").right_aligned();
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
@@ -355,7 +434,8 @@ impl<'a> Line<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::{style::Stylize, text::Line};
|
||||
///
|
||||
/// let line = Line::from(vec!["Hello".blue(), " world!".green()]);
|
||||
/// assert_eq!(12, line.width());
|
||||
/// ```
|
||||
@@ -376,7 +456,10 @@ impl<'a> Line<'a> {
|
||||
/// ```rust
|
||||
/// use std::iter::Iterator;
|
||||
///
|
||||
/// use ratatui::{prelude::*, text::StyledGrapheme};
|
||||
/// use ratatui_core::{
|
||||
/// style::{Color, Style},
|
||||
/// text::{Line, StyledGrapheme},
|
||||
/// };
|
||||
///
|
||||
/// let line = Line::styled("Text", Style::default().fg(Color::Yellow));
|
||||
/// let style = Style::default().fg(Color::Green).bg(Color::Black);
|
||||
@@ -391,6 +474,8 @@ impl<'a> Line<'a> {
|
||||
/// ]
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// [`Color`]: crate::style::Color
|
||||
pub fn styled_graphemes<S: Into<Style>>(
|
||||
&'a self,
|
||||
base_style: S,
|
||||
@@ -415,13 +500,19 @@ impl<'a> Line<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::{
|
||||
/// style::{Color, Modifier},
|
||||
/// text::Line,
|
||||
/// };
|
||||
///
|
||||
/// let line = Line::styled("My text", Modifier::ITALIC);
|
||||
///
|
||||
/// let styled_line = Line::styled("My text", (Color::Yellow, Modifier::ITALIC));
|
||||
///
|
||||
/// assert_eq!(styled_line, line.patch_style(Color::Yellow));
|
||||
/// ```
|
||||
///
|
||||
/// [`Color`]: crate::style::Color
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn patch_style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.style = self.style.patch(style);
|
||||
@@ -437,8 +528,12 @@ impl<'a> Line<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// # let style = Style::default().yellow();
|
||||
/// use ratatui_core::{
|
||||
/// style::{Style, Stylize},
|
||||
/// text::Line,
|
||||
/// };
|
||||
///
|
||||
/// let line = Line::styled("My text", style);
|
||||
///
|
||||
/// assert_eq!(Style::reset(), line.reset_style().style);
|
||||
@@ -466,7 +561,8 @@ impl<'a> Line<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::text::{Line, Span};
|
||||
///
|
||||
/// let mut line = Line::from("Hello, ");
|
||||
/// line.push_span(Span::raw("world!"));
|
||||
/// line.push_span(" How are you?");
|
||||
@@ -515,6 +611,12 @@ impl<'a> From<&'a str> for Line<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Cow<'a, str>> for Line<'a> {
|
||||
fn from(s: Cow<'a, str>) -> Self {
|
||||
Self::raw(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Vec<Span<'a>>> for Line<'a> {
|
||||
fn from(spans: Vec<Span<'a>>) -> Self {
|
||||
Self {
|
||||
@@ -581,12 +683,25 @@ impl<'a> Extend<Span<'a>> for Line<'a> {
|
||||
|
||||
impl Widget for Line<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.render_ref(area, buf);
|
||||
Widget::render(&self, area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetRef for Line<'_> {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
impl Widget for &Line<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.render_with_alignment(area, buf, None);
|
||||
}
|
||||
}
|
||||
|
||||
impl Line<'_> {
|
||||
/// An internal implementation method for `Widget::render` that allows the parent widget to
|
||||
/// define a default alignment, to be used if `Line::alignment` is `None`.
|
||||
pub(crate) fn render_with_alignment(
|
||||
&self,
|
||||
area: Rect,
|
||||
buf: &mut Buffer,
|
||||
parent_alignment: Option<Alignment>,
|
||||
) {
|
||||
let area = area.intersection(buf.area);
|
||||
if area.is_empty() {
|
||||
return;
|
||||
@@ -599,10 +714,12 @@ impl WidgetRef for Line<'_> {
|
||||
|
||||
buf.set_style(area, self.style);
|
||||
|
||||
let alignment = self.alignment.or(parent_alignment);
|
||||
|
||||
let area_width = usize::from(area.width);
|
||||
let can_render_complete_line = line_width <= area_width;
|
||||
if can_render_complete_line {
|
||||
let indent_width = match self.alignment {
|
||||
let indent_width = match alignment {
|
||||
Some(Alignment::Center) => (area_width.saturating_sub(line_width)) / 2,
|
||||
Some(Alignment::Right) => area_width.saturating_sub(line_width),
|
||||
Some(Alignment::Left) | None => 0,
|
||||
@@ -613,7 +730,7 @@ impl WidgetRef for Line<'_> {
|
||||
} else {
|
||||
// There is not enough space to render the whole line. As the right side is truncated by
|
||||
// the area width, only truncate the left.
|
||||
let skip_width = match self.alignment {
|
||||
let skip_width = match alignment {
|
||||
Some(Alignment::Center) => (line_width.saturating_sub(area_width)) / 2,
|
||||
Some(Alignment::Right) => line_width.saturating_sub(area_width),
|
||||
Some(Alignment::Left) | None => 0,
|
||||
@@ -630,7 +747,7 @@ fn render_spans(spans: &[Span], mut area: Rect, buf: &mut Buffer, span_skip_widt
|
||||
if area.is_empty() {
|
||||
break;
|
||||
}
|
||||
span.render_ref(area, buf);
|
||||
span.render(area, buf);
|
||||
let span_width = u16::try_from(span_width).unwrap_or(u16::MAX);
|
||||
area = area.indent_x(span_width);
|
||||
}
|
||||
@@ -732,6 +849,7 @@ mod tests {
|
||||
use rstest::{fixture, rstest};
|
||||
|
||||
use super::*;
|
||||
use crate::style::{Color, Modifier, Stylize};
|
||||
|
||||
#[fixture]
|
||||
fn small_buf() -> Buffer {
|
||||
@@ -741,11 +859,11 @@ mod tests {
|
||||
#[test]
|
||||
fn raw_str() {
|
||||
let line = Line::raw("test content");
|
||||
assert_eq!(line.spans, vec![Span::raw("test content")]);
|
||||
assert_eq!(line.spans, [Span::raw("test content")]);
|
||||
assert_eq!(line.alignment, None);
|
||||
|
||||
let line = Line::raw("a\nb");
|
||||
assert_eq!(line.spans, vec![Span::raw("a"), Span::raw("b")]);
|
||||
assert_eq!(line.spans, [Span::raw("a"), Span::raw("b")]);
|
||||
assert_eq!(line.alignment, None);
|
||||
}
|
||||
|
||||
@@ -754,7 +872,7 @@ mod tests {
|
||||
let style = Style::new().yellow();
|
||||
let content = "Hello, world!";
|
||||
let line = Line::styled(content, style);
|
||||
assert_eq!(line.spans, vec![Span::raw(content)]);
|
||||
assert_eq!(line.spans, [Span::raw(content)]);
|
||||
assert_eq!(line.style, style);
|
||||
}
|
||||
|
||||
@@ -763,7 +881,7 @@ mod tests {
|
||||
let style = Style::new().yellow();
|
||||
let content = String::from("Hello, world!");
|
||||
let line = Line::styled(content.clone(), style);
|
||||
assert_eq!(line.spans, vec![Span::raw(content)]);
|
||||
assert_eq!(line.spans, [Span::raw(content)]);
|
||||
assert_eq!(line.style, style);
|
||||
}
|
||||
|
||||
@@ -772,7 +890,7 @@ mod tests {
|
||||
let style = Style::new().yellow();
|
||||
let content = Cow::from("Hello, world!");
|
||||
let line = Line::styled(content.clone(), style);
|
||||
assert_eq!(line.spans, vec![Span::raw(content)]);
|
||||
assert_eq!(line.spans, [Span::raw(content)]);
|
||||
assert_eq!(line.style, style);
|
||||
}
|
||||
|
||||
@@ -861,28 +979,28 @@ mod tests {
|
||||
fn from_string() {
|
||||
let s = String::from("Hello, world!");
|
||||
let line = Line::from(s);
|
||||
assert_eq!(line.spans, vec![Span::from("Hello, world!")]);
|
||||
assert_eq!(line.spans, [Span::from("Hello, world!")]);
|
||||
|
||||
let s = String::from("Hello\nworld!");
|
||||
let line = Line::from(s);
|
||||
assert_eq!(line.spans, vec![Span::from("Hello"), Span::from("world!")]);
|
||||
assert_eq!(line.spans, [Span::from("Hello"), Span::from("world!")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_str() {
|
||||
let s = "Hello, world!";
|
||||
let line = Line::from(s);
|
||||
assert_eq!(line.spans, vec![Span::from("Hello, world!")]);
|
||||
assert_eq!(line.spans, [Span::from("Hello, world!")]);
|
||||
|
||||
let s = "Hello\nworld!";
|
||||
let line = Line::from(s);
|
||||
assert_eq!(line.spans, vec![Span::from("Hello"), Span::from("world!")]);
|
||||
assert_eq!(line.spans, [Span::from("Hello"), Span::from("world!")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_line() {
|
||||
let line = 42.to_line();
|
||||
assert_eq!(vec![Span::from("42")], line.spans);
|
||||
assert_eq!(line.spans, [Span::from("42")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -925,7 +1043,7 @@ mod tests {
|
||||
fn from_span() {
|
||||
let span = Span::styled("Hello, world!", Style::default().fg(Color::Yellow));
|
||||
let line = Line::from(span.clone());
|
||||
assert_eq!(line.spans, vec![span],);
|
||||
assert_eq!(line.spans, [span]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -969,14 +1087,14 @@ mod tests {
|
||||
#[test]
|
||||
fn extend() {
|
||||
let mut line = Line::from("Hello, ");
|
||||
line.extend(vec![Span::raw("world!")]);
|
||||
assert_eq!(line.spans, vec![Span::raw("Hello, "), Span::raw("world!")]);
|
||||
line.extend([Span::raw("world!")]);
|
||||
assert_eq!(line.spans, [Span::raw("Hello, "), Span::raw("world!")]);
|
||||
|
||||
let mut line = Line::from("Hello, ");
|
||||
line.extend(vec![Span::raw("world! "), Span::raw("How are you?")]);
|
||||
line.extend([Span::raw("world! "), Span::raw("How are you?")]);
|
||||
assert_eq!(
|
||||
line.spans,
|
||||
vec![
|
||||
[
|
||||
Span::raw("Hello, "),
|
||||
Span::raw("world! "),
|
||||
Span::raw("How are you?")
|
||||
@@ -1193,7 +1311,7 @@ mod tests {
|
||||
assert_eq!(buf, Buffer::with_lines(["lo wo"]));
|
||||
}
|
||||
|
||||
/// Part of a regression test for <https://github.com/ratatui-org/ratatui/issues/1032> which
|
||||
/// Part of a regression test for <https://github.com/ratatui/ratatui/issues/1032> which
|
||||
/// found panics with truncating lines that contained multi-byte characters.
|
||||
#[test]
|
||||
fn regression_1032() {
|
||||
@@ -1201,7 +1319,7 @@ mod tests {
|
||||
"🦀 RFC8628 OAuth 2.0 Device Authorization GrantでCLIからGithubのaccess tokenを取得する"
|
||||
);
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 83, 1));
|
||||
line.render_ref(buf.area, &mut buf);
|
||||
line.render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines([
|
||||
"🦀 RFC8628 OAuth 2.0 Device Authorization GrantでCLIからGithubのaccess tokenを取得 "
|
||||
]));
|
||||
@@ -1209,7 +1327,7 @@ mod tests {
|
||||
|
||||
/// Documentary test to highlight the crab emoji width / length discrepancy
|
||||
///
|
||||
/// Part of a regression test for <https://github.com/ratatui-org/ratatui/issues/1032> which
|
||||
/// Part of a regression test for <https://github.com/ratatui/ratatui/issues/1032> which
|
||||
/// found panics with truncating lines that contained multi-byte characters.
|
||||
#[test]
|
||||
fn crab_emoji_width() {
|
||||
@@ -1220,7 +1338,7 @@ mod tests {
|
||||
assert_eq!(crab.width(), 2); // display width
|
||||
}
|
||||
|
||||
/// Part of a regression test for <https://github.com/ratatui-org/ratatui/issues/1032> which
|
||||
/// Part of a regression test for <https://github.com/ratatui/ratatui/issues/1032> which
|
||||
/// found panics with truncating lines that contained multi-byte characters.
|
||||
#[rstest]
|
||||
#[case::left_4(Alignment::Left, 4, "1234")]
|
||||
@@ -1238,11 +1356,11 @@ mod tests {
|
||||
) {
|
||||
let line = Line::from("1234🦀7890").alignment(alignment);
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, buf_width, 1));
|
||||
line.render_ref(buf.area, &mut buf);
|
||||
line.render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines([expected]));
|
||||
}
|
||||
|
||||
/// Part of a regression test for <https://github.com/ratatui-org/ratatui/issues/1032> which
|
||||
/// Part of a regression test for <https://github.com/ratatui/ratatui/issues/1032> which
|
||||
/// found panics with truncating lines that contained multi-byte characters.
|
||||
///
|
||||
/// centering is tricky because there's an ambiguity about whether to take one more char
|
||||
@@ -1291,7 +1409,7 @@ mod tests {
|
||||
};
|
||||
let line = Line::from(value).centered();
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, buf_width, 1));
|
||||
line.render_ref(buf.area, &mut buf);
|
||||
line.render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines([expected]));
|
||||
}
|
||||
|
||||
@@ -1309,7 +1427,7 @@ mod tests {
|
||||
// Fill buffer with stuff to ensure the output is indeed padded
|
||||
let mut buf = Buffer::filled(Rect::new(0, 0, 10, 1), Cell::new("X"));
|
||||
let area = Rect::new(2, 0, 6, 1);
|
||||
line.render_ref(area, &mut buf);
|
||||
line.render(area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines([expected]));
|
||||
}
|
||||
|
||||
@@ -1327,11 +1445,11 @@ mod tests {
|
||||
let area = Rect::new(0, 0, buf_width, 1);
|
||||
// Fill buffer with stuff to ensure the output is indeed padded
|
||||
let mut buf = Buffer::filled(area, Cell::new("X"));
|
||||
line.render_ref(buf.area, &mut buf);
|
||||
line.render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines([expected]));
|
||||
}
|
||||
|
||||
/// Part of a regression test for <https://github.com/ratatui-org/ratatui/issues/1032> which
|
||||
/// Part of a regression test for <https://github.com/ratatui/ratatui/issues/1032> which
|
||||
/// found panics with truncating lines that contained multi-byte characters.
|
||||
///
|
||||
/// Flag emoji are actually two independent characters, so they can be truncated in the
|
||||
@@ -1345,7 +1463,7 @@ mod tests {
|
||||
assert_eq!(str.width(), 6); // flag is 2 display width
|
||||
}
|
||||
|
||||
/// Part of a regression test for <https://github.com/ratatui-org/ratatui/issues/1032> which
|
||||
/// Part of a regression test for <https://github.com/ratatui/ratatui/issues/1032> which
|
||||
/// found panics with truncating lines that contained multi-byte characters.
|
||||
#[rstest]
|
||||
#[case::flag_1(1, " ")]
|
||||
@@ -1358,7 +1476,7 @@ mod tests {
|
||||
fn render_truncates_flag(#[case] buf_width: u16, #[case] expected: &str) {
|
||||
let line = Line::from("🇺🇸1234");
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, buf_width, 1));
|
||||
line.render_ref(buf.area, &mut buf);
|
||||
line.render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines([expected]));
|
||||
}
|
||||
|
||||
@@ -1382,7 +1500,7 @@ mod tests {
|
||||
assert!(line.width() >= min_width);
|
||||
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 32, 1));
|
||||
line.render_ref(buf.area, &mut buf);
|
||||
line.render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines([expected]));
|
||||
}
|
||||
|
||||
@@ -1406,7 +1524,7 @@ mod tests {
|
||||
assert!(line.width() >= min_width);
|
||||
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 32, 1));
|
||||
line.render_ref(buf.area, &mut buf);
|
||||
line.render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines([expected]));
|
||||
}
|
||||
|
||||
@@ -1505,4 +1623,49 @@ mod tests {
|
||||
assert_eq!(result, "Hello world!");
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::empty(Line::default(), "Line::default()")]
|
||||
#[case::raw(Line::raw("Hello, world!"), r#"Line::from("Hello, world!")"#)]
|
||||
#[case::styled(
|
||||
Line::styled("Hello, world!", Color::Yellow),
|
||||
r#"Line::from("Hello, world!").yellow()"#
|
||||
)]
|
||||
#[case::styled_complex(
|
||||
Line::from(String::from("Hello, world!")).green().on_blue().bold().italic().not_dim(),
|
||||
r#"Line::from("Hello, world!").green().on_blue().bold().italic().not_dim()"#
|
||||
)]
|
||||
#[case::styled_span(
|
||||
Line::from(Span::styled("Hello, world!", Color::Yellow)),
|
||||
r#"Line::from(Span::from("Hello, world!").yellow())"#
|
||||
)]
|
||||
#[case::styled_line_and_span(
|
||||
Line::from(vec![
|
||||
Span::styled("Hello", Color::Yellow),
|
||||
Span::styled(" world!", Color::Green),
|
||||
]).italic(),
|
||||
r#"Line::from_iter([Span::from("Hello").yellow(), Span::from(" world!").green()]).italic()"#
|
||||
)]
|
||||
#[case::spans_vec(
|
||||
Line::from(vec![
|
||||
Span::styled("Hello", Color::Blue),
|
||||
Span::styled(" world!", Color::Green),
|
||||
]),
|
||||
r#"Line::from_iter([Span::from("Hello").blue(), Span::from(" world!").green()])"#,
|
||||
)]
|
||||
#[case::left_aligned(
|
||||
Line::from("Hello, world!").left_aligned(),
|
||||
r#"Line::from("Hello, world!").left_aligned()"#
|
||||
)]
|
||||
#[case::centered(
|
||||
Line::from("Hello, world!").centered(),
|
||||
r#"Line::from("Hello, world!").centered()"#
|
||||
)]
|
||||
#[case::right_aligned(
|
||||
Line::from("Hello, world!").right_aligned(),
|
||||
r#"Line::from("Hello, world!").right_aligned()"#
|
||||
)]
|
||||
fn debug(#[case] line: Line, #[case] expected: &str) {
|
||||
assert_eq!(format!("{line:?}"), expected);
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,26 @@
|
||||
use std::{borrow::Cow, fmt};
|
||||
|
||||
use super::Text;
|
||||
use crate::text::Text;
|
||||
|
||||
/// A wrapper around a string that is masked when displayed.
|
||||
///
|
||||
/// The masked string is displayed as a series of the same character.
|
||||
/// This might be used to display a password field or similar secure data.
|
||||
/// The masked string is displayed as a series of the same character. This might be used to display
|
||||
/// a password field or similar secure data.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui_core::{
|
||||
/// buffer::Buffer,
|
||||
/// layout::Rect,
|
||||
/// text::{Masked, Text},
|
||||
/// widgets::Widget,
|
||||
/// };
|
||||
///
|
||||
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
|
||||
/// let password = Masked::new("12345", 'x');
|
||||
///
|
||||
/// Paragraph::new(password).render(buffer.area, &mut buffer);
|
||||
/// Text::from(password).render(buffer.area, &mut buffer);
|
||||
/// assert_eq!(buffer, Buffer::with_lines(["xxxxx"]));
|
||||
/// ```
|
||||
#[derive(Default, Clone, Eq, PartialEq, Hash)]
|
||||
@@ -125,10 +130,10 @@ mod tests {
|
||||
let masked = Masked::new("12345", 'x');
|
||||
|
||||
let text: Text = (&masked).into();
|
||||
assert_eq!(text.lines, vec![Line::from("xxxxx")]);
|
||||
assert_eq!(text.lines, [Line::from("xxxxx")]);
|
||||
|
||||
let text: Text = masked.into();
|
||||
assert_eq!(text.lines, vec![Line::from("xxxxx")]);
|
||||
assert_eq!(text.lines, [Line::from("xxxxx")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -3,7 +3,13 @@ use std::{borrow::Cow, fmt};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{prelude::*, style::Styled, text::StyledGrapheme};
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::{Style, Styled},
|
||||
text::{Line, StyledGrapheme},
|
||||
widgets::Widget,
|
||||
};
|
||||
|
||||
/// Represents a part of a line that is contiguous and where all characters share the same style.
|
||||
///
|
||||
@@ -36,7 +42,7 @@ use crate::{prelude::*, style::Styled, text::StyledGrapheme};
|
||||
/// any type convertible to [`Cow<str>`].
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::prelude::*;
|
||||
/// use ratatui_core::text::Span;
|
||||
///
|
||||
/// let span = Span::raw("test content");
|
||||
/// let span = Span::raw(String::from("test content"));
|
||||
@@ -50,7 +56,10 @@ use crate::{prelude::*, style::Styled, text::StyledGrapheme};
|
||||
/// the [`Stylize`] trait.
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::prelude::*;
|
||||
/// use ratatui_core::{
|
||||
/// style::{Style, Stylize},
|
||||
/// text::Span,
|
||||
/// };
|
||||
///
|
||||
/// let span = Span::styled("test content", Style::new().green());
|
||||
/// let span = Span::styled(String::from("test content"), Style::new().green());
|
||||
@@ -64,7 +73,7 @@ use crate::{prelude::*, style::Styled, text::StyledGrapheme};
|
||||
/// defined in the [`Stylize`] trait.
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::prelude::*;
|
||||
/// use ratatui_core::{style::Stylize, text::Span};
|
||||
///
|
||||
/// let span = Span::raw("test content").green().on_yellow().italic();
|
||||
/// let span = Span::raw(String::from("test content"))
|
||||
@@ -73,27 +82,40 @@ use crate::{prelude::*, style::Styled, text::StyledGrapheme};
|
||||
/// .italic();
|
||||
/// ```
|
||||
///
|
||||
/// `Span` implements the [`Widget`] trait, which allows it to be rendered to a [`Buffer`]. Usually
|
||||
/// apps will use the [`Paragraph`] widget instead of rendering `Span` directly, as it handles text
|
||||
/// `Span` implements the [`Widget`] trait, which allows it to be rendered to a [`Buffer`]. Often
|
||||
/// apps will use the `Paragraph` widget instead of rendering `Span` directly, as it handles text
|
||||
/// wrapping and alignment for you.
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::prelude::*;
|
||||
/// ```rust,ignore
|
||||
/// use ratatui::{style::Stylize, Frame};
|
||||
///
|
||||
/// # fn render_frame(frame: &mut Frame) {
|
||||
/// frame.render_widget("test content".green().on_yellow().italic(), frame.size());
|
||||
/// frame.render_widget("test content".green().on_yellow().italic(), frame.area());
|
||||
/// # }
|
||||
/// ```
|
||||
/// [`Line`]: crate::text::Line
|
||||
/// [`Paragraph`]: crate::widgets::Paragraph
|
||||
/// [`Stylize`]: crate::style::Stylize
|
||||
/// [`Cow<str>`]: std::borrow::Cow
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
#[derive(Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Span<'a> {
|
||||
/// The content of the span as a Clone-on-write string.
|
||||
pub content: Cow<'a, str>,
|
||||
/// The style of the span.
|
||||
pub style: Style,
|
||||
/// The content of the span as a Clone-on-write string.
|
||||
pub content: Cow<'a, str>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for Span<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if self.content.is_empty() {
|
||||
write!(f, "Span::default()")?;
|
||||
} else {
|
||||
write!(f, "Span::from({:?})", self.content)?;
|
||||
}
|
||||
if self.style != Style::default() {
|
||||
self.style.fmt_stylize(f)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Span<'a> {
|
||||
@@ -102,7 +124,8 @@ impl<'a> Span<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::text::Span;
|
||||
///
|
||||
/// Span::raw("test content");
|
||||
/// Span::raw(String::from("test content"));
|
||||
/// ```
|
||||
@@ -127,11 +150,17 @@ impl<'a> Span<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::{
|
||||
/// style::{Style, Stylize},
|
||||
/// text::Span,
|
||||
/// };
|
||||
///
|
||||
/// let style = Style::new().yellow().on_green().italic();
|
||||
/// Span::styled("test content", style);
|
||||
/// Span::styled(String::from("test content"), style);
|
||||
/// ```
|
||||
///
|
||||
/// [`Color`]: crate::style::Color
|
||||
pub fn styled<T, S>(content: T, style: S) -> Self
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
@@ -153,7 +182,8 @@ impl<'a> Span<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::text::Span;
|
||||
///
|
||||
/// let mut span = Span::default().content("content");
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
@@ -178,9 +208,15 @@ impl<'a> Span<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::{
|
||||
/// style::{Style, Stylize},
|
||||
/// text::Span,
|
||||
/// };
|
||||
///
|
||||
/// let mut span = Span::default().style(Style::new().green());
|
||||
/// ```
|
||||
///
|
||||
/// [`Color`]: crate::style::Color
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.style = style.into();
|
||||
@@ -197,11 +233,17 @@ impl<'a> Span<'a> {
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::{
|
||||
/// style::{Style, Stylize},
|
||||
/// text::Span,
|
||||
/// };
|
||||
///
|
||||
/// let span = Span::styled("test content", Style::new().green().italic())
|
||||
/// .patch_style(Style::new().red().on_yellow().bold());
|
||||
/// assert_eq!(span.style, Style::new().red().on_yellow().italic().bold());
|
||||
/// ```
|
||||
///
|
||||
/// [`Color`]: crate::style::Color
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn patch_style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.style = self.style.patch(style);
|
||||
@@ -217,7 +259,11 @@ impl<'a> Span<'a> {
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::{
|
||||
/// style::{Style, Stylize},
|
||||
/// text::Span,
|
||||
/// };
|
||||
///
|
||||
/// let span = Span::styled(
|
||||
/// "Test Content",
|
||||
/// Style::new().dark_gray().on_yellow().italic(),
|
||||
@@ -248,7 +294,10 @@ impl<'a> Span<'a> {
|
||||
/// ```rust
|
||||
/// use std::iter::Iterator;
|
||||
///
|
||||
/// use ratatui::{prelude::*, text::StyledGrapheme};
|
||||
/// use ratatui_core::{
|
||||
/// style::{Style, Stylize},
|
||||
/// text::{Span, StyledGrapheme},
|
||||
/// };
|
||||
///
|
||||
/// let span = Span::styled("Test", Style::new().green().italic());
|
||||
/// let style = Style::new().red().on_yellow();
|
||||
@@ -263,6 +312,8 @@ impl<'a> Span<'a> {
|
||||
/// ],
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// [`Color`]: crate::style::Color
|
||||
pub fn styled_graphemes<S: Into<Style>>(
|
||||
&'a self,
|
||||
base_style: S,
|
||||
@@ -280,7 +331,8 @@ impl<'a> Span<'a> {
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::style::Stylize;
|
||||
///
|
||||
/// let line = "Test Content".green().italic().into_left_aligned_line();
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
@@ -299,7 +351,8 @@ impl<'a> Span<'a> {
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::style::Stylize;
|
||||
///
|
||||
/// let line = "Test Content".green().italic().into_centered_line();
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
@@ -318,7 +371,8 @@ impl<'a> Span<'a> {
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::style::Stylize;
|
||||
///
|
||||
/// let line = "Test Content".green().italic().into_right_aligned_line();
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
@@ -364,12 +418,12 @@ impl<'a> Styled for Span<'a> {
|
||||
|
||||
impl Widget for Span<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.render_ref(area, buf);
|
||||
Widget::render(&self, area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetRef for Span<'_> {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
impl Widget for &Span<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let area = area.intersection(buf.area);
|
||||
if area.is_empty() {
|
||||
return;
|
||||
@@ -452,10 +506,10 @@ impl fmt::Display for Span<'_> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use buffer::Cell;
|
||||
use rstest::fixture;
|
||||
use rstest::{fixture, rstest};
|
||||
|
||||
use super::*;
|
||||
use crate::{buffer::Cell, layout::Alignment, style::Stylize};
|
||||
|
||||
#[fixture]
|
||||
fn small_buf() -> Buffer {
|
||||
@@ -569,7 +623,7 @@ mod tests {
|
||||
assert_eq!(Span::raw("").width(), 0);
|
||||
assert_eq!(Span::raw("test").width(), 4);
|
||||
assert_eq!(Span::raw("test content").width(), 12);
|
||||
// Needs reconsideration: https://github.com/ratatui-org/ratatui/issues/1271
|
||||
// Needs reconsideration: https://github.com/ratatui/ratatui/issues/1271
|
||||
assert_eq!(Span::raw("test\ncontent").width(), 12);
|
||||
}
|
||||
|
||||
@@ -786,7 +840,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// Regression test for <https://github.com/ratatui-org/ratatui/issues/1160> One line contains
|
||||
/// Regression test for <https://github.com/ratatui/ratatui/issues/1160> One line contains
|
||||
/// some Unicode Left-Right-Marks (U+200E)
|
||||
///
|
||||
/// The issue was that a zero-width character at the end of the buffer causes the buffer bounds
|
||||
@@ -831,4 +885,16 @@ mod tests {
|
||||
Line::from(vec![Span::raw("test"), Span::raw("content")])
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::default(Span::default(), "Span::default()")]
|
||||
#[case::raw(Span::raw("test"), r#"Span::from("test")"#)]
|
||||
#[case::styled(Span::styled("test", Style::new().green()), r#"Span::from("test").green()"#)]
|
||||
#[case::styled_italic(
|
||||
Span::styled("test", Style::new().green().italic()),
|
||||
r#"Span::from("test").green().italic()"#
|
||||
)]
|
||||
fn debug(#[case] span: Span, #[case] expected: &str) {
|
||||
assert_eq!(format!("{span:?}"), expected);
|
||||
}
|
||||
}
|
||||
290
src/text/text.rs → ratatui-core/src/text/text.rs
Normal file → Executable file
290
src/text/text.rs → ratatui-core/src/text/text.rs
Normal file → Executable file
@@ -1,9 +1,13 @@
|
||||
#![warn(missing_docs)]
|
||||
use std::{borrow::Cow, fmt};
|
||||
|
||||
use itertools::{Itertools, Position};
|
||||
|
||||
use crate::{prelude::*, style::Styled};
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Rect},
|
||||
style::{Style, Styled},
|
||||
text::{Line, Span},
|
||||
widgets::Widget,
|
||||
};
|
||||
|
||||
/// A string split over one or more lines.
|
||||
///
|
||||
@@ -64,7 +68,10 @@ use crate::{prelude::*, style::Styled};
|
||||
/// ```rust
|
||||
/// use std::{borrow::Cow, iter};
|
||||
///
|
||||
/// use ratatui::prelude::*;
|
||||
/// use ratatui_core::{
|
||||
/// style::{Color, Modifier, Style, Stylize},
|
||||
/// text::{Line, Span, Text},
|
||||
/// };
|
||||
///
|
||||
/// let style = Style::new().yellow().italic();
|
||||
/// let text = Text::raw("The first line\nThe second line").style(style);
|
||||
@@ -101,7 +108,11 @@ use crate::{prelude::*, style::Styled};
|
||||
/// [`Stylize`] trait.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::{
|
||||
/// style::{Color, Modifier, Style, Stylize},
|
||||
/// text::{Line, Text},
|
||||
/// };
|
||||
///
|
||||
/// let text = Text::from("The first line\nThe second line").style(Style::new().yellow().italic());
|
||||
/// let text = Text::from("The first line\nThe second line")
|
||||
/// .yellow()
|
||||
@@ -118,7 +129,11 @@ use crate::{prelude::*, style::Styled};
|
||||
/// Lines composing the text can also be individually aligned with [`Line::alignment`].
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::{
|
||||
/// layout::Alignment,
|
||||
/// text::{Line, Text},
|
||||
/// };
|
||||
///
|
||||
/// let text = Text::from("The first line\nThe second line").alignment(Alignment::Right);
|
||||
/// let text = Text::from("The first line\nThe second line").right_aligned();
|
||||
/// let text = Text::from(vec![
|
||||
@@ -131,17 +146,23 @@ use crate::{prelude::*, style::Styled};
|
||||
///
|
||||
/// ## Rendering Text
|
||||
/// `Text` implements the [`Widget`] trait, which means it can be rendered to a [`Buffer`] or to a
|
||||
/// [`Frame`].
|
||||
/// `Frame`.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// # use ratatui_core::{buffer::Buffer, layout::Rect};
|
||||
/// use ratatui_core::{text::Text, widgets::Widget};
|
||||
///
|
||||
/// // within another widget's `render` method:
|
||||
/// # fn render(area: Rect, buf: &mut Buffer) {
|
||||
/// let text = Text::from("The first line\nThe second line");
|
||||
/// text.render(area, buf);
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// // within a terminal.draw closure:
|
||||
/// Or you can use the `render_widget` method on a `Frame` within a `Terminal::draw` closure.
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// # use ratatui::{Frame, layout::Rect, text::Text};
|
||||
/// # fn draw(frame: &mut Frame, area: Rect) {
|
||||
/// let text = Text::from("The first line\nThe second line");
|
||||
/// frame.render_widget(text, area);
|
||||
@@ -150,11 +171,17 @@ use crate::{prelude::*, style::Styled};
|
||||
///
|
||||
/// ## Rendering Text with a Paragraph Widget
|
||||
///
|
||||
/// Usually apps will use the [`Paragraph`] widget instead of rendering a `Text` directly as it
|
||||
/// Usually apps will use the `Paragraph` widget instead of rendering a `Text` directly as it
|
||||
/// provides more functionality.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// ```rust,ignore
|
||||
/// use ratatui::{
|
||||
/// buffer::Buffer,
|
||||
/// layout::Rect,
|
||||
/// text::Text,
|
||||
/// widgets::{Paragraph, Widget, Wrap},
|
||||
/// };
|
||||
///
|
||||
/// # fn render(area: Rect, buf: &mut Buffer) {
|
||||
/// let text = Text::from("The first line\nThe second line");
|
||||
/// let paragraph = Paragraph::new(text)
|
||||
@@ -165,14 +192,37 @@ use crate::{prelude::*, style::Styled};
|
||||
/// ```
|
||||
///
|
||||
/// [`Paragraph`]: crate::widgets::Paragraph
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
/// [`Stylize`]: crate::style::Stylize
|
||||
#[derive(Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Text<'a> {
|
||||
/// The lines that make up this piece of text.
|
||||
pub lines: Vec<Line<'a>>,
|
||||
/// The style of this text.
|
||||
pub style: Style,
|
||||
/// The alignment of this text.
|
||||
pub alignment: Option<Alignment>,
|
||||
/// The style of this text.
|
||||
pub style: Style,
|
||||
/// The lines that make up this piece of text.
|
||||
pub lines: Vec<Line<'a>>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for Text<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if self.lines.is_empty() {
|
||||
f.write_str("Text::default()")?;
|
||||
} else if self.lines.len() == 1 {
|
||||
write!(f, "Text::from({:?})", self.lines[0])?;
|
||||
} else {
|
||||
f.write_str("Text::from_iter(")?;
|
||||
f.debug_list().entries(self.lines.iter()).finish()?;
|
||||
f.write_str(")")?;
|
||||
}
|
||||
self.style.fmt_stylize(f)?;
|
||||
match self.alignment {
|
||||
Some(Alignment::Left) => f.write_str(".left_aligned()")?,
|
||||
Some(Alignment::Center) => f.write_str(".centered()")?,
|
||||
Some(Alignment::Right) => f.write_str(".right_aligned()")?,
|
||||
_ => (),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Text<'a> {
|
||||
@@ -181,7 +231,8 @@ impl<'a> Text<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::text::Text;
|
||||
///
|
||||
/// Text::raw("The first line\nThe second line");
|
||||
/// Text::raw(String::from("The first line\nThe second line"));
|
||||
/// ```
|
||||
@@ -206,13 +257,19 @@ impl<'a> Text<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::{
|
||||
/// style::{Color, Modifier, Style},
|
||||
/// text::Text,
|
||||
/// };
|
||||
///
|
||||
/// let style = Style::default()
|
||||
/// .fg(Color::Yellow)
|
||||
/// .add_modifier(Modifier::ITALIC);
|
||||
/// Text::styled("The first line\nThe second line", style);
|
||||
/// Text::styled(String::from("The first line\nThe second line"), style);
|
||||
/// ```
|
||||
///
|
||||
/// [`Color`]: crate::style::Color
|
||||
pub fn styled<T, S>(content: T, style: S) -> Self
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
@@ -226,7 +283,8 @@ impl<'a> Text<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::text::Text;
|
||||
///
|
||||
/// let text = Text::from("The first line\nThe second line");
|
||||
/// assert_eq!(15, text.width());
|
||||
/// ```
|
||||
@@ -239,7 +297,8 @@ impl<'a> Text<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::text::Text;
|
||||
///
|
||||
/// let text = Text::from("The first line\nThe second line");
|
||||
/// assert_eq!(2, text.height());
|
||||
/// ```
|
||||
@@ -260,9 +319,15 @@ impl<'a> Text<'a> {
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::{
|
||||
/// style::{Style, Stylize},
|
||||
/// text::Text,
|
||||
/// };
|
||||
///
|
||||
/// let mut line = Text::from("foo").style(Style::new().red());
|
||||
/// ```
|
||||
///
|
||||
/// [`Color`]: crate::style::Color
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.style = style.into();
|
||||
@@ -286,7 +351,11 @@ impl<'a> Text<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::{
|
||||
/// style::{Color, Modifier},
|
||||
/// text::Text,
|
||||
/// };
|
||||
///
|
||||
/// let raw_text = Text::styled("The first line\nThe second line", Modifier::ITALIC);
|
||||
/// let styled_text = Text::styled(
|
||||
/// String::from("The first line\nThe second line"),
|
||||
@@ -297,6 +366,9 @@ impl<'a> Text<'a> {
|
||||
/// let raw_text = raw_text.patch_style(Color::Yellow);
|
||||
/// assert_eq!(raw_text, styled_text);
|
||||
/// ```
|
||||
///
|
||||
/// [`Color`]: crate::style::Color
|
||||
/// [`Stylize`]: crate::style::Stylize
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn patch_style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.style = self.style.patch(style);
|
||||
@@ -312,7 +384,11 @@ impl<'a> Text<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::{
|
||||
/// style::{Color, Modifier, Style},
|
||||
/// text::Text,
|
||||
/// };
|
||||
///
|
||||
/// let text = Text::styled(
|
||||
/// "The first line\nThe second line",
|
||||
/// (Color::Yellow, Modifier::ITALIC),
|
||||
@@ -339,7 +415,8 @@ impl<'a> Text<'a> {
|
||||
/// Set alignment to the whole text.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::{layout::Alignment, text::Text};
|
||||
///
|
||||
/// let mut text = Text::from("Hi, what's up?");
|
||||
/// assert_eq!(None, text.alignment);
|
||||
/// assert_eq!(
|
||||
@@ -351,7 +428,11 @@ impl<'a> Text<'a> {
|
||||
/// Set a default alignment and override it on a per line basis.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::{
|
||||
/// layout::Alignment,
|
||||
/// text::{Line, Text},
|
||||
/// };
|
||||
///
|
||||
/// let text = Text::from(vec![
|
||||
/// Line::from("left").alignment(Alignment::Left),
|
||||
/// Line::from("default"),
|
||||
@@ -388,7 +469,8 @@ impl<'a> Text<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::text::Text;
|
||||
///
|
||||
/// let text = Text::from("Hi, what's up?").left_aligned();
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
@@ -407,7 +489,8 @@ impl<'a> Text<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::text::Text;
|
||||
///
|
||||
/// let text = Text::from("Hi, what's up?").centered();
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
@@ -426,7 +509,8 @@ impl<'a> Text<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::text::Text;
|
||||
///
|
||||
/// let text = Text::from("Hi, what's up?").right_aligned();
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
@@ -452,7 +536,8 @@ impl<'a> Text<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::text::{Line, Span, Text};
|
||||
///
|
||||
/// let mut text = Text::from("Hello, world!");
|
||||
/// text.push_line(Line::from("How are you?"));
|
||||
/// text.push_line(Span::from("How are you?"));
|
||||
@@ -470,7 +555,8 @@ impl<'a> Text<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::text::{Span, Text};
|
||||
///
|
||||
/// let mut text = Text::from("Hello, world!");
|
||||
/// text.push_span(Span::from("How are you?"));
|
||||
/// text.push_span("How are you?");
|
||||
@@ -632,12 +718,11 @@ impl<T: fmt::Display> ToText for T {
|
||||
|
||||
impl fmt::Display for Text<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
for (position, line) in self.iter().with_position() {
|
||||
if position == Position::Last {
|
||||
write!(f, "{line}")?;
|
||||
} else {
|
||||
if let Some((last, rest)) = self.lines.split_last() {
|
||||
for line in rest {
|
||||
writeln!(f, "{line}")?;
|
||||
}
|
||||
write!(f, "{last}")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -645,31 +730,16 @@ impl fmt::Display for Text<'_> {
|
||||
|
||||
impl Widget for Text<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.render_ref(area, buf);
|
||||
Widget::render(&self, area, buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetRef for Text<'_> {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
impl Widget for &Text<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let area = area.intersection(buf.area);
|
||||
buf.set_style(area, self.style);
|
||||
for (line, row) in self.iter().zip(area.rows()) {
|
||||
let line_width = line.width() as u16;
|
||||
|
||||
let x_offset = match (self.alignment, line.alignment) {
|
||||
(Some(Alignment::Center), None) => area.width.saturating_sub(line_width) / 2,
|
||||
(Some(Alignment::Right), None) => area.width.saturating_sub(line_width),
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
let line_area = Rect {
|
||||
x: area.x + x_offset,
|
||||
y: row.y,
|
||||
width: area.width - x_offset,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
line.render(line_area, buf);
|
||||
for (line, line_area) in self.iter().zip(area.rows()) {
|
||||
line.render_with_alignment(line_area, buf, self.alignment);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -693,6 +763,7 @@ mod tests {
|
||||
use rstest::{fixture, rstest};
|
||||
|
||||
use super::*;
|
||||
use crate::style::{Color, Modifier, Stylize};
|
||||
|
||||
#[fixture]
|
||||
fn small_buf() -> Buffer {
|
||||
@@ -794,7 +865,7 @@ mod tests {
|
||||
#[test]
|
||||
fn from_line() {
|
||||
let text = Text::from(Line::from("The first line"));
|
||||
assert_eq!(text.lines, vec![Line::from("The first line")]);
|
||||
assert_eq!(text.lines, [Line::from("The first line")]);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
@@ -945,11 +1016,12 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_raw_text() {
|
||||
let text = Text::raw("The first line\nThe second line");
|
||||
|
||||
assert_eq!(format!("{text}"), "The first line\nThe second line");
|
||||
#[rstest]
|
||||
#[case::one_line("The first line")]
|
||||
#[case::multiple_lines("The first line\nThe second line")]
|
||||
fn display_raw_text(#[case] value: &str) {
|
||||
let text = Text::raw(value);
|
||||
assert_eq!(format!("{text}"), value);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1041,7 +1113,7 @@ mod tests {
|
||||
fn push_line_empty() {
|
||||
let mut text = Text::default();
|
||||
text.push_line(Line::from("Hello, world!"));
|
||||
assert_eq!(text.lines, vec![Line::from("Hello, world!")]);
|
||||
assert_eq!(text.lines, [Line::from("Hello, world!")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1063,7 +1135,7 @@ mod tests {
|
||||
fn push_span_empty() {
|
||||
let mut text = Text::default();
|
||||
text.push_span(Span::raw("Hello, world!"));
|
||||
assert_eq!(text.lines, vec![Line::from(Span::raw("Hello, world!"))],);
|
||||
assert_eq!(text.lines, [Line::from(Span::raw("Hello, world!"))]);
|
||||
}
|
||||
|
||||
mod widget {
|
||||
@@ -1112,6 +1184,33 @@ mod tests {
|
||||
assert_eq!(buf, Buffer::with_lines([" foo "]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_right_aligned_with_truncation() {
|
||||
let text = Text::from("123456789").alignment(Alignment::Right);
|
||||
let area = Rect::new(0, 0, 5, 1);
|
||||
let mut buf = Buffer::empty(area);
|
||||
text.render(area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["56789"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_centered_odd_with_truncation() {
|
||||
let text = Text::from("123456789").alignment(Alignment::Center);
|
||||
let area = Rect::new(0, 0, 5, 1);
|
||||
let mut buf = Buffer::empty(area);
|
||||
text.render(area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["34567"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_centered_even_with_truncation() {
|
||||
let text = Text::from("123456789").alignment(Alignment::Center);
|
||||
let area = Rect::new(0, 0, 6, 1);
|
||||
let mut buf = Buffer::empty(area);
|
||||
text.render(area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["234567"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_one_line_right() {
|
||||
let text = Text::from(vec![
|
||||
@@ -1234,4 +1333,69 @@ mod tests {
|
||||
assert_eq!(result, "Hello world!");
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::default(Text::default(), "Text::default()")]
|
||||
// TODO jm: these could be improved to inspect the line / span if there's only one. e.g.
|
||||
// Text::from("Hello, world!") and Text::from("Hello, world!".blue()) but the current
|
||||
// implementation is good enough for now.
|
||||
#[case::raw(
|
||||
Text::raw("Hello, world!"),
|
||||
r#"Text::from(Line::from("Hello, world!"))"#
|
||||
)]
|
||||
#[case::styled(
|
||||
Text::styled("Hello, world!", Color::Yellow),
|
||||
r#"Text::from(Line::from("Hello, world!")).yellow()"#
|
||||
)]
|
||||
#[case::complex_styled(
|
||||
Text::from("Hello, world!").yellow().on_blue().bold().italic().not_dim().not_hidden(),
|
||||
r#"Text::from(Line::from("Hello, world!")).yellow().on_blue().bold().italic().not_dim().not_hidden()"#
|
||||
)]
|
||||
#[case::alignment(
|
||||
Text::from("Hello, world!").centered(),
|
||||
r#"Text::from(Line::from("Hello, world!")).centered()"#
|
||||
)]
|
||||
#[case::styled_alignment(
|
||||
Text::styled("Hello, world!", Color::Yellow).centered(),
|
||||
r#"Text::from(Line::from("Hello, world!")).yellow().centered()"#
|
||||
)]
|
||||
#[case::multiple_lines(
|
||||
Text::from(vec![
|
||||
Line::from("Hello, world!"),
|
||||
Line::from("How are you?")
|
||||
]),
|
||||
r#"Text::from_iter([Line::from("Hello, world!"), Line::from("How are you?")])"#
|
||||
)]
|
||||
fn debug(#[case] text: Text, #[case] expected: &str) {
|
||||
assert_eq!(format!("{text:?}"), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn debug_alternate() {
|
||||
let text = Text::from_iter([
|
||||
Line::from("Hello, world!"),
|
||||
Line::from("How are you?").bold().left_aligned(),
|
||||
Line::from_iter([
|
||||
Span::from("I'm "),
|
||||
Span::from("doing ").italic(),
|
||||
Span::from("great!").bold(),
|
||||
]),
|
||||
])
|
||||
.on_blue()
|
||||
.italic()
|
||||
.centered();
|
||||
assert_eq!(
|
||||
format!("{text:#?}"),
|
||||
indoc::indoc! {r#"
|
||||
Text::from_iter([
|
||||
Line::from("Hello, world!"),
|
||||
Line::from("How are you?").bold().left_aligned(),
|
||||
Line::from_iter([
|
||||
Span::from("I'm "),
|
||||
Span::from("doing ").italic(),
|
||||
Span::from("great!").bold(),
|
||||
]),
|
||||
]).on_blue().italic().centered()"#}
|
||||
);
|
||||
}
|
||||
}
|
||||
8
ratatui-core/src/widgets.rs
Normal file
8
ratatui-core/src/widgets.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
#![warn(missing_docs)]
|
||||
//! The `widgets` module contains the `Widget` and `StatefulWidget` traits, which are used to
|
||||
//! render UI elements on the screen.
|
||||
|
||||
pub use self::{stateful_widget::StatefulWidget, widget::Widget};
|
||||
|
||||
mod stateful_widget;
|
||||
mod widget;
|
||||
181
ratatui-core/src/widgets/stateful_widget.rs
Normal file
181
ratatui-core/src/widgets/stateful_widget.rs
Normal file
@@ -0,0 +1,181 @@
|
||||
use crate::{buffer::Buffer, layout::Rect};
|
||||
|
||||
/// A `StatefulWidget` is a widget that can take advantage of some local state to remember things
|
||||
/// between two draw calls.
|
||||
///
|
||||
/// Most widgets can be drawn directly based on the input parameters. However, some features may
|
||||
/// require some kind of associated state to be implemented.
|
||||
///
|
||||
/// For example, the `List` widget can highlight the item currently selected. This can be translated
|
||||
/// in an offset, which is the number of elements to skip in order to have the selected item within
|
||||
/// the viewport currently allocated to this widget. The widget can therefore only provide the
|
||||
/// following behavior: whenever the selected item is out of the viewport scroll to a predefined
|
||||
/// position (making the selected item the last viewable item or the one in the middle for example).
|
||||
/// Nonetheless, if the widget has access to the last computed offset then it can implement a
|
||||
/// natural scrolling experience where the last offset is reused until the selected item is out of
|
||||
/// the viewport.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// use std::io;
|
||||
///
|
||||
/// use ratatui::{
|
||||
/// backend::TestBackend,
|
||||
/// widgets::{List, ListItem, ListState, StatefulWidget, Widget},
|
||||
/// Terminal,
|
||||
/// };
|
||||
///
|
||||
/// // Let's say we have some events to display.
|
||||
/// struct Events {
|
||||
/// // `items` is the state managed by your application.
|
||||
/// items: Vec<String>,
|
||||
/// // `state` is the state that can be modified by the UI. It stores the index of the selected
|
||||
/// // item as well as the offset computed during the previous draw call (used to implement
|
||||
/// // natural scrolling).
|
||||
/// state: ListState,
|
||||
/// }
|
||||
///
|
||||
/// impl Events {
|
||||
/// fn new(items: Vec<String>) -> Events {
|
||||
/// Events {
|
||||
/// items,
|
||||
/// state: ListState::default(),
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// pub fn set_items(&mut self, items: Vec<String>) {
|
||||
/// self.items = items;
|
||||
/// // We reset the state as the associated items have changed. This effectively reset
|
||||
/// // the selection as well as the stored offset.
|
||||
/// self.state = ListState::default();
|
||||
/// }
|
||||
///
|
||||
/// // Select the next item. This will not be reflected until the widget is drawn in the
|
||||
/// // `Terminal::draw` callback using `Frame::render_stateful_widget`.
|
||||
/// pub fn next(&mut self) {
|
||||
/// let i = match self.state.selected() {
|
||||
/// Some(i) => {
|
||||
/// if i >= self.items.len() - 1 {
|
||||
/// 0
|
||||
/// } else {
|
||||
/// i + 1
|
||||
/// }
|
||||
/// }
|
||||
/// None => 0,
|
||||
/// };
|
||||
/// self.state.select(Some(i));
|
||||
/// }
|
||||
///
|
||||
/// // Select the previous item. This will not be reflected until the widget is drawn in the
|
||||
/// // `Terminal::draw` callback using `Frame::render_stateful_widget`.
|
||||
/// pub fn previous(&mut self) {
|
||||
/// let i = match self.state.selected() {
|
||||
/// Some(i) => {
|
||||
/// if i == 0 {
|
||||
/// self.items.len() - 1
|
||||
/// } else {
|
||||
/// i - 1
|
||||
/// }
|
||||
/// }
|
||||
/// None => 0,
|
||||
/// };
|
||||
/// self.state.select(Some(i));
|
||||
/// }
|
||||
///
|
||||
/// // Unselect the currently selected item if any. The implementation of `ListState` makes
|
||||
/// // sure that the stored offset is also reset.
|
||||
/// pub fn unselect(&mut self) {
|
||||
/// self.state.select(None);
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// # let backend = TestBackend::new(5, 5);
|
||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||
///
|
||||
/// let mut events = Events::new(vec![String::from("Item 1"), String::from("Item 2")]);
|
||||
///
|
||||
/// loop {
|
||||
/// terminal.draw(|f| {
|
||||
/// // The items managed by the application are transformed to something
|
||||
/// // that is understood by ratatui.
|
||||
/// let items: Vec<ListItem> = events
|
||||
/// .items
|
||||
/// .iter()
|
||||
/// .map(|i| ListItem::new(i.as_str()))
|
||||
/// .collect();
|
||||
/// // The `List` widget is then built with those items.
|
||||
/// let list = List::new(items);
|
||||
/// // Finally the widget is rendered using the associated state. `events.state` is
|
||||
/// // effectively the only thing that we will "remember" from this draw call.
|
||||
/// f.render_stateful_widget(list, f.size(), &mut events.state);
|
||||
/// });
|
||||
///
|
||||
/// // In response to some input events or an external http request or whatever:
|
||||
/// events.next();
|
||||
/// }
|
||||
/// ```
|
||||
pub trait StatefulWidget {
|
||||
/// State associated with the stateful widget.
|
||||
///
|
||||
/// If you don't need this then you probably want to implement [`Widget`] instead.
|
||||
///
|
||||
/// [`Widget`]: super::Widget
|
||||
type State: ?Sized;
|
||||
/// Draws the current state of the widget in the given buffer. That is the only method required
|
||||
/// to implement a custom stateful widget.
|
||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rstest::{fixture, rstest};
|
||||
|
||||
use super::*;
|
||||
use crate::{buffer::Buffer, layout::Rect, text::Line, widgets::Widget};
|
||||
|
||||
#[fixture]
|
||||
fn buf() -> Buffer {
|
||||
Buffer::empty(Rect::new(0, 0, 20, 1))
|
||||
}
|
||||
|
||||
#[fixture]
|
||||
fn state() -> String {
|
||||
"world".to_string()
|
||||
}
|
||||
|
||||
struct PersonalGreeting;
|
||||
|
||||
impl StatefulWidget for PersonalGreeting {
|
||||
type State = String;
|
||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||
Line::from(format!("Hello {state}")).render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn render(mut buf: Buffer, mut state: String) {
|
||||
let widget = PersonalGreeting;
|
||||
widget.render(buf.area, &mut buf, &mut state);
|
||||
assert_eq!(buf, Buffer::with_lines(["Hello world "]));
|
||||
}
|
||||
|
||||
struct Bytes;
|
||||
|
||||
/// A widget with an unsized state type.
|
||||
impl StatefulWidget for Bytes {
|
||||
type State = [u8];
|
||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||
let slice = std::str::from_utf8(state).unwrap();
|
||||
Line::from(format!("Bytes: {slice}")).render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn render_unsized_state_type(mut buf: Buffer) {
|
||||
let widget = Bytes;
|
||||
let state = b"hello";
|
||||
widget.render(buf.area, &mut buf, &mut state.clone());
|
||||
assert_eq!(buf, Buffer::with_lines(["Bytes: hello "]));
|
||||
}
|
||||
}
|
||||
160
ratatui-core/src/widgets/widget.rs
Normal file
160
ratatui-core/src/widgets/widget.rs
Normal file
@@ -0,0 +1,160 @@
|
||||
use crate::{buffer::Buffer, layout::Rect, style::Style};
|
||||
|
||||
/// A `Widget` is a type that can be drawn on a [`Buffer`] in a given [`Rect`].
|
||||
///
|
||||
/// Prior to Ratatui 0.26.0, widgets generally were created for each frame as they were consumed
|
||||
/// during rendering. This meant that they were not meant to be stored but used as *commands* to
|
||||
/// draw common figures in the UI.
|
||||
///
|
||||
/// Starting with Ratatui 0.26.0, all the internal widgets implement Widget for a reference to
|
||||
/// themselves. This allows you to store a reference to a widget and render it later. Widget crates
|
||||
/// should consider also doing this to allow for more flexibility in how widgets are used.
|
||||
///
|
||||
/// In Ratatui 0.26.0, we also added an unstable `WidgetRef` trait and implemented this on all the
|
||||
/// internal widgets. In addition to the above benefit of rendering references to widgets, this also
|
||||
/// allows you to render boxed widgets. This is useful when you want to store a collection of
|
||||
/// widgets with different types. You can then iterate over the collection and render each widget.
|
||||
/// See <https://github.com/ratatui/ratatui/issues/1287> for more information.
|
||||
///
|
||||
/// In general where you expect a widget to immutably work on its data, we recommended to implement
|
||||
/// `Widget` for a reference to the widget (`impl Widget for &MyWidget`). If you need to store state
|
||||
/// between draw calls, implement `StatefulWidget` if you want the Widget to be immutable, or
|
||||
/// implement `Widget` for a mutable reference to the widget (`impl Widget for &mut MyWidget`) if
|
||||
/// you want the widget to be mutable. The mutable widget pattern is used infrequently in apps, but
|
||||
/// can be quite useful.
|
||||
///
|
||||
/// A blanket implementation of `Widget` for `&W` where `W` implements `WidgetRef` is provided.
|
||||
/// Widget is also implemented for `&str` and `String` types.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// use ratatui::{
|
||||
/// backend::TestBackend,
|
||||
/// widgets::{Clear, Widget},
|
||||
/// Terminal,
|
||||
/// };
|
||||
/// # let backend = TestBackend::new(5, 5);
|
||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||
///
|
||||
/// terminal.draw(|frame| {
|
||||
/// frame.render_widget(Clear, frame.area());
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// It's common to render widgets inside other widgets:
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui_core::{buffer::Buffer, layout::Rect, text::Line, widgets::Widget};
|
||||
///
|
||||
/// struct MyWidget;
|
||||
///
|
||||
/// impl Widget for MyWidget {
|
||||
/// fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
/// Line::raw("Hello").render(area, buf);
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub trait Widget {
|
||||
/// Draws the current state of the widget in the given buffer. That is the only method required
|
||||
/// to implement a custom widget.
|
||||
fn render(self, area: Rect, buf: &mut Buffer)
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
||||
|
||||
/// Renders a string slice as a widget.
|
||||
///
|
||||
/// This implementation allows a string slice (`&str`) to act as a widget, meaning it can be drawn
|
||||
/// onto a [`Buffer`] in a specified [`Rect`]. The slice represents a static string which can be
|
||||
/// rendered by reference, thereby avoiding the need for string cloning or ownership transfer when
|
||||
/// drawing the text to the screen.
|
||||
impl Widget for &str {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
buf.set_stringn(area.x, area.y, self, area.width as usize, Style::new());
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders a `String` object as a widget.
|
||||
///
|
||||
/// This implementation enables an owned `String` to be treated as a widget, which can be rendered
|
||||
/// on a [`Buffer`] within the bounds of a given [`Rect`].
|
||||
impl Widget for String {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
buf.set_stringn(area.x, area.y, self, area.width as usize, Style::new());
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Widget> Widget for Option<W> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
if let Some(widget) = self {
|
||||
widget.render(area, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rstest::{fixture, rstest};
|
||||
|
||||
use super::*;
|
||||
use crate::{buffer::Buffer, layout::Rect, text::Line};
|
||||
|
||||
#[fixture]
|
||||
fn buf() -> Buffer {
|
||||
Buffer::empty(Rect::new(0, 0, 20, 1))
|
||||
}
|
||||
|
||||
struct Greeting;
|
||||
|
||||
impl Widget for Greeting {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
Line::from("Hello").render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn render(mut buf: Buffer) {
|
||||
let widget = Greeting;
|
||||
widget.render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["Hello "]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn render_str(mut buf: Buffer) {
|
||||
"hello world".render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["hello world "]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn render_str_truncate(mut buf: Buffer) {
|
||||
let area = Rect::new(buf.area.x, buf.area.y, 11, buf.area.height);
|
||||
"hello world, just hello".render(area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["hello world "]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn render_option_str(mut buf: Buffer) {
|
||||
Some("hello world").render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["hello world "]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn render_string(mut buf: Buffer) {
|
||||
String::from("hello world").render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["hello world "]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn render_string_truncate(mut buf: Buffer) {
|
||||
let area = Rect::new(buf.area.x, buf.area.y, 11, buf.area.height);
|
||||
String::from("hello world, just hello").render(area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["hello world "]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn render_option_string(mut buf: Buffer) {
|
||||
Some(String::from("hello world")).render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["hello world "]));
|
||||
}
|
||||
}
|
||||
45
ratatui-crossterm/Cargo.toml
Normal file
45
ratatui-crossterm/Cargo.toml
Normal file
@@ -0,0 +1,45 @@
|
||||
[package]
|
||||
name = "ratatui-crossterm"
|
||||
version = "0.1.0-alpha.0"
|
||||
description = "Crossterm backend for the Ratatui Terminal UI library."
|
||||
documentation = "https://docs.rs/ratatui-crossterm/"
|
||||
readme = "README.md"
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
keywords.workspace = true
|
||||
categories.workspace = true
|
||||
license.workspace = true
|
||||
exclude.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[features]
|
||||
default = ["underline-color"]
|
||||
|
||||
## enables the backend code that sets the underline color.
|
||||
## Underline color is only supported by the [`CrosstermBackend`](backend::CrosstermBackend) backend,
|
||||
## and is not supported on Windows 7.
|
||||
underline-color = ["ratatui-core/underline-color"]
|
||||
|
||||
## Use terminal scrolling regions to make Terminal::insert_before less prone to flickering.
|
||||
scrolling-regions = ["ratatui-core/scrolling-regions"]
|
||||
|
||||
#! The following features are unstable and may change in the future:
|
||||
|
||||
## Enable all unstable features.
|
||||
unstable = ["unstable-backend-writer"]
|
||||
|
||||
## Enables getting access to backends' writers.
|
||||
unstable-backend-writer = []
|
||||
|
||||
|
||||
[dependencies]
|
||||
crossterm.workspace = true
|
||||
document-features = { workspace = true, optional = true }
|
||||
instability.workspace = true
|
||||
ratatui-core = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
ratatui = { path = "../ratatui", features = ["crossterm"] }
|
||||
rstest.workspace = true
|
||||
10
ratatui-crossterm/README.md
Normal file
10
ratatui-crossterm/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Ratatui-crossterm
|
||||
|
||||
<!-- cargo-rdme start -->
|
||||
|
||||
This module provides the [`CrosstermBackend`] implementation for the [`Backend`] trait. It uses
|
||||
the [Crossterm] crate to interact with the terminal.
|
||||
|
||||
[Crossterm]: https://crates.io/crates/crossterm
|
||||
|
||||
<!-- cargo-rdme end -->
|
||||
812
ratatui-crossterm/src/lib.rs
Normal file
812
ratatui-crossterm/src/lib.rs
Normal file
@@ -0,0 +1,812 @@
|
||||
// show the feature flags in the generated documentation
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![doc(
|
||||
html_logo_url = "https://raw.githubusercontent.com/ratatui/ratatui/main/assets/logo.png",
|
||||
html_favicon_url = "https://raw.githubusercontent.com/ratatui/ratatui/main/assets/favicon.ico"
|
||||
)]
|
||||
#![warn(missing_docs)]
|
||||
//! This module provides the [`CrosstermBackend`] implementation for the [`Backend`] trait. It uses
|
||||
//! the [Crossterm] crate to interact with the terminal.
|
||||
//!
|
||||
//! [Crossterm]: https://crates.io/crates/crossterm
|
||||
#![cfg_attr(feature = "document-features", doc = "\n## Features")]
|
||||
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
|
||||
|
||||
use std::io::{self, Write};
|
||||
|
||||
pub use crossterm;
|
||||
#[cfg(feature = "underline-color")]
|
||||
use crossterm::style::SetUnderlineColor;
|
||||
use crossterm::{
|
||||
cursor::{Hide, MoveTo, Show},
|
||||
execute, queue,
|
||||
style::{
|
||||
Attribute as CrosstermAttribute, Attributes as CrosstermAttributes,
|
||||
Color as CrosstermColor, Colors as CrosstermColors, ContentStyle, Print, SetAttribute,
|
||||
SetBackgroundColor, SetColors, SetForegroundColor,
|
||||
},
|
||||
terminal::{self, Clear},
|
||||
};
|
||||
use ratatui_core::{
|
||||
backend::{Backend, ClearType, WindowSize},
|
||||
buffer::Cell,
|
||||
layout::{Position, Size},
|
||||
style::{Color, Modifier, Style},
|
||||
};
|
||||
|
||||
/// A [`Backend`] implementation that uses [Crossterm] to render to the terminal.
|
||||
///
|
||||
/// The `CrosstermBackend` struct is a wrapper around a writer implementing [`Write`], which is
|
||||
/// used to send commands to the terminal. It provides methods for drawing content, manipulating
|
||||
/// the cursor, and clearing the terminal screen.
|
||||
///
|
||||
/// Most applications should not call the methods on `CrosstermBackend` directly, but will instead
|
||||
/// use the [`Terminal`] struct, which provides a more ergonomic interface.
|
||||
///
|
||||
/// Usually applications will enable raw mode and switch to alternate screen mode after creating
|
||||
/// a `CrosstermBackend`. This is done by calling [`crossterm::terminal::enable_raw_mode`] and
|
||||
/// [`crossterm::terminal::EnterAlternateScreen`] (and the corresponding disable/leave functions
|
||||
/// when the application exits). This is not done automatically by the backend because it is
|
||||
/// possible that the application may want to use the terminal for other purposes (like showing
|
||||
/// help text) before entering alternate screen mode.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use std::io::{stderr, stdout};
|
||||
///
|
||||
/// use crossterm::{
|
||||
/// terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
/// ExecutableCommand,
|
||||
/// };
|
||||
/// use ratatui::{backend::CrosstermBackend, Terminal};
|
||||
///
|
||||
/// let mut backend = CrosstermBackend::new(stdout());
|
||||
/// // or
|
||||
/// let backend = CrosstermBackend::new(stderr());
|
||||
/// let mut terminal = Terminal::new(backend)?;
|
||||
///
|
||||
/// enable_raw_mode()?;
|
||||
/// stdout().execute(EnterAlternateScreen)?;
|
||||
///
|
||||
/// terminal.clear()?;
|
||||
/// terminal.draw(|frame| {
|
||||
/// // -- snip --
|
||||
/// })?;
|
||||
///
|
||||
/// stdout().execute(LeaveAlternateScreen)?;
|
||||
/// disable_raw_mode()?;
|
||||
///
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
///
|
||||
/// See the the [Examples] directory for more examples. See the [`backend`] module documentation
|
||||
/// for more details on raw mode and alternate screen.
|
||||
///
|
||||
/// [`Write`]: std::io::Write
|
||||
/// [`Terminal`]: https://docs.rs/ratatui/latest/ratatui/struct.Terminal.html
|
||||
/// [`backend`]: ratatui_core::backend
|
||||
/// [Crossterm]: https://crates.io/crates/crossterm
|
||||
/// [Examples]: https://github.com/ratatui/ratatui/tree/main/ratatui/examples/README.md
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct CrosstermBackend<W: Write> {
|
||||
/// The writer used to send commands to the terminal.
|
||||
writer: W,
|
||||
}
|
||||
|
||||
impl<W> CrosstermBackend<W>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
/// Creates a new `CrosstermBackend` with the given writer.
|
||||
///
|
||||
/// Most applications will use either [`stdout`](std::io::stdout) or
|
||||
/// [`stderr`](std::io::stderr) as writer. See the [FAQ] to determine which one to use.
|
||||
///
|
||||
/// [FAQ]: https://ratatui.rs/faq/#should-i-use-stdout-or-stderr
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use std::io::stdout;
|
||||
///
|
||||
/// use ratatui::backend::CrosstermBackend;
|
||||
///
|
||||
/// let backend = CrosstermBackend::new(stdout());
|
||||
/// ```
|
||||
pub const fn new(writer: W) -> Self {
|
||||
Self { writer }
|
||||
}
|
||||
|
||||
/// Gets the writer.
|
||||
#[instability::unstable(
|
||||
feature = "backend-writer",
|
||||
issue = "https://github.com/ratatui/ratatui/pull/991"
|
||||
)]
|
||||
pub const fn writer(&self) -> &W {
|
||||
&self.writer
|
||||
}
|
||||
|
||||
/// Gets the writer as a mutable reference.
|
||||
///
|
||||
/// Note: writing to the writer may cause incorrect output after the write. This is due to the
|
||||
/// way that the Terminal implements diffing Buffers.
|
||||
#[instability::unstable(
|
||||
feature = "backend-writer",
|
||||
issue = "https://github.com/ratatui/ratatui/pull/991"
|
||||
)]
|
||||
pub fn writer_mut(&mut self) -> &mut W {
|
||||
&mut self.writer
|
||||
}
|
||||
}
|
||||
|
||||
impl<W> Write for CrosstermBackend<W>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
/// Writes a buffer of bytes to the underlying buffer.
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
self.writer.write(buf)
|
||||
}
|
||||
|
||||
/// Flushes the underlying buffer.
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.writer.flush()
|
||||
}
|
||||
}
|
||||
|
||||
impl<W> Backend for CrosstermBackend<W>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
|
||||
where
|
||||
I: Iterator<Item = (u16, u16, &'a Cell)>,
|
||||
{
|
||||
let mut fg = Color::Reset;
|
||||
let mut bg = Color::Reset;
|
||||
#[cfg(feature = "underline-color")]
|
||||
let mut underline_color = Color::Reset;
|
||||
let mut modifier = Modifier::empty();
|
||||
let mut last_pos: Option<Position> = None;
|
||||
for (x, y, cell) in content {
|
||||
// Move the cursor if the previous location was not (x - 1, y)
|
||||
if !matches!(last_pos, Some(p) if x == p.x + 1 && y == p.y) {
|
||||
queue!(self.writer, MoveTo(x, y))?;
|
||||
}
|
||||
last_pos = Some(Position { x, y });
|
||||
if cell.modifier != modifier {
|
||||
let diff = ModifierDiff {
|
||||
from: modifier,
|
||||
to: cell.modifier,
|
||||
};
|
||||
diff.queue(&mut self.writer)?;
|
||||
modifier = cell.modifier;
|
||||
}
|
||||
if cell.fg != fg || cell.bg != bg {
|
||||
queue!(
|
||||
self.writer,
|
||||
SetColors(CrosstermColors::new(
|
||||
cell.fg.into_crossterm(),
|
||||
cell.bg.into_crossterm(),
|
||||
))
|
||||
)?;
|
||||
fg = cell.fg;
|
||||
bg = cell.bg;
|
||||
}
|
||||
#[cfg(feature = "underline-color")]
|
||||
if cell.underline_color != underline_color {
|
||||
let color = cell.underline_color.into_crossterm();
|
||||
queue!(self.writer, SetUnderlineColor(color))?;
|
||||
underline_color = cell.underline_color;
|
||||
}
|
||||
|
||||
queue!(self.writer, Print(cell.symbol()))?;
|
||||
}
|
||||
|
||||
#[cfg(feature = "underline-color")]
|
||||
return queue!(
|
||||
self.writer,
|
||||
SetForegroundColor(CrosstermColor::Reset),
|
||||
SetBackgroundColor(CrosstermColor::Reset),
|
||||
SetUnderlineColor(CrosstermColor::Reset),
|
||||
SetAttribute(CrosstermAttribute::Reset),
|
||||
);
|
||||
#[cfg(not(feature = "underline-color"))]
|
||||
return queue!(
|
||||
self.writer,
|
||||
SetForegroundColor(CrosstermColor::Reset),
|
||||
SetBackgroundColor(CrosstermColor::Reset),
|
||||
SetAttribute(CrosstermAttribute::Reset),
|
||||
);
|
||||
}
|
||||
|
||||
fn hide_cursor(&mut self) -> io::Result<()> {
|
||||
execute!(self.writer, Hide)
|
||||
}
|
||||
|
||||
fn show_cursor(&mut self) -> io::Result<()> {
|
||||
execute!(self.writer, Show)
|
||||
}
|
||||
|
||||
fn get_cursor_position(&mut self) -> io::Result<Position> {
|
||||
crossterm::cursor::position()
|
||||
.map(|(x, y)| Position { x, y })
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))
|
||||
}
|
||||
|
||||
fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()> {
|
||||
let Position { x, y } = position.into();
|
||||
execute!(self.writer, MoveTo(x, y))
|
||||
}
|
||||
|
||||
fn clear(&mut self) -> io::Result<()> {
|
||||
self.clear_region(ClearType::All)
|
||||
}
|
||||
|
||||
fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
|
||||
execute!(
|
||||
self.writer,
|
||||
Clear(match clear_type {
|
||||
ClearType::All => crossterm::terminal::ClearType::All,
|
||||
ClearType::AfterCursor => crossterm::terminal::ClearType::FromCursorDown,
|
||||
ClearType::BeforeCursor => crossterm::terminal::ClearType::FromCursorUp,
|
||||
ClearType::CurrentLine => crossterm::terminal::ClearType::CurrentLine,
|
||||
ClearType::UntilNewLine => crossterm::terminal::ClearType::UntilNewLine,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
fn append_lines(&mut self, n: u16) -> io::Result<()> {
|
||||
for _ in 0..n {
|
||||
queue!(self.writer, Print("\n"))?;
|
||||
}
|
||||
self.writer.flush()
|
||||
}
|
||||
|
||||
fn size(&self) -> io::Result<Size> {
|
||||
let (width, height) = terminal::size()?;
|
||||
Ok(Size { width, height })
|
||||
}
|
||||
|
||||
fn window_size(&mut self) -> io::Result<WindowSize> {
|
||||
let crossterm::terminal::WindowSize {
|
||||
columns,
|
||||
rows,
|
||||
width,
|
||||
height,
|
||||
} = terminal::window_size()?;
|
||||
Ok(WindowSize {
|
||||
columns_rows: Size {
|
||||
width: columns,
|
||||
height: rows,
|
||||
},
|
||||
pixels: Size { width, height },
|
||||
})
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.writer.flush()
|
||||
}
|
||||
|
||||
#[cfg(feature = "scrolling-regions")]
|
||||
fn scroll_region_up(&mut self, region: std::ops::Range<u16>, amount: u16) -> io::Result<()> {
|
||||
queue!(
|
||||
self.writer,
|
||||
ScrollUpInRegion {
|
||||
first_row: region.start,
|
||||
last_row: region.end.saturating_sub(1),
|
||||
lines_to_scroll: amount,
|
||||
}
|
||||
)?;
|
||||
self.writer.flush()
|
||||
}
|
||||
|
||||
#[cfg(feature = "scrolling-regions")]
|
||||
fn scroll_region_down(&mut self, region: std::ops::Range<u16>, amount: u16) -> io::Result<()> {
|
||||
queue!(
|
||||
self.writer,
|
||||
ScrollDownInRegion {
|
||||
first_row: region.start,
|
||||
last_row: region.end.saturating_sub(1),
|
||||
lines_to_scroll: amount,
|
||||
}
|
||||
)?;
|
||||
self.writer.flush()
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for converting a Ratatui type to a Crossterm type.
|
||||
///
|
||||
/// This trait is needed for avoiding the orphan rule when implementing `From` for crossterm types
|
||||
/// once these are moved to a separate crate.
|
||||
pub trait IntoCrossterm<C> {
|
||||
/// Converts the ratatui type to a crossterm type.
|
||||
fn into_crossterm(self) -> C;
|
||||
}
|
||||
|
||||
/// A trait for converting a Crossterm type to a Ratatui type.
|
||||
///
|
||||
/// This trait is needed for avoiding the orphan rule when implementing `From` for crossterm types
|
||||
/// once these are moved to a separate crate.
|
||||
pub trait FromCrossterm<C> {
|
||||
/// Converts the crossterm type to a ratatui type.
|
||||
fn from_crossterm(value: C) -> Self;
|
||||
}
|
||||
|
||||
impl IntoCrossterm<CrosstermColor> for Color {
|
||||
fn into_crossterm(self) -> CrosstermColor {
|
||||
match self {
|
||||
Self::Reset => CrosstermColor::Reset,
|
||||
Self::Black => CrosstermColor::Black,
|
||||
Self::Red => CrosstermColor::DarkRed,
|
||||
Self::Green => CrosstermColor::DarkGreen,
|
||||
Self::Yellow => CrosstermColor::DarkYellow,
|
||||
Self::Blue => CrosstermColor::DarkBlue,
|
||||
Self::Magenta => CrosstermColor::DarkMagenta,
|
||||
Self::Cyan => CrosstermColor::DarkCyan,
|
||||
Self::Gray => CrosstermColor::Grey,
|
||||
Self::DarkGray => CrosstermColor::DarkGrey,
|
||||
Self::LightRed => CrosstermColor::Red,
|
||||
Self::LightGreen => CrosstermColor::Green,
|
||||
Self::LightBlue => CrosstermColor::Blue,
|
||||
Self::LightYellow => CrosstermColor::Yellow,
|
||||
Self::LightMagenta => CrosstermColor::Magenta,
|
||||
Self::LightCyan => CrosstermColor::Cyan,
|
||||
Self::White => CrosstermColor::White,
|
||||
Self::Indexed(i) => CrosstermColor::AnsiValue(i),
|
||||
Self::Rgb(r, g, b) => CrosstermColor::Rgb { r, g, b },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromCrossterm<CrosstermColor> for Color {
|
||||
fn from_crossterm(value: CrosstermColor) -> Self {
|
||||
match value {
|
||||
CrosstermColor::Reset => Self::Reset,
|
||||
CrosstermColor::Black => Self::Black,
|
||||
CrosstermColor::DarkRed => Self::Red,
|
||||
CrosstermColor::DarkGreen => Self::Green,
|
||||
CrosstermColor::DarkYellow => Self::Yellow,
|
||||
CrosstermColor::DarkBlue => Self::Blue,
|
||||
CrosstermColor::DarkMagenta => Self::Magenta,
|
||||
CrosstermColor::DarkCyan => Self::Cyan,
|
||||
CrosstermColor::Grey => Self::Gray,
|
||||
CrosstermColor::DarkGrey => Self::DarkGray,
|
||||
CrosstermColor::Red => Self::LightRed,
|
||||
CrosstermColor::Green => Self::LightGreen,
|
||||
CrosstermColor::Blue => Self::LightBlue,
|
||||
CrosstermColor::Yellow => Self::LightYellow,
|
||||
CrosstermColor::Magenta => Self::LightMagenta,
|
||||
CrosstermColor::Cyan => Self::LightCyan,
|
||||
CrosstermColor::White => Self::White,
|
||||
CrosstermColor::Rgb { r, g, b } => Self::Rgb(r, g, b),
|
||||
CrosstermColor::AnsiValue(v) => Self::Indexed(v),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The `ModifierDiff` struct is used to calculate the difference between two `Modifier`
|
||||
/// values. This is useful when updating the terminal display, as it allows for more
|
||||
/// efficient updates by only sending the necessary changes.
|
||||
struct ModifierDiff {
|
||||
pub from: Modifier,
|
||||
pub to: Modifier,
|
||||
}
|
||||
|
||||
impl ModifierDiff {
|
||||
fn queue<W>(self, mut w: W) -> io::Result<()>
|
||||
where
|
||||
W: io::Write,
|
||||
{
|
||||
//use crossterm::Attribute;
|
||||
let removed = self.from - self.to;
|
||||
if removed.contains(Modifier::REVERSED) {
|
||||
queue!(w, SetAttribute(CrosstermAttribute::NoReverse))?;
|
||||
}
|
||||
if removed.contains(Modifier::BOLD) {
|
||||
queue!(w, SetAttribute(CrosstermAttribute::NormalIntensity))?;
|
||||
if self.to.contains(Modifier::DIM) {
|
||||
queue!(w, SetAttribute(CrosstermAttribute::Dim))?;
|
||||
}
|
||||
}
|
||||
if removed.contains(Modifier::ITALIC) {
|
||||
queue!(w, SetAttribute(CrosstermAttribute::NoItalic))?;
|
||||
}
|
||||
if removed.contains(Modifier::UNDERLINED) {
|
||||
queue!(w, SetAttribute(CrosstermAttribute::NoUnderline))?;
|
||||
}
|
||||
if removed.contains(Modifier::DIM) {
|
||||
queue!(w, SetAttribute(CrosstermAttribute::NormalIntensity))?;
|
||||
}
|
||||
if removed.contains(Modifier::CROSSED_OUT) {
|
||||
queue!(w, SetAttribute(CrosstermAttribute::NotCrossedOut))?;
|
||||
}
|
||||
if removed.contains(Modifier::SLOW_BLINK) || removed.contains(Modifier::RAPID_BLINK) {
|
||||
queue!(w, SetAttribute(CrosstermAttribute::NoBlink))?;
|
||||
}
|
||||
|
||||
let added = self.to - self.from;
|
||||
if added.contains(Modifier::REVERSED) {
|
||||
queue!(w, SetAttribute(CrosstermAttribute::Reverse))?;
|
||||
}
|
||||
if added.contains(Modifier::BOLD) {
|
||||
queue!(w, SetAttribute(CrosstermAttribute::Bold))?;
|
||||
}
|
||||
if added.contains(Modifier::ITALIC) {
|
||||
queue!(w, SetAttribute(CrosstermAttribute::Italic))?;
|
||||
}
|
||||
if added.contains(Modifier::UNDERLINED) {
|
||||
queue!(w, SetAttribute(CrosstermAttribute::Underlined))?;
|
||||
}
|
||||
if added.contains(Modifier::DIM) {
|
||||
queue!(w, SetAttribute(CrosstermAttribute::Dim))?;
|
||||
}
|
||||
if added.contains(Modifier::CROSSED_OUT) {
|
||||
queue!(w, SetAttribute(CrosstermAttribute::CrossedOut))?;
|
||||
}
|
||||
if added.contains(Modifier::SLOW_BLINK) {
|
||||
queue!(w, SetAttribute(CrosstermAttribute::SlowBlink))?;
|
||||
}
|
||||
if added.contains(Modifier::RAPID_BLINK) {
|
||||
queue!(w, SetAttribute(CrosstermAttribute::RapidBlink))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromCrossterm<CrosstermAttribute> for Modifier {
|
||||
fn from_crossterm(value: CrosstermAttribute) -> Self {
|
||||
// `Attribute*s*` (note the *s*) contains multiple `Attribute` We convert `Attribute` to
|
||||
// `Attribute*s*` (containing only 1 value) to avoid implementing the conversion again
|
||||
Self::from_crossterm(CrosstermAttributes::from(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl FromCrossterm<CrosstermAttributes> for Modifier {
|
||||
fn from_crossterm(value: CrosstermAttributes) -> Self {
|
||||
let mut res = Self::empty();
|
||||
if value.has(CrosstermAttribute::Bold) {
|
||||
res |= Self::BOLD;
|
||||
}
|
||||
if value.has(CrosstermAttribute::Dim) {
|
||||
res |= Self::DIM;
|
||||
}
|
||||
if value.has(CrosstermAttribute::Italic) {
|
||||
res |= Self::ITALIC;
|
||||
}
|
||||
if value.has(CrosstermAttribute::Underlined)
|
||||
|| value.has(CrosstermAttribute::DoubleUnderlined)
|
||||
|| value.has(CrosstermAttribute::Undercurled)
|
||||
|| value.has(CrosstermAttribute::Underdotted)
|
||||
|| value.has(CrosstermAttribute::Underdashed)
|
||||
{
|
||||
res |= Self::UNDERLINED;
|
||||
}
|
||||
if value.has(CrosstermAttribute::SlowBlink) {
|
||||
res |= Self::SLOW_BLINK;
|
||||
}
|
||||
if value.has(CrosstermAttribute::RapidBlink) {
|
||||
res |= Self::RAPID_BLINK;
|
||||
}
|
||||
if value.has(CrosstermAttribute::Reverse) {
|
||||
res |= Self::REVERSED;
|
||||
}
|
||||
if value.has(CrosstermAttribute::Hidden) {
|
||||
res |= Self::HIDDEN;
|
||||
}
|
||||
if value.has(CrosstermAttribute::CrossedOut) {
|
||||
res |= Self::CROSSED_OUT;
|
||||
}
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
impl FromCrossterm<ContentStyle> for Style {
|
||||
fn from_crossterm(value: ContentStyle) -> Self {
|
||||
let mut sub_modifier = Modifier::empty();
|
||||
if value.attributes.has(CrosstermAttribute::NoBold) {
|
||||
sub_modifier |= Modifier::BOLD;
|
||||
}
|
||||
if value.attributes.has(CrosstermAttribute::NoItalic) {
|
||||
sub_modifier |= Modifier::ITALIC;
|
||||
}
|
||||
if value.attributes.has(CrosstermAttribute::NotCrossedOut) {
|
||||
sub_modifier |= Modifier::CROSSED_OUT;
|
||||
}
|
||||
if value.attributes.has(CrosstermAttribute::NoUnderline) {
|
||||
sub_modifier |= Modifier::UNDERLINED;
|
||||
}
|
||||
if value.attributes.has(CrosstermAttribute::NoHidden) {
|
||||
sub_modifier |= Modifier::HIDDEN;
|
||||
}
|
||||
if value.attributes.has(CrosstermAttribute::NoBlink) {
|
||||
sub_modifier |= Modifier::RAPID_BLINK | Modifier::SLOW_BLINK;
|
||||
}
|
||||
if value.attributes.has(CrosstermAttribute::NoReverse) {
|
||||
sub_modifier |= Modifier::REVERSED;
|
||||
}
|
||||
|
||||
Self {
|
||||
fg: value.foreground_color.map(FromCrossterm::from_crossterm),
|
||||
bg: value.background_color.map(FromCrossterm::from_crossterm),
|
||||
#[cfg(feature = "underline-color")]
|
||||
underline_color: value.underline_color.map(FromCrossterm::from_crossterm),
|
||||
add_modifier: Modifier::from_crossterm(value.attributes),
|
||||
sub_modifier,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A command that scrolls the terminal screen a given number of rows up in a specific scrolling
|
||||
/// region.
|
||||
///
|
||||
/// This will hopefully be replaced by a struct in crossterm proper. There are two outstanding
|
||||
/// crossterm PRs that will address this:
|
||||
/// - [918](https://github.com/crossterm-rs/crossterm/pull/918)
|
||||
/// - [923](https://github.com/crossterm-rs/crossterm/pull/923)
|
||||
#[cfg(feature = "scrolling-regions")]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
struct ScrollUpInRegion {
|
||||
/// The first row of the scrolling region.
|
||||
pub first_row: u16,
|
||||
|
||||
/// The last row of the scrolling region.
|
||||
pub last_row: u16,
|
||||
|
||||
/// The number of lines to scroll up by.
|
||||
pub lines_to_scroll: u16,
|
||||
}
|
||||
|
||||
#[cfg(feature = "scrolling-regions")]
|
||||
impl crate::crossterm::Command for ScrollUpInRegion {
|
||||
fn write_ansi(&self, f: &mut impl std::fmt::Write) -> std::fmt::Result {
|
||||
if self.lines_to_scroll != 0 {
|
||||
// Set a scrolling region that contains just the desired lines.
|
||||
write!(
|
||||
f,
|
||||
crate::crossterm::csi!("{};{}r"),
|
||||
self.first_row.saturating_add(1),
|
||||
self.last_row.saturating_add(1)
|
||||
)?;
|
||||
// Scroll the region by the desired count.
|
||||
write!(f, crate::crossterm::csi!("{}S"), self.lines_to_scroll)?;
|
||||
// Reset the scrolling region to be the whole screen.
|
||||
write!(f, crate::crossterm::csi!("r"))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn execute_winapi(&self) -> io::Result<()> {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Unsupported,
|
||||
"ScrollUpInRegion command not supported for winapi",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// A command that scrolls the terminal screen a given number of rows down in a specific scrolling
|
||||
/// region.
|
||||
///
|
||||
/// This will hopefully be replaced by a struct in crossterm proper. There are two outstanding
|
||||
/// crossterm PRs that will address this:
|
||||
/// - [918](https://github.com/crossterm-rs/crossterm/pull/918)
|
||||
/// - [923](https://github.com/crossterm-rs/crossterm/pull/923)
|
||||
#[cfg(feature = "scrolling-regions")]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
struct ScrollDownInRegion {
|
||||
/// The first row of the scrolling region.
|
||||
pub first_row: u16,
|
||||
|
||||
/// The last row of the scrolling region.
|
||||
pub last_row: u16,
|
||||
|
||||
/// The number of lines to scroll down by.
|
||||
pub lines_to_scroll: u16,
|
||||
}
|
||||
|
||||
#[cfg(feature = "scrolling-regions")]
|
||||
impl crate::crossterm::Command for ScrollDownInRegion {
|
||||
fn write_ansi(&self, f: &mut impl std::fmt::Write) -> std::fmt::Result {
|
||||
if self.lines_to_scroll != 0 {
|
||||
// Set a scrolling region that contains just the desired lines.
|
||||
write!(
|
||||
f,
|
||||
crate::crossterm::csi!("{};{}r"),
|
||||
self.first_row.saturating_add(1),
|
||||
self.last_row.saturating_add(1)
|
||||
)?;
|
||||
// Scroll the region by the desired count.
|
||||
write!(f, crate::crossterm::csi!("{}T"), self.lines_to_scroll)?;
|
||||
// Reset the scrolling region to be the whole screen.
|
||||
write!(f, crate::crossterm::csi!("r"))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn execute_winapi(&self) -> io::Result<()> {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Unsupported,
|
||||
"ScrollDownInRegion command not supported for winapi",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[rstest]
|
||||
#[case(CrosstermColor::Reset, Color::Reset)]
|
||||
#[case(CrosstermColor::Black, Color::Black)]
|
||||
#[case(CrosstermColor::DarkGrey, Color::DarkGray)]
|
||||
#[case(CrosstermColor::Red, Color::LightRed)]
|
||||
#[case(CrosstermColor::DarkRed, Color::Red)]
|
||||
#[case(CrosstermColor::Green, Color::LightGreen)]
|
||||
#[case(CrosstermColor::DarkGreen, Color::Green)]
|
||||
#[case(CrosstermColor::Yellow, Color::LightYellow)]
|
||||
#[case(CrosstermColor::DarkYellow, Color::Yellow)]
|
||||
#[case(CrosstermColor::Blue, Color::LightBlue)]
|
||||
#[case(CrosstermColor::DarkBlue, Color::Blue)]
|
||||
#[case(CrosstermColor::Magenta, Color::LightMagenta)]
|
||||
#[case(CrosstermColor::DarkMagenta, Color::Magenta)]
|
||||
#[case(CrosstermColor::Cyan, Color::LightCyan)]
|
||||
#[case(CrosstermColor::DarkCyan, Color::Cyan)]
|
||||
#[case(CrosstermColor::White, Color::White)]
|
||||
#[case(CrosstermColor::Grey, Color::Gray)]
|
||||
#[case(CrosstermColor::Rgb { r: 0, g: 0, b: 0 }, Color::Rgb(0, 0, 0) )]
|
||||
#[case(CrosstermColor::Rgb { r: 10, g: 20, b: 30 }, Color::Rgb(10, 20, 30) )]
|
||||
#[case(CrosstermColor::AnsiValue(32), Color::Indexed(32))]
|
||||
#[case(CrosstermColor::AnsiValue(37), Color::Indexed(37))]
|
||||
fn from_crossterm_color(#[case] crossterm_color: CrosstermColor, #[case] color: Color) {
|
||||
assert_eq!(Color::from_crossterm(crossterm_color), color);
|
||||
}
|
||||
|
||||
mod modifier {
|
||||
use super::*;
|
||||
|
||||
#[rstest]
|
||||
#[case(CrosstermAttribute::Reset, Modifier::empty())]
|
||||
#[case(CrosstermAttribute::Bold, Modifier::BOLD)]
|
||||
#[case(CrosstermAttribute::NoBold, Modifier::empty())]
|
||||
#[case(CrosstermAttribute::Italic, Modifier::ITALIC)]
|
||||
#[case(CrosstermAttribute::NoItalic, Modifier::empty())]
|
||||
#[case(CrosstermAttribute::Underlined, Modifier::UNDERLINED)]
|
||||
#[case(CrosstermAttribute::NoUnderline, Modifier::empty())]
|
||||
#[case(CrosstermAttribute::OverLined, Modifier::empty())]
|
||||
#[case(CrosstermAttribute::NotOverLined, Modifier::empty())]
|
||||
#[case(CrosstermAttribute::DoubleUnderlined, Modifier::UNDERLINED)]
|
||||
#[case(CrosstermAttribute::Undercurled, Modifier::UNDERLINED)]
|
||||
#[case(CrosstermAttribute::Underdotted, Modifier::UNDERLINED)]
|
||||
#[case(CrosstermAttribute::Underdashed, Modifier::UNDERLINED)]
|
||||
#[case(CrosstermAttribute::Dim, Modifier::DIM)]
|
||||
#[case(CrosstermAttribute::NormalIntensity, Modifier::empty())]
|
||||
#[case(CrosstermAttribute::CrossedOut, Modifier::CROSSED_OUT)]
|
||||
#[case(CrosstermAttribute::NotCrossedOut, Modifier::empty())]
|
||||
#[case(CrosstermAttribute::NoUnderline, Modifier::empty())]
|
||||
#[case(CrosstermAttribute::SlowBlink, Modifier::SLOW_BLINK)]
|
||||
#[case(CrosstermAttribute::RapidBlink, Modifier::RAPID_BLINK)]
|
||||
#[case(CrosstermAttribute::Hidden, Modifier::HIDDEN)]
|
||||
#[case(CrosstermAttribute::NoHidden, Modifier::empty())]
|
||||
#[case(CrosstermAttribute::Reverse, Modifier::REVERSED)]
|
||||
#[case(CrosstermAttribute::NoReverse, Modifier::empty())]
|
||||
fn from_crossterm_attribute(
|
||||
#[case] crossterm_attribute: CrosstermAttribute,
|
||||
#[case] ratatui_modifier: Modifier,
|
||||
) {
|
||||
assert_eq!(
|
||||
Modifier::from_crossterm(crossterm_attribute),
|
||||
ratatui_modifier
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(&[CrosstermAttribute::Bold], Modifier::BOLD)]
|
||||
#[case(&[CrosstermAttribute::Bold, CrosstermAttribute::Italic], Modifier::BOLD | Modifier::ITALIC)]
|
||||
#[case(&[CrosstermAttribute::Bold, CrosstermAttribute::NotCrossedOut], Modifier::BOLD)]
|
||||
#[case(&[CrosstermAttribute::Dim, CrosstermAttribute::Underdotted], Modifier::DIM | Modifier::UNDERLINED)]
|
||||
#[case(&[CrosstermAttribute::Dim, CrosstermAttribute::SlowBlink, CrosstermAttribute::Italic], Modifier::DIM | Modifier::SLOW_BLINK | Modifier::ITALIC)]
|
||||
#[case(&[CrosstermAttribute::Hidden, CrosstermAttribute::NoUnderline, CrosstermAttribute::NotCrossedOut], Modifier::HIDDEN)]
|
||||
#[case(&[CrosstermAttribute::Reverse], Modifier::REVERSED)]
|
||||
#[case(&[CrosstermAttribute::Reset], Modifier::empty())]
|
||||
#[case(&[CrosstermAttribute::RapidBlink, CrosstermAttribute::CrossedOut], Modifier::RAPID_BLINK | Modifier::CROSSED_OUT)]
|
||||
fn from_crossterm_attributes(
|
||||
#[case] crossterm_attributes: &[CrosstermAttribute],
|
||||
#[case] ratatui_modifier: Modifier,
|
||||
) {
|
||||
assert_eq!(
|
||||
Modifier::from_crossterm(CrosstermAttributes::from(crossterm_attributes)),
|
||||
ratatui_modifier
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(ContentStyle::default(), Style::default())]
|
||||
#[case(
|
||||
ContentStyle {
|
||||
foreground_color: Some(CrosstermColor::DarkYellow),
|
||||
..Default::default()
|
||||
},
|
||||
Style::default().fg(Color::Yellow)
|
||||
)]
|
||||
#[case(
|
||||
ContentStyle {
|
||||
background_color: Some(CrosstermColor::DarkYellow),
|
||||
..Default::default()
|
||||
},
|
||||
Style::default().bg(Color::Yellow)
|
||||
)]
|
||||
#[case(
|
||||
ContentStyle {
|
||||
attributes: CrosstermAttributes::from(CrosstermAttribute::Bold),
|
||||
..Default::default()
|
||||
},
|
||||
Style::default().add_modifier(Modifier::BOLD)
|
||||
)]
|
||||
#[case(
|
||||
ContentStyle {
|
||||
attributes: CrosstermAttributes::from(CrosstermAttribute::NoBold),
|
||||
..Default::default()
|
||||
},
|
||||
Style::default().remove_modifier(Modifier::BOLD)
|
||||
)]
|
||||
#[case(
|
||||
ContentStyle {
|
||||
attributes: CrosstermAttributes::from(CrosstermAttribute::Italic),
|
||||
..Default::default()
|
||||
},
|
||||
Style::default().add_modifier(Modifier::ITALIC)
|
||||
)]
|
||||
#[case(
|
||||
ContentStyle {
|
||||
attributes: CrosstermAttributes::from(CrosstermAttribute::NoItalic),
|
||||
..Default::default()
|
||||
},
|
||||
Style::default().remove_modifier(Modifier::ITALIC)
|
||||
)]
|
||||
#[case(
|
||||
ContentStyle {
|
||||
attributes: CrosstermAttributes::from(
|
||||
[CrosstermAttribute::Bold, CrosstermAttribute::Italic].as_ref()
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.add_modifier(Modifier::ITALIC)
|
||||
)]
|
||||
#[case(
|
||||
ContentStyle {
|
||||
attributes: CrosstermAttributes::from(
|
||||
[CrosstermAttribute::NoBold, CrosstermAttribute::NoItalic].as_ref()
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
Style::default()
|
||||
.remove_modifier(Modifier::BOLD)
|
||||
.remove_modifier(Modifier::ITALIC)
|
||||
)]
|
||||
fn from_crossterm_content_style(#[case] content_style: ContentStyle, #[case] style: Style) {
|
||||
assert_eq!(Style::from_crossterm(content_style), style);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "underline-color")]
|
||||
fn from_crossterm_content_style_underline() {
|
||||
let content_style = ContentStyle {
|
||||
underline_color: Some(CrosstermColor::DarkRed),
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(
|
||||
Style::from_crossterm(content_style),
|
||||
Style::default().underline_color(Color::Red)
|
||||
);
|
||||
}
|
||||
}
|
||||
35
ratatui-termion/Cargo.toml
Normal file
35
ratatui-termion/Cargo.toml
Normal file
@@ -0,0 +1,35 @@
|
||||
[package]
|
||||
name = "ratatui-termion"
|
||||
version = "0.1.0-alpha.0"
|
||||
description = "Termion backend for the Ratatui Terminal UI library."
|
||||
documentation = "https://docs.rs/ratatui-termion/"
|
||||
readme = "README.md"
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
keywords.workspace = true
|
||||
categories.workspace = true
|
||||
license.workspace = true
|
||||
exclude.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
## Use terminal scrolling regions to make Terminal::insert_before less prone to flickering.
|
||||
scrolling-regions = ["ratatui-core/scrolling-regions"]
|
||||
|
||||
[dependencies]
|
||||
document-features = { workspace = true, optional = true }
|
||||
ratatui-core = { workspace = true }
|
||||
termion.workspace = true
|
||||
instability.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
rstest.workspace = true
|
||||
11
ratatui-termion/README.md
Normal file
11
ratatui-termion/README.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Ratatui-termion
|
||||
|
||||
<!-- cargo-rdme start -->
|
||||
|
||||
This module provides the [`TermionBackend`] implementation for the [`Backend`] trait. It uses
|
||||
the [Termion] crate to interact with the terminal.
|
||||
|
||||
[`Backend`]: ratatui_core::backend::Backend
|
||||
[Termion]: https://docs.rs/termion
|
||||
|
||||
<!-- cargo-rdme end -->
|
||||
@@ -1,21 +1,32 @@
|
||||
// show the feature flags in the generated documentation
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![doc(
|
||||
html_logo_url = "https://raw.githubusercontent.com/ratatui/ratatui/main/assets/logo.png",
|
||||
html_favicon_url = "https://raw.githubusercontent.com/ratatui/ratatui/main/assets/favicon.ico"
|
||||
)]
|
||||
#![warn(missing_docs)]
|
||||
//! This module provides the [`TermionBackend`] implementation for the [`Backend`] trait. It uses
|
||||
//! the [Termion] crate to interact with the terminal.
|
||||
//!
|
||||
//! [`Backend`]: crate::backend::Backend
|
||||
//! [`TermionBackend`]: crate::backend::TermionBackend
|
||||
//! [`Backend`]: ratatui_core::backend::Backend
|
||||
//! [Termion]: https://docs.rs/termion
|
||||
#![cfg_attr(feature = "document-features", doc = "\n## Features")]
|
||||
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
|
||||
|
||||
use std::{
|
||||
fmt,
|
||||
io::{self, Write},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
use ratatui_core::{
|
||||
backend::{Backend, ClearType, WindowSize},
|
||||
buffer::Cell,
|
||||
layout::{Position, Size},
|
||||
style::{Color, Modifier, Style},
|
||||
termion::{self, color as tcolor, color::Color as _, style as tstyle},
|
||||
};
|
||||
pub use termion;
|
||||
use termion::{color as tcolor, color::Color as _, style as tstyle};
|
||||
|
||||
/// A [`Backend`] implementation that uses [Termion] to render to the terminal.
|
||||
///
|
||||
@@ -40,8 +51,9 @@ use crate::{
|
||||
/// use std::io::{stderr, stdout};
|
||||
///
|
||||
/// use ratatui::{
|
||||
/// prelude::*,
|
||||
/// backend::TermionBackend,
|
||||
/// termion::{raw::IntoRawMode, screen::IntoAlternateScreen},
|
||||
/// Terminal,
|
||||
/// };
|
||||
///
|
||||
/// let writer = stdout().into_raw_mode()?.into_alternate_screen()?;
|
||||
@@ -60,7 +72,7 @@ use crate::{
|
||||
///
|
||||
/// [`IntoRawMode::into_raw_mode()`]: termion::raw::IntoRawMode
|
||||
/// [`IntoAlternateScreen::into_alternate_screen()`]: termion::screen::IntoAlternateScreen
|
||||
/// [`Terminal`]: crate::terminal::Terminal
|
||||
/// [`Terminal`]: ratatui::terminal::Terminal
|
||||
/// [Termion]: https://docs.rs/termion
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct TermionBackend<W>
|
||||
@@ -76,11 +88,18 @@ where
|
||||
{
|
||||
/// Creates a new Termion backend with the given writer.
|
||||
///
|
||||
/// Most applications will use either [`stdout`](std::io::stdout) or
|
||||
/// [`stderr`](std::io::stderr) as writer. See the [FAQ] to determine which one to use.
|
||||
///
|
||||
/// [FAQ]: https://ratatui.rs/faq/#should-i-use-stdout-or-stderr
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use std::io::stdout;
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use std::io::stdout;
|
||||
///
|
||||
/// use ratatui::backend::TermionBackend;
|
||||
///
|
||||
/// let backend = TermionBackend::new(stdout());
|
||||
/// ```
|
||||
pub const fn new(writer: W) -> Self {
|
||||
@@ -90,7 +109,7 @@ where
|
||||
/// Gets the writer.
|
||||
#[instability::unstable(
|
||||
feature = "backend-writer",
|
||||
issue = "https://github.com/ratatui-org/ratatui/pull/991"
|
||||
issue = "https://github.com/ratatui/ratatui/pull/991"
|
||||
)]
|
||||
pub const fn writer(&self) -> &W {
|
||||
&self.writer
|
||||
@@ -101,7 +120,7 @@ where
|
||||
/// way that the Terminal implements diffing Buffers.
|
||||
#[instability::unstable(
|
||||
feature = "backend-writer",
|
||||
issue = "https://github.com/ratatui-org/ratatui/pull/991"
|
||||
issue = "https://github.com/ratatui/ratatui/pull/991"
|
||||
)]
|
||||
pub fn writer_mut(&mut self) -> &mut W {
|
||||
&mut self.writer
|
||||
@@ -231,6 +250,30 @@ where
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.writer.flush()
|
||||
}
|
||||
|
||||
#[cfg(feature = "scrolling-regions")]
|
||||
fn scroll_region_up(&mut self, region: std::ops::Range<u16>, amount: u16) -> io::Result<()> {
|
||||
write!(
|
||||
self.writer,
|
||||
"{}{}{}",
|
||||
SetRegion(region.start.saturating_add(1), region.end),
|
||||
termion::scroll::Up(amount),
|
||||
ResetRegion,
|
||||
)?;
|
||||
self.writer.flush()
|
||||
}
|
||||
|
||||
#[cfg(feature = "scrolling-regions")]
|
||||
fn scroll_region_down(&mut self, region: std::ops::Range<u16>, amount: u16) -> io::Result<()> {
|
||||
write!(
|
||||
self.writer,
|
||||
"{}{}{}",
|
||||
SetRegion(region.start.saturating_add(1), region.end),
|
||||
termion::scroll::Down(amount),
|
||||
ResetRegion,
|
||||
)?;
|
||||
self.writer.flush()
|
||||
}
|
||||
}
|
||||
struct Fg(Color);
|
||||
|
||||
@@ -295,22 +338,40 @@ impl fmt::Display for Bg {
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for converting a Termion type to a Ratatui type.
|
||||
///
|
||||
/// This trait is necessary to avoid the orphan rule, as we cannot implement a trait for a type
|
||||
/// defined in another crate.
|
||||
pub trait FromTermion<T> {
|
||||
/// Convert the Termion type to the Ratatui type.
|
||||
fn from_termion(termion: T) -> Self;
|
||||
}
|
||||
|
||||
/// A trait for converting a Ratatui type to a Termion type.
|
||||
///
|
||||
/// This trait is necessary to avoid the orphan rule, as we cannot implement a trait for a type
|
||||
/// defined in another crate.
|
||||
pub trait IntoTermion<T> {
|
||||
/// Convert the Ratatui type to the Termion type.
|
||||
fn into_termion(self) -> T;
|
||||
}
|
||||
|
||||
macro_rules! from_termion_for_color {
|
||||
($termion_color:ident, $color:ident) => {
|
||||
impl From<tcolor::$termion_color> for Color {
|
||||
fn from(_: tcolor::$termion_color) -> Self {
|
||||
impl FromTermion<tcolor::$termion_color> for Color {
|
||||
fn from_termion(_: tcolor::$termion_color) -> Self {
|
||||
Color::$color
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tcolor::Bg<tcolor::$termion_color>> for Style {
|
||||
fn from(_: tcolor::Bg<tcolor::$termion_color>) -> Self {
|
||||
impl FromTermion<tcolor::Bg<tcolor::$termion_color>> for Style {
|
||||
fn from_termion(_: tcolor::Bg<tcolor::$termion_color>) -> Self {
|
||||
Style::default().bg(Color::$color)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tcolor::Fg<tcolor::$termion_color>> for Style {
|
||||
fn from(_: tcolor::Fg<tcolor::$termion_color>) -> Self {
|
||||
impl FromTermion<tcolor::Fg<tcolor::$termion_color>> for Style {
|
||||
fn from_termion(_: tcolor::Fg<tcolor::$termion_color>) -> Self {
|
||||
Style::default().fg(Color::$color)
|
||||
}
|
||||
}
|
||||
@@ -335,38 +396,38 @@ from_termion_for_color!(LightMagenta, LightMagenta);
|
||||
from_termion_for_color!(LightCyan, LightCyan);
|
||||
from_termion_for_color!(LightWhite, White);
|
||||
|
||||
impl From<tcolor::AnsiValue> for Color {
|
||||
fn from(value: tcolor::AnsiValue) -> Self {
|
||||
impl FromTermion<tcolor::AnsiValue> for Color {
|
||||
fn from_termion(value: tcolor::AnsiValue) -> Self {
|
||||
Self::Indexed(value.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tcolor::Bg<tcolor::AnsiValue>> for Style {
|
||||
fn from(value: tcolor::Bg<tcolor::AnsiValue>) -> Self {
|
||||
impl FromTermion<tcolor::Bg<tcolor::AnsiValue>> for Style {
|
||||
fn from_termion(value: tcolor::Bg<tcolor::AnsiValue>) -> Self {
|
||||
Self::default().bg(Color::Indexed(value.0 .0))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tcolor::Fg<tcolor::AnsiValue>> for Style {
|
||||
fn from(value: tcolor::Fg<tcolor::AnsiValue>) -> Self {
|
||||
impl FromTermion<tcolor::Fg<tcolor::AnsiValue>> for Style {
|
||||
fn from_termion(value: tcolor::Fg<tcolor::AnsiValue>) -> Self {
|
||||
Self::default().fg(Color::Indexed(value.0 .0))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tcolor::Rgb> for Color {
|
||||
fn from(value: tcolor::Rgb) -> Self {
|
||||
impl FromTermion<tcolor::Rgb> for Color {
|
||||
fn from_termion(value: tcolor::Rgb) -> Self {
|
||||
Self::Rgb(value.0, value.1, value.2)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tcolor::Bg<tcolor::Rgb>> for Style {
|
||||
fn from(value: tcolor::Bg<tcolor::Rgb>) -> Self {
|
||||
impl FromTermion<tcolor::Bg<tcolor::Rgb>> for Style {
|
||||
fn from_termion(value: tcolor::Bg<tcolor::Rgb>) -> Self {
|
||||
Self::default().bg(Color::Rgb(value.0 .0, value.0 .1, value.0 .2))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tcolor::Fg<tcolor::Rgb>> for Style {
|
||||
fn from(value: tcolor::Fg<tcolor::Rgb>) -> Self {
|
||||
impl FromTermion<tcolor::Fg<tcolor::Rgb>> for Style {
|
||||
fn from_termion(value: tcolor::Fg<tcolor::Rgb>) -> Self {
|
||||
Self::default().fg(Color::Rgb(value.0 .0, value.0 .1, value.0 .2))
|
||||
}
|
||||
}
|
||||
@@ -438,8 +499,8 @@ impl fmt::Display for ModifierDiff {
|
||||
|
||||
macro_rules! from_termion_for_modifier {
|
||||
($termion_modifier:ident, $modifier:ident) => {
|
||||
impl From<tstyle::$termion_modifier> for Modifier {
|
||||
fn from(_: tstyle::$termion_modifier) -> Self {
|
||||
impl FromTermion<tstyle::$termion_modifier> for Modifier {
|
||||
fn from_termion(_: tstyle::$termion_modifier) -> Self {
|
||||
Modifier::$modifier
|
||||
}
|
||||
}
|
||||
@@ -454,38 +515,68 @@ from_termion_for_modifier!(Faint, DIM);
|
||||
from_termion_for_modifier!(CrossedOut, CROSSED_OUT);
|
||||
from_termion_for_modifier!(Blink, SLOW_BLINK);
|
||||
|
||||
impl From<termion::style::Reset> for Modifier {
|
||||
fn from(_: termion::style::Reset) -> Self {
|
||||
impl FromTermion<termion::style::Reset> for Modifier {
|
||||
fn from_termion(_: termion::style::Reset) -> Self {
|
||||
Self::empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// Set scrolling region.
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub struct SetRegion(pub u16, pub u16);
|
||||
|
||||
impl fmt::Display for SetRegion {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "\x1B[{};{}r", self.0, self.1)
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset scrolling region.
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub struct ResetRegion;
|
||||
|
||||
impl fmt::Display for ResetRegion {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "\x1B[r")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ratatui_core::style::Stylize;
|
||||
|
||||
use super::*;
|
||||
use crate::style::Stylize;
|
||||
|
||||
#[test]
|
||||
fn from_termion_color() {
|
||||
assert_eq!(Color::from(tcolor::Reset), Color::Reset);
|
||||
assert_eq!(Color::from(tcolor::Black), Color::Black);
|
||||
assert_eq!(Color::from(tcolor::Red), Color::Red);
|
||||
assert_eq!(Color::from(tcolor::Green), Color::Green);
|
||||
assert_eq!(Color::from(tcolor::Yellow), Color::Yellow);
|
||||
assert_eq!(Color::from(tcolor::Blue), Color::Blue);
|
||||
assert_eq!(Color::from(tcolor::Magenta), Color::Magenta);
|
||||
assert_eq!(Color::from(tcolor::Cyan), Color::Cyan);
|
||||
assert_eq!(Color::from(tcolor::White), Color::Gray);
|
||||
assert_eq!(Color::from(tcolor::LightBlack), Color::DarkGray);
|
||||
assert_eq!(Color::from(tcolor::LightRed), Color::LightRed);
|
||||
assert_eq!(Color::from(tcolor::LightGreen), Color::LightGreen);
|
||||
assert_eq!(Color::from(tcolor::LightBlue), Color::LightBlue);
|
||||
assert_eq!(Color::from(tcolor::LightYellow), Color::LightYellow);
|
||||
assert_eq!(Color::from(tcolor::LightMagenta), Color::LightMagenta);
|
||||
assert_eq!(Color::from(tcolor::LightCyan), Color::LightCyan);
|
||||
assert_eq!(Color::from(tcolor::LightWhite), Color::White);
|
||||
assert_eq!(Color::from(tcolor::AnsiValue(31)), Color::Indexed(31));
|
||||
assert_eq!(Color::from(tcolor::Rgb(1, 2, 3)), Color::Rgb(1, 2, 3));
|
||||
assert_eq!(Color::from_termion(tcolor::Reset), Color::Reset);
|
||||
assert_eq!(Color::from_termion(tcolor::Black), Color::Black);
|
||||
assert_eq!(Color::from_termion(tcolor::Red), Color::Red);
|
||||
assert_eq!(Color::from_termion(tcolor::Green), Color::Green);
|
||||
assert_eq!(Color::from_termion(tcolor::Yellow), Color::Yellow);
|
||||
assert_eq!(Color::from_termion(tcolor::Blue), Color::Blue);
|
||||
assert_eq!(Color::from_termion(tcolor::Magenta), Color::Magenta);
|
||||
assert_eq!(Color::from_termion(tcolor::Cyan), Color::Cyan);
|
||||
assert_eq!(Color::from_termion(tcolor::White), Color::Gray);
|
||||
assert_eq!(Color::from_termion(tcolor::LightBlack), Color::DarkGray);
|
||||
assert_eq!(Color::from_termion(tcolor::LightRed), Color::LightRed);
|
||||
assert_eq!(Color::from_termion(tcolor::LightGreen), Color::LightGreen);
|
||||
assert_eq!(Color::from_termion(tcolor::LightBlue), Color::LightBlue);
|
||||
assert_eq!(Color::from_termion(tcolor::LightYellow), Color::LightYellow);
|
||||
assert_eq!(
|
||||
Color::from_termion(tcolor::LightMagenta),
|
||||
Color::LightMagenta
|
||||
);
|
||||
assert_eq!(Color::from_termion(tcolor::LightCyan), Color::LightCyan);
|
||||
assert_eq!(Color::from_termion(tcolor::LightWhite), Color::White);
|
||||
assert_eq!(
|
||||
Color::from_termion(tcolor::AnsiValue(31)),
|
||||
Color::Indexed(31)
|
||||
);
|
||||
assert_eq!(
|
||||
Color::from_termion(tcolor::Rgb(1, 2, 3)),
|
||||
Color::Rgb(1, 2, 3)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -493,38 +584,62 @@ mod tests {
|
||||
use tc::Bg;
|
||||
use tcolor as tc;
|
||||
|
||||
assert_eq!(Style::from(Bg(tc::Reset)), Style::new().bg(Color::Reset));
|
||||
assert_eq!(Style::from(Bg(tc::Black)), Style::new().on_black());
|
||||
assert_eq!(Style::from(Bg(tc::Red)), Style::new().on_red());
|
||||
assert_eq!(Style::from(Bg(tc::Green)), Style::new().on_green());
|
||||
assert_eq!(Style::from(Bg(tc::Yellow)), Style::new().on_yellow());
|
||||
assert_eq!(Style::from(Bg(tc::Blue)), Style::new().on_blue());
|
||||
assert_eq!(Style::from(Bg(tc::Magenta)), Style::new().on_magenta());
|
||||
assert_eq!(Style::from(Bg(tc::Cyan)), Style::new().on_cyan());
|
||||
assert_eq!(Style::from(Bg(tc::White)), Style::new().on_gray());
|
||||
assert_eq!(Style::from(Bg(tc::LightBlack)), Style::new().on_dark_gray());
|
||||
assert_eq!(Style::from(Bg(tc::LightRed)), Style::new().on_light_red());
|
||||
assert_eq!(
|
||||
Style::from(Bg(tc::LightGreen)),
|
||||
Style::from_termion(Bg(tc::Reset)),
|
||||
Style::new().bg(Color::Reset)
|
||||
);
|
||||
assert_eq!(Style::from_termion(Bg(tc::Black)), Style::new().on_black());
|
||||
assert_eq!(Style::from_termion(Bg(tc::Red)), Style::new().on_red());
|
||||
assert_eq!(Style::from_termion(Bg(tc::Green)), Style::new().on_green());
|
||||
assert_eq!(
|
||||
Style::from_termion(Bg(tc::Yellow)),
|
||||
Style::new().on_yellow()
|
||||
);
|
||||
assert_eq!(Style::from_termion(Bg(tc::Blue)), Style::new().on_blue());
|
||||
assert_eq!(
|
||||
Style::from_termion(Bg(tc::Magenta)),
|
||||
Style::new().on_magenta()
|
||||
);
|
||||
assert_eq!(Style::from_termion(Bg(tc::Cyan)), Style::new().on_cyan());
|
||||
assert_eq!(Style::from_termion(Bg(tc::White)), Style::new().on_gray());
|
||||
assert_eq!(
|
||||
Style::from_termion(Bg(tc::LightBlack)),
|
||||
Style::new().on_dark_gray()
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from_termion(Bg(tc::LightRed)),
|
||||
Style::new().on_light_red()
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from_termion(Bg(tc::LightGreen)),
|
||||
Style::new().on_light_green()
|
||||
);
|
||||
assert_eq!(Style::from(Bg(tc::LightBlue)), Style::new().on_light_blue());
|
||||
assert_eq!(
|
||||
Style::from(Bg(tc::LightYellow)),
|
||||
Style::from_termion(Bg(tc::LightBlue)),
|
||||
Style::new().on_light_blue()
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from_termion(Bg(tc::LightYellow)),
|
||||
Style::new().on_light_yellow()
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from(Bg(tc::LightMagenta)),
|
||||
Style::from_termion(Bg(tc::LightMagenta)),
|
||||
Style::new().on_light_magenta()
|
||||
);
|
||||
assert_eq!(Style::from(Bg(tc::LightCyan)), Style::new().on_light_cyan());
|
||||
assert_eq!(Style::from(Bg(tc::LightWhite)), Style::new().on_white());
|
||||
assert_eq!(
|
||||
Style::from(Bg(tc::AnsiValue(31))),
|
||||
Style::from_termion(Bg(tc::LightCyan)),
|
||||
Style::new().on_light_cyan()
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from_termion(Bg(tc::LightWhite)),
|
||||
Style::new().on_white()
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from_termion(Bg(tc::AnsiValue(31))),
|
||||
Style::new().bg(Color::Indexed(31))
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from(Bg(tc::Rgb(1, 2, 3))),
|
||||
Style::from_termion(Bg(tc::Rgb(1, 2, 3))),
|
||||
Style::new().bg(Color::Rgb(1, 2, 3))
|
||||
);
|
||||
}
|
||||
@@ -534,48 +649,78 @@ mod tests {
|
||||
use tc::Fg;
|
||||
use tcolor as tc;
|
||||
|
||||
assert_eq!(Style::from(Fg(tc::Reset)), Style::new().fg(Color::Reset));
|
||||
assert_eq!(Style::from(Fg(tc::Black)), Style::new().black());
|
||||
assert_eq!(Style::from(Fg(tc::Red)), Style::new().red());
|
||||
assert_eq!(Style::from(Fg(tc::Green)), Style::new().green());
|
||||
assert_eq!(Style::from(Fg(tc::Yellow)), Style::new().yellow());
|
||||
assert_eq!(Style::from(Fg(tc::Blue)), Style::default().blue());
|
||||
assert_eq!(Style::from(Fg(tc::Magenta)), Style::default().magenta());
|
||||
assert_eq!(Style::from(Fg(tc::Cyan)), Style::default().cyan());
|
||||
assert_eq!(Style::from(Fg(tc::White)), Style::default().gray());
|
||||
assert_eq!(Style::from(Fg(tc::LightBlack)), Style::new().dark_gray());
|
||||
assert_eq!(Style::from(Fg(tc::LightRed)), Style::new().light_red());
|
||||
assert_eq!(Style::from(Fg(tc::LightGreen)), Style::new().light_green());
|
||||
assert_eq!(Style::from(Fg(tc::LightBlue)), Style::new().light_blue());
|
||||
assert_eq!(
|
||||
Style::from(Fg(tc::LightYellow)),
|
||||
Style::from_termion(Fg(tc::Reset)),
|
||||
Style::new().fg(Color::Reset)
|
||||
);
|
||||
assert_eq!(Style::from_termion(Fg(tc::Black)), Style::new().black());
|
||||
assert_eq!(Style::from_termion(Fg(tc::Red)), Style::new().red());
|
||||
assert_eq!(Style::from_termion(Fg(tc::Green)), Style::new().green());
|
||||
assert_eq!(Style::from_termion(Fg(tc::Yellow)), Style::new().yellow());
|
||||
assert_eq!(Style::from_termion(Fg(tc::Blue)), Style::default().blue());
|
||||
assert_eq!(
|
||||
Style::from_termion(Fg(tc::Magenta)),
|
||||
Style::default().magenta()
|
||||
);
|
||||
assert_eq!(Style::from_termion(Fg(tc::Cyan)), Style::default().cyan());
|
||||
assert_eq!(Style::from_termion(Fg(tc::White)), Style::default().gray());
|
||||
assert_eq!(
|
||||
Style::from_termion(Fg(tc::LightBlack)),
|
||||
Style::new().dark_gray()
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from_termion(Fg(tc::LightRed)),
|
||||
Style::new().light_red()
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from_termion(Fg(tc::LightGreen)),
|
||||
Style::new().light_green()
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from_termion(Fg(tc::LightBlue)),
|
||||
Style::new().light_blue()
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from_termion(Fg(tc::LightYellow)),
|
||||
Style::new().light_yellow()
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from(Fg(tc::LightMagenta)),
|
||||
Style::from_termion(Fg(tc::LightMagenta)),
|
||||
Style::new().light_magenta()
|
||||
);
|
||||
assert_eq!(Style::from(Fg(tc::LightCyan)), Style::new().light_cyan());
|
||||
assert_eq!(Style::from(Fg(tc::LightWhite)), Style::new().white());
|
||||
assert_eq!(
|
||||
Style::from(Fg(tc::AnsiValue(31))),
|
||||
Style::from_termion(Fg(tc::LightCyan)),
|
||||
Style::new().light_cyan()
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from_termion(Fg(tc::LightWhite)),
|
||||
Style::new().white()
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from_termion(Fg(tc::AnsiValue(31))),
|
||||
Style::default().fg(Color::Indexed(31))
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from(Fg(tc::Rgb(1, 2, 3))),
|
||||
Style::from_termion(Fg(tc::Rgb(1, 2, 3))),
|
||||
Style::default().fg(Color::Rgb(1, 2, 3))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_termion_style() {
|
||||
assert_eq!(Modifier::from(tstyle::Invert), Modifier::REVERSED);
|
||||
assert_eq!(Modifier::from(tstyle::Bold), Modifier::BOLD);
|
||||
assert_eq!(Modifier::from(tstyle::Italic), Modifier::ITALIC);
|
||||
assert_eq!(Modifier::from(tstyle::Underline), Modifier::UNDERLINED);
|
||||
assert_eq!(Modifier::from(tstyle::Faint), Modifier::DIM);
|
||||
assert_eq!(Modifier::from(tstyle::CrossedOut), Modifier::CROSSED_OUT);
|
||||
assert_eq!(Modifier::from(tstyle::Blink), Modifier::SLOW_BLINK);
|
||||
assert_eq!(Modifier::from(tstyle::Reset), Modifier::empty());
|
||||
assert_eq!(Modifier::from_termion(tstyle::Invert), Modifier::REVERSED);
|
||||
assert_eq!(Modifier::from_termion(tstyle::Bold), Modifier::BOLD);
|
||||
assert_eq!(Modifier::from_termion(tstyle::Italic), Modifier::ITALIC);
|
||||
assert_eq!(
|
||||
Modifier::from_termion(tstyle::Underline),
|
||||
Modifier::UNDERLINED
|
||||
);
|
||||
assert_eq!(Modifier::from_termion(tstyle::Faint), Modifier::DIM);
|
||||
assert_eq!(
|
||||
Modifier::from_termion(tstyle::CrossedOut),
|
||||
Modifier::CROSSED_OUT
|
||||
);
|
||||
assert_eq!(Modifier::from_termion(tstyle::Blink), Modifier::SLOW_BLINK);
|
||||
assert_eq!(Modifier::from_termion(tstyle::Reset), Modifier::empty());
|
||||
}
|
||||
}
|
||||
39
ratatui-termwiz/Cargo.toml
Normal file
39
ratatui-termwiz/Cargo.toml
Normal file
@@ -0,0 +1,39 @@
|
||||
[package]
|
||||
name = "ratatui-termwiz"
|
||||
version = "0.1.0-alpha.0"
|
||||
description = "Termwiz backend for the Ratatui Terminal UI library."
|
||||
documentation = "https://docs.rs/ratatui-termwiz/"
|
||||
readme = "README.md"
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
keywords.workspace = true
|
||||
categories.workspace = true
|
||||
license.workspace = true
|
||||
exclude.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
## Enables the backend code that sets the underline color.
|
||||
## Underline color is not supported on Windows 7.
|
||||
underline-color = []
|
||||
|
||||
## Use terminal scrolling regions to make Terminal::insert_before less prone to flickering.
|
||||
scrolling-regions = ["ratatui-core/scrolling-regions"]
|
||||
|
||||
[dependencies]
|
||||
document-features = { workspace = true, optional = true }
|
||||
ratatui-core = { workspace = true }
|
||||
termwiz.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
ratatui = { path = "../ratatui", features = ["termwiz"] }
|
||||
rstest.workspace = true
|
||||
11
ratatui-termwiz/README.md
Normal file
11
ratatui-termwiz/README.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Ratatui-termwiz
|
||||
|
||||
<!-- cargo-rdme start -->
|
||||
|
||||
This module provides the [`TermwizBackend`] implementation for the [`Backend`] trait. It uses
|
||||
the [Termwiz] crate to interact with the terminal.
|
||||
|
||||
[`Backend`]: trait.Backend.html
|
||||
[Termwiz]: https://crates.io/crates/termwiz
|
||||
|
||||
<!-- cargo-rdme end -->
|
||||
858
ratatui-termwiz/src/lib.rs
Normal file
858
ratatui-termwiz/src/lib.rs
Normal file
@@ -0,0 +1,858 @@
|
||||
// show the feature flags in the generated documentation
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![doc(
|
||||
html_logo_url = "https://raw.githubusercontent.com/ratatui/ratatui/main/assets/logo.png",
|
||||
html_favicon_url = "https://raw.githubusercontent.com/ratatui/ratatui/main/assets/favicon.ico"
|
||||
)]
|
||||
#![warn(missing_docs)]
|
||||
//! This module provides the [`TermwizBackend`] implementation for the [`Backend`] trait. It uses
|
||||
//! the [Termwiz] crate to interact with the terminal.
|
||||
//!
|
||||
//! [`Backend`]: trait.Backend.html
|
||||
//! [Termwiz]: https://crates.io/crates/termwiz
|
||||
#![cfg_attr(feature = "document-features", doc = "\n## Features")]
|
||||
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
|
||||
|
||||
use std::{error::Error, io};
|
||||
|
||||
use ratatui_core::{
|
||||
backend::{Backend, WindowSize},
|
||||
buffer::Cell,
|
||||
layout::{Position, Size},
|
||||
style::{Color, Modifier, Style},
|
||||
};
|
||||
pub use termwiz;
|
||||
use termwiz::{
|
||||
caps::Capabilities,
|
||||
cell::{AttributeChange, Blink, CellAttributes, Intensity, Underline},
|
||||
color::{AnsiColor, ColorAttribute, ColorSpec, LinearRgba, RgbColor, SrgbaTuple},
|
||||
surface::{Change, CursorVisibility, Position as TermwizPosition},
|
||||
terminal::{buffered::BufferedTerminal, ScreenSize, SystemTerminal, Terminal},
|
||||
};
|
||||
|
||||
/// A [`Backend`] implementation that uses [Termwiz] to render to the terminal.
|
||||
///
|
||||
/// The `TermwizBackend` struct is a wrapper around a [`BufferedTerminal`], which is used to send
|
||||
/// commands to the terminal. It provides methods for drawing content, manipulating the cursor, and
|
||||
/// clearing the terminal screen.
|
||||
///
|
||||
/// Most applications should not call the methods on `TermwizBackend` directly, but will instead
|
||||
/// use the [`Terminal`] struct, which provides a more ergonomic interface.
|
||||
///
|
||||
/// This backend automatically enables raw mode and switches to the alternate screen when it is
|
||||
/// created using the [`TermwizBackend::new`] method (and disables raw mode and returns to the main
|
||||
/// screen when dropped). Use the [`TermwizBackend::with_buffered_terminal`] to create a new
|
||||
/// instance with a custom [`BufferedTerminal`] if this is not desired.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use ratatui::{backend::TermwizBackend, Terminal};
|
||||
///
|
||||
/// let backend = TermwizBackend::new()?;
|
||||
/// let mut terminal = Terminal::new(backend)?;
|
||||
///
|
||||
/// terminal.clear()?;
|
||||
/// terminal.draw(|frame| {
|
||||
/// // -- snip --
|
||||
/// })?;
|
||||
/// # std::result::Result::Ok::<(), Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
///
|
||||
/// See the the [Examples] directory for more examples. See the [`backend`] module documentation
|
||||
/// for more details on raw mode and alternate screen.
|
||||
///
|
||||
/// [`backend`]: ratatui_core::backend
|
||||
/// [`Terminal`]: https://docs.rs/ratatui/latest/ratatui/struct.Terminal.html
|
||||
/// [`BufferedTerminal`]: termwiz::terminal::buffered::BufferedTerminal
|
||||
/// [Termwiz]: https://crates.io/crates/termwiz
|
||||
/// [Examples]: https://github.com/ratatui/ratatui/tree/main/ratatui/examples/README.md
|
||||
pub struct TermwizBackend {
|
||||
buffered_terminal: BufferedTerminal<SystemTerminal>,
|
||||
}
|
||||
|
||||
impl TermwizBackend {
|
||||
/// Creates a new Termwiz backend instance.
|
||||
///
|
||||
/// The backend will automatically enable raw mode and enter the alternate screen.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if unable to do any of the following:
|
||||
/// - query the terminal capabilities.
|
||||
/// - enter raw mode.
|
||||
/// - enter the alternate screen.
|
||||
/// - create the system or buffered terminal.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use ratatui::backend::TermwizBackend;
|
||||
///
|
||||
/// let backend = TermwizBackend::new()?;
|
||||
/// # Ok::<(), Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
pub fn new() -> Result<Self, Box<dyn Error>> {
|
||||
let mut buffered_terminal =
|
||||
BufferedTerminal::new(SystemTerminal::new(Capabilities::new_from_env()?)?)?;
|
||||
buffered_terminal.terminal().set_raw_mode()?;
|
||||
buffered_terminal.terminal().enter_alternate_screen()?;
|
||||
Ok(Self { buffered_terminal })
|
||||
}
|
||||
|
||||
/// Creates a new Termwiz backend instance with the given buffered terminal.
|
||||
pub const fn with_buffered_terminal(instance: BufferedTerminal<SystemTerminal>) -> Self {
|
||||
Self {
|
||||
buffered_terminal: instance,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a reference to the buffered terminal used by the backend.
|
||||
pub const fn buffered_terminal(&self) -> &BufferedTerminal<SystemTerminal> {
|
||||
&self.buffered_terminal
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the buffered terminal used by the backend.
|
||||
pub fn buffered_terminal_mut(&mut self) -> &mut BufferedTerminal<SystemTerminal> {
|
||||
&mut self.buffered_terminal
|
||||
}
|
||||
}
|
||||
|
||||
impl Backend for TermwizBackend {
|
||||
fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
|
||||
where
|
||||
I: Iterator<Item = (u16, u16, &'a Cell)>,
|
||||
{
|
||||
for (x, y, cell) in content {
|
||||
self.buffered_terminal.add_changes(vec![
|
||||
Change::CursorPosition {
|
||||
x: TermwizPosition::Absolute(x as usize),
|
||||
y: TermwizPosition::Absolute(y as usize),
|
||||
},
|
||||
Change::Attribute(AttributeChange::Foreground(cell.fg.into_termwiz())),
|
||||
Change::Attribute(AttributeChange::Background(cell.bg.into_termwiz())),
|
||||
]);
|
||||
|
||||
self.buffered_terminal
|
||||
.add_change(Change::Attribute(AttributeChange::Intensity(
|
||||
if cell.modifier.contains(Modifier::BOLD) {
|
||||
Intensity::Bold
|
||||
} else if cell.modifier.contains(Modifier::DIM) {
|
||||
Intensity::Half
|
||||
} else {
|
||||
Intensity::Normal
|
||||
},
|
||||
)));
|
||||
|
||||
self.buffered_terminal
|
||||
.add_change(Change::Attribute(AttributeChange::Italic(
|
||||
cell.modifier.contains(Modifier::ITALIC),
|
||||
)));
|
||||
|
||||
self.buffered_terminal
|
||||
.add_change(Change::Attribute(AttributeChange::Underline(
|
||||
if cell.modifier.contains(Modifier::UNDERLINED) {
|
||||
Underline::Single
|
||||
} else {
|
||||
Underline::None
|
||||
},
|
||||
)));
|
||||
|
||||
self.buffered_terminal
|
||||
.add_change(Change::Attribute(AttributeChange::Reverse(
|
||||
cell.modifier.contains(Modifier::REVERSED),
|
||||
)));
|
||||
|
||||
self.buffered_terminal
|
||||
.add_change(Change::Attribute(AttributeChange::Invisible(
|
||||
cell.modifier.contains(Modifier::HIDDEN),
|
||||
)));
|
||||
|
||||
self.buffered_terminal
|
||||
.add_change(Change::Attribute(AttributeChange::StrikeThrough(
|
||||
cell.modifier.contains(Modifier::CROSSED_OUT),
|
||||
)));
|
||||
|
||||
self.buffered_terminal
|
||||
.add_change(Change::Attribute(AttributeChange::Blink(
|
||||
if cell.modifier.contains(Modifier::SLOW_BLINK) {
|
||||
Blink::Slow
|
||||
} else if cell.modifier.contains(Modifier::RAPID_BLINK) {
|
||||
Blink::Rapid
|
||||
} else {
|
||||
Blink::None
|
||||
},
|
||||
)));
|
||||
|
||||
self.buffered_terminal.add_change(cell.symbol());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn hide_cursor(&mut self) -> io::Result<()> {
|
||||
self.buffered_terminal
|
||||
.add_change(Change::CursorVisibility(CursorVisibility::Hidden));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn show_cursor(&mut self) -> io::Result<()> {
|
||||
self.buffered_terminal
|
||||
.add_change(Change::CursorVisibility(CursorVisibility::Visible));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_cursor_position(&mut self) -> io::Result<Position> {
|
||||
let (x, y) = self.buffered_terminal.cursor_position();
|
||||
Ok(Position::new(x as u16, y as u16))
|
||||
}
|
||||
|
||||
fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()> {
|
||||
let Position { x, y } = position.into();
|
||||
self.buffered_terminal.add_change(Change::CursorPosition {
|
||||
x: TermwizPosition::Absolute(x as usize),
|
||||
y: TermwizPosition::Absolute(y as usize),
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear(&mut self) -> io::Result<()> {
|
||||
self.buffered_terminal
|
||||
.add_change(Change::ClearScreen(termwiz::color::ColorAttribute::Default));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn size(&self) -> io::Result<Size> {
|
||||
let (cols, rows) = self.buffered_terminal.dimensions();
|
||||
Ok(Size::new(u16_max(cols), u16_max(rows)))
|
||||
}
|
||||
|
||||
fn window_size(&mut self) -> io::Result<WindowSize> {
|
||||
let ScreenSize {
|
||||
cols,
|
||||
rows,
|
||||
xpixel,
|
||||
ypixel,
|
||||
} = self
|
||||
.buffered_terminal
|
||||
.terminal()
|
||||
.get_screen_size()
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
|
||||
Ok(WindowSize {
|
||||
columns_rows: Size {
|
||||
width: u16_max(cols),
|
||||
height: u16_max(rows),
|
||||
},
|
||||
pixels: Size {
|
||||
width: u16_max(xpixel),
|
||||
height: u16_max(ypixel),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.buffered_terminal
|
||||
.flush()
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "scrolling-regions")]
|
||||
fn scroll_region_up(&mut self, region: std::ops::Range<u16>, amount: u16) -> io::Result<()> {
|
||||
// termwiz doesn't have a command to just set the scrolling region. Instead, setting the
|
||||
// scrolling region and scrolling are combined. However, this has the side-effect of
|
||||
// leaving the scrolling region set. To reset the scrolling region, termwiz advises one to
|
||||
// make a scrolling-region scroll command that contains the entire screen, but scrolls by 0
|
||||
// lines. See [`Change::ScrollRegionUp`] for more details.
|
||||
let (_, rows) = self.buffered_terminal.dimensions();
|
||||
self.buffered_terminal.add_changes(vec![
|
||||
Change::ScrollRegionUp {
|
||||
first_row: region.start as usize,
|
||||
region_size: region.len(),
|
||||
scroll_count: amount as usize,
|
||||
},
|
||||
Change::ScrollRegionUp {
|
||||
first_row: 0,
|
||||
region_size: rows,
|
||||
scroll_count: 0,
|
||||
},
|
||||
]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "scrolling-regions")]
|
||||
fn scroll_region_down(&mut self, region: std::ops::Range<u16>, amount: u16) -> io::Result<()> {
|
||||
// termwiz doesn't have a command to just set the scrolling region. Instead, setting the
|
||||
// scrolling region and scrolling are combined. However, this has the side-effect of
|
||||
// leaving the scrolling region set. To reset the scrolling region, termwiz advises one to
|
||||
// make a scrolling-region scroll command that contains the entire screen, but scrolls by 0
|
||||
// lines. See [`Change::ScrollRegionDown`] for more details.
|
||||
let (_, rows) = self.buffered_terminal.dimensions();
|
||||
self.buffered_terminal.add_changes(vec![
|
||||
Change::ScrollRegionDown {
|
||||
first_row: region.start as usize,
|
||||
region_size: region.len(),
|
||||
scroll_count: amount as usize,
|
||||
},
|
||||
Change::ScrollRegionDown {
|
||||
first_row: 0,
|
||||
region_size: rows,
|
||||
scroll_count: 0,
|
||||
},
|
||||
]);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for converting types from Termwiz to Ratatui.
|
||||
///
|
||||
/// This trait replaces the `From` trait for converting types from Termwiz to Ratatui. It is
|
||||
/// necessary because the `From` trait is not implemented for types defined in external crates.
|
||||
pub trait FromTermwiz<T> {
|
||||
/// Converts the given Termwiz type to the Ratatui type.
|
||||
fn from_termwiz(termwiz: T) -> Self;
|
||||
}
|
||||
|
||||
/// A trait for converting types from Ratatui to Termwiz.
|
||||
///
|
||||
/// This trait replaces the `Into` trait for converting types from Ratatui to Termwiz. It is
|
||||
/// necessary because the `Into` trait is not implemented for types defined in external crates.
|
||||
pub trait IntoTermwiz<T> {
|
||||
/// Converts the given Ratatui type to the Termwiz type.
|
||||
fn into_termwiz(self) -> T;
|
||||
}
|
||||
|
||||
/// A replacement for the `Into` trait for converting types from Ratatui to Termwiz.
|
||||
///
|
||||
/// This trait is necessary because the `Into` trait is not implemented for types defined in
|
||||
/// external crates.
|
||||
///
|
||||
/// A blanket implementation is provided for all types that implement `FromTermwiz`.
|
||||
///
|
||||
/// This trait is private to the module as it would otherwise conflict with the other backend
|
||||
/// modules. It is mainly used to avoid rewriting all the `.into()` calls in this module.
|
||||
trait IntoRatatui<R> {
|
||||
fn into_ratatui(self) -> R;
|
||||
}
|
||||
|
||||
impl<C, R: FromTermwiz<C>> IntoRatatui<R> for C {
|
||||
fn into_ratatui(self) -> R {
|
||||
R::from_termwiz(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromTermwiz<CellAttributes> for Style {
|
||||
fn from_termwiz(value: CellAttributes) -> Self {
|
||||
let mut style = Self::new()
|
||||
.add_modifier(value.intensity().into_ratatui())
|
||||
.add_modifier(value.underline().into_ratatui())
|
||||
.add_modifier(value.blink().into_ratatui());
|
||||
|
||||
if value.italic() {
|
||||
style.add_modifier |= Modifier::ITALIC;
|
||||
}
|
||||
if value.reverse() {
|
||||
style.add_modifier |= Modifier::REVERSED;
|
||||
}
|
||||
if value.strikethrough() {
|
||||
style.add_modifier |= Modifier::CROSSED_OUT;
|
||||
}
|
||||
if value.invisible() {
|
||||
style.add_modifier |= Modifier::HIDDEN;
|
||||
}
|
||||
|
||||
style.fg = Some(value.foreground().into_ratatui());
|
||||
style.bg = Some(value.background().into_ratatui());
|
||||
#[cfg(feature = "underline-color")]
|
||||
{
|
||||
style.underline_color = Some(value.underline_color().into_ratatui());
|
||||
}
|
||||
|
||||
style
|
||||
}
|
||||
}
|
||||
|
||||
impl FromTermwiz<Intensity> for Modifier {
|
||||
fn from_termwiz(value: Intensity) -> Self {
|
||||
match value {
|
||||
Intensity::Normal => Self::empty(),
|
||||
Intensity::Bold => Self::BOLD,
|
||||
Intensity::Half => Self::DIM,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromTermwiz<Underline> for Modifier {
|
||||
fn from_termwiz(value: Underline) -> Self {
|
||||
match value {
|
||||
Underline::None => Self::empty(),
|
||||
_ => Self::UNDERLINED,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromTermwiz<Blink> for Modifier {
|
||||
fn from_termwiz(value: Blink) -> Self {
|
||||
match value {
|
||||
Blink::None => Self::empty(),
|
||||
Blink::Slow => Self::SLOW_BLINK,
|
||||
Blink::Rapid => Self::RAPID_BLINK,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoTermwiz<ColorAttribute> for Color {
|
||||
fn into_termwiz(self) -> ColorAttribute {
|
||||
match self {
|
||||
Self::Reset => ColorAttribute::Default,
|
||||
Self::Black => AnsiColor::Black.into(),
|
||||
Self::DarkGray => AnsiColor::Grey.into(),
|
||||
Self::Gray => AnsiColor::Silver.into(),
|
||||
Self::Red => AnsiColor::Maroon.into(),
|
||||
Self::LightRed => AnsiColor::Red.into(),
|
||||
Self::Green => AnsiColor::Green.into(),
|
||||
Self::LightGreen => AnsiColor::Lime.into(),
|
||||
Self::Yellow => AnsiColor::Olive.into(),
|
||||
Self::LightYellow => AnsiColor::Yellow.into(),
|
||||
Self::Magenta => AnsiColor::Purple.into(),
|
||||
Self::LightMagenta => AnsiColor::Fuchsia.into(),
|
||||
Self::Cyan => AnsiColor::Teal.into(),
|
||||
Self::LightCyan => AnsiColor::Aqua.into(),
|
||||
Self::White => AnsiColor::White.into(),
|
||||
Self::Blue => AnsiColor::Navy.into(),
|
||||
Self::LightBlue => AnsiColor::Blue.into(),
|
||||
Self::Indexed(i) => ColorAttribute::PaletteIndex(i),
|
||||
Self::Rgb(r, g, b) => {
|
||||
ColorAttribute::TrueColorWithDefaultFallback(SrgbaTuple::from((r, g, b)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromTermwiz<AnsiColor> for Color {
|
||||
fn from_termwiz(value: AnsiColor) -> Self {
|
||||
match value {
|
||||
AnsiColor::Black => Self::Black,
|
||||
AnsiColor::Grey => Self::DarkGray,
|
||||
AnsiColor::Silver => Self::Gray,
|
||||
AnsiColor::Maroon => Self::Red,
|
||||
AnsiColor::Red => Self::LightRed,
|
||||
AnsiColor::Green => Self::Green,
|
||||
AnsiColor::Lime => Self::LightGreen,
|
||||
AnsiColor::Olive => Self::Yellow,
|
||||
AnsiColor::Yellow => Self::LightYellow,
|
||||
AnsiColor::Purple => Self::Magenta,
|
||||
AnsiColor::Fuchsia => Self::LightMagenta,
|
||||
AnsiColor::Teal => Self::Cyan,
|
||||
AnsiColor::Aqua => Self::LightCyan,
|
||||
AnsiColor::White => Self::White,
|
||||
AnsiColor::Navy => Self::Blue,
|
||||
AnsiColor::Blue => Self::LightBlue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromTermwiz<ColorAttribute> for Color {
|
||||
fn from_termwiz(value: ColorAttribute) -> Self {
|
||||
match value {
|
||||
ColorAttribute::TrueColorWithDefaultFallback(srgba)
|
||||
| ColorAttribute::TrueColorWithPaletteFallback(srgba, _) => srgba.into_ratatui(),
|
||||
ColorAttribute::PaletteIndex(i) => Self::Indexed(i),
|
||||
ColorAttribute::Default => Self::Reset,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromTermwiz<ColorSpec> for Color {
|
||||
fn from_termwiz(value: ColorSpec) -> Self {
|
||||
match value {
|
||||
ColorSpec::Default => Self::Reset,
|
||||
ColorSpec::PaletteIndex(i) => Self::Indexed(i),
|
||||
ColorSpec::TrueColor(srgba) => srgba.into_ratatui(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromTermwiz<SrgbaTuple> for Color {
|
||||
fn from_termwiz(value: SrgbaTuple) -> Self {
|
||||
let (r, g, b, _) = value.to_srgb_u8();
|
||||
Self::Rgb(r, g, b)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromTermwiz<RgbColor> for Color {
|
||||
fn from_termwiz(value: RgbColor) -> Self {
|
||||
let (r, g, b) = value.to_tuple_rgb8();
|
||||
Self::Rgb(r, g, b)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromTermwiz<LinearRgba> for Color {
|
||||
fn from_termwiz(value: LinearRgba) -> Self {
|
||||
value.to_srgb().into_ratatui()
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn u16_max(i: usize) -> u16 {
|
||||
u16::try_from(i).unwrap_or(u16::MAX)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
mod into_color {
|
||||
use Color as C;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn from_linear_rgba() {
|
||||
// full black + opaque
|
||||
assert_eq!(
|
||||
C::from_termwiz(LinearRgba(0., 0., 0., 1.)),
|
||||
Color::Rgb(0, 0, 0)
|
||||
);
|
||||
// full black + transparent
|
||||
assert_eq!(
|
||||
C::from_termwiz(LinearRgba(0., 0., 0., 0.)),
|
||||
Color::Rgb(0, 0, 0)
|
||||
);
|
||||
|
||||
// full white + opaque
|
||||
assert_eq!(
|
||||
C::from_termwiz(LinearRgba(1., 1., 1., 1.)),
|
||||
C::Rgb(254, 254, 254)
|
||||
);
|
||||
// full white + transparent
|
||||
assert_eq!(
|
||||
C::from_termwiz(LinearRgba(1., 1., 1., 0.)),
|
||||
C::Rgb(254, 254, 254)
|
||||
);
|
||||
|
||||
// full red
|
||||
assert_eq!(
|
||||
C::from_termwiz(LinearRgba(1., 0., 0., 1.)),
|
||||
C::Rgb(254, 0, 0)
|
||||
);
|
||||
// full green
|
||||
assert_eq!(
|
||||
C::from_termwiz(LinearRgba(0., 1., 0., 1.)),
|
||||
C::Rgb(0, 254, 0)
|
||||
);
|
||||
// full blue
|
||||
assert_eq!(
|
||||
C::from_termwiz(LinearRgba(0., 0., 1., 1.)),
|
||||
C::Rgb(0, 0, 254)
|
||||
);
|
||||
|
||||
// See https://stackoverflow.com/questions/12524623/what-are-the-practical-differences-when-working-with-colors-in-a-linear-vs-a-no
|
||||
// for an explanation
|
||||
|
||||
// half red
|
||||
assert_eq!(
|
||||
C::from_termwiz(LinearRgba(0.214, 0., 0., 1.)),
|
||||
C::Rgb(127, 0, 0)
|
||||
);
|
||||
// half green
|
||||
assert_eq!(
|
||||
C::from_termwiz(LinearRgba(0., 0.214, 0., 1.)),
|
||||
C::Rgb(0, 127, 0)
|
||||
);
|
||||
// half blue
|
||||
assert_eq!(
|
||||
C::from_termwiz(LinearRgba(0., 0., 0.214, 1.)),
|
||||
C::Rgb(0, 0, 127)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_srgba() {
|
||||
// full black + opaque
|
||||
assert_eq!(
|
||||
C::from_termwiz(SrgbaTuple(0., 0., 0., 1.)),
|
||||
Color::Rgb(0, 0, 0)
|
||||
);
|
||||
// full black + transparent
|
||||
assert_eq!(
|
||||
C::from_termwiz(SrgbaTuple(0., 0., 0., 0.)),
|
||||
Color::Rgb(0, 0, 0)
|
||||
);
|
||||
|
||||
// full white + opaque
|
||||
assert_eq!(
|
||||
C::from_termwiz(SrgbaTuple(1., 1., 1., 1.)),
|
||||
C::Rgb(255, 255, 255)
|
||||
);
|
||||
// full white + transparent
|
||||
assert_eq!(
|
||||
C::from_termwiz(SrgbaTuple(1., 1., 1., 0.)),
|
||||
C::Rgb(255, 255, 255)
|
||||
);
|
||||
|
||||
// full red
|
||||
assert_eq!(
|
||||
C::from_termwiz(SrgbaTuple(1., 0., 0., 1.)),
|
||||
C::Rgb(255, 0, 0)
|
||||
);
|
||||
// full green
|
||||
assert_eq!(
|
||||
C::from_termwiz(SrgbaTuple(0., 1., 0., 1.)),
|
||||
C::Rgb(0, 255, 0)
|
||||
);
|
||||
// full blue
|
||||
assert_eq!(
|
||||
C::from_termwiz(SrgbaTuple(0., 0., 1., 1.)),
|
||||
C::Rgb(0, 0, 255)
|
||||
);
|
||||
|
||||
// half red
|
||||
assert_eq!(
|
||||
C::from_termwiz(SrgbaTuple(0.5, 0., 0., 1.)),
|
||||
C::Rgb(127, 0, 0)
|
||||
);
|
||||
// half green
|
||||
assert_eq!(
|
||||
C::from_termwiz(SrgbaTuple(0., 0.5, 0., 1.)),
|
||||
C::Rgb(0, 127, 0)
|
||||
);
|
||||
// half blue
|
||||
assert_eq!(
|
||||
C::from_termwiz(SrgbaTuple(0., 0., 0.5, 1.)),
|
||||
C::Rgb(0, 0, 127)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_rgbcolor() {
|
||||
// full black
|
||||
assert_eq!(
|
||||
C::from_termwiz(RgbColor::new_8bpc(0, 0, 0)),
|
||||
Color::Rgb(0, 0, 0)
|
||||
);
|
||||
// full white
|
||||
assert_eq!(
|
||||
C::from_termwiz(RgbColor::new_8bpc(255, 255, 255)),
|
||||
C::Rgb(255, 255, 255)
|
||||
);
|
||||
|
||||
// full red
|
||||
assert_eq!(
|
||||
C::from_termwiz(RgbColor::new_8bpc(255, 0, 0)),
|
||||
C::Rgb(255, 0, 0)
|
||||
);
|
||||
// full green
|
||||
assert_eq!(
|
||||
C::from_termwiz(RgbColor::new_8bpc(0, 255, 0)),
|
||||
C::Rgb(0, 255, 0)
|
||||
);
|
||||
// full blue
|
||||
assert_eq!(
|
||||
C::from_termwiz(RgbColor::new_8bpc(0, 0, 255)),
|
||||
C::Rgb(0, 0, 255)
|
||||
);
|
||||
|
||||
// half red
|
||||
assert_eq!(
|
||||
C::from_termwiz(RgbColor::new_8bpc(127, 0, 0)),
|
||||
C::Rgb(127, 0, 0)
|
||||
);
|
||||
// half green
|
||||
assert_eq!(
|
||||
C::from_termwiz(RgbColor::new_8bpc(0, 127, 0)),
|
||||
C::Rgb(0, 127, 0)
|
||||
);
|
||||
// half blue
|
||||
assert_eq!(
|
||||
C::from_termwiz(RgbColor::new_8bpc(0, 0, 127)),
|
||||
C::Rgb(0, 0, 127)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_colorspec() {
|
||||
assert_eq!(C::from_termwiz(ColorSpec::Default), C::Reset);
|
||||
assert_eq!(C::from_termwiz(ColorSpec::PaletteIndex(33)), C::Indexed(33));
|
||||
assert_eq!(
|
||||
C::from_termwiz(ColorSpec::TrueColor(SrgbaTuple(0., 0., 0., 1.))),
|
||||
C::Rgb(0, 0, 0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_colorattribute() {
|
||||
assert_eq!(C::from_termwiz(ColorAttribute::Default), C::Reset);
|
||||
assert_eq!(
|
||||
C::from_termwiz(ColorAttribute::PaletteIndex(32)),
|
||||
C::Indexed(32)
|
||||
);
|
||||
assert_eq!(
|
||||
C::from_termwiz(ColorAttribute::TrueColorWithDefaultFallback(SrgbaTuple(
|
||||
0., 0., 0., 1.
|
||||
))),
|
||||
C::Rgb(0, 0, 0)
|
||||
);
|
||||
assert_eq!(
|
||||
C::from_termwiz(ColorAttribute::TrueColorWithPaletteFallback(
|
||||
SrgbaTuple(0., 0., 0., 1.),
|
||||
31
|
||||
)),
|
||||
C::Rgb(0, 0, 0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_ansicolor() {
|
||||
assert_eq!(C::from_termwiz(AnsiColor::Black), Color::Black);
|
||||
assert_eq!(C::from_termwiz(AnsiColor::Grey), Color::DarkGray);
|
||||
assert_eq!(C::from_termwiz(AnsiColor::Silver), Color::Gray);
|
||||
assert_eq!(C::from_termwiz(AnsiColor::Maroon), Color::Red);
|
||||
assert_eq!(C::from_termwiz(AnsiColor::Red), Color::LightRed);
|
||||
assert_eq!(C::from_termwiz(AnsiColor::Green), Color::Green);
|
||||
assert_eq!(C::from_termwiz(AnsiColor::Lime), Color::LightGreen);
|
||||
assert_eq!(C::from_termwiz(AnsiColor::Olive), Color::Yellow);
|
||||
assert_eq!(C::from_termwiz(AnsiColor::Yellow), Color::LightYellow);
|
||||
assert_eq!(C::from_termwiz(AnsiColor::Purple), Color::Magenta);
|
||||
assert_eq!(C::from_termwiz(AnsiColor::Fuchsia), Color::LightMagenta);
|
||||
assert_eq!(C::from_termwiz(AnsiColor::Teal), Color::Cyan);
|
||||
assert_eq!(C::from_termwiz(AnsiColor::Aqua), Color::LightCyan);
|
||||
assert_eq!(C::from_termwiz(AnsiColor::White), Color::White);
|
||||
assert_eq!(C::from_termwiz(AnsiColor::Navy), Color::Blue);
|
||||
assert_eq!(C::from_termwiz(AnsiColor::Blue), Color::LightBlue);
|
||||
}
|
||||
}
|
||||
|
||||
mod into_modifier {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn from_intensity() {
|
||||
assert_eq!(Modifier::from_termwiz(Intensity::Normal), Modifier::empty());
|
||||
assert_eq!(Modifier::from_termwiz(Intensity::Bold), Modifier::BOLD);
|
||||
assert_eq!(Modifier::from_termwiz(Intensity::Half), Modifier::DIM);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_underline() {
|
||||
assert_eq!(Modifier::from_termwiz(Underline::None), Modifier::empty());
|
||||
assert_eq!(
|
||||
Modifier::from_termwiz(Underline::Single),
|
||||
Modifier::UNDERLINED
|
||||
);
|
||||
assert_eq!(
|
||||
Modifier::from_termwiz(Underline::Double),
|
||||
Modifier::UNDERLINED
|
||||
);
|
||||
assert_eq!(
|
||||
Modifier::from_termwiz(Underline::Curly),
|
||||
Modifier::UNDERLINED
|
||||
);
|
||||
assert_eq!(
|
||||
Modifier::from_termwiz(Underline::Dashed),
|
||||
Modifier::UNDERLINED
|
||||
);
|
||||
assert_eq!(
|
||||
Modifier::from_termwiz(Underline::Dotted),
|
||||
Modifier::UNDERLINED
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_blink() {
|
||||
assert_eq!(Modifier::from_termwiz(Blink::None), Modifier::empty());
|
||||
assert_eq!(Modifier::from_termwiz(Blink::Slow), Modifier::SLOW_BLINK);
|
||||
assert_eq!(Modifier::from_termwiz(Blink::Rapid), Modifier::RAPID_BLINK);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_cell_attribute_for_style() {
|
||||
use ratatui_core::style::Stylize;
|
||||
|
||||
#[cfg(feature = "underline-color")]
|
||||
const STYLE: Style = Style::new()
|
||||
.underline_color(Color::Reset)
|
||||
.fg(Color::Reset)
|
||||
.bg(Color::Reset);
|
||||
#[cfg(not(feature = "underline-color"))]
|
||||
const STYLE: Style = Style::new().fg(Color::Reset).bg(Color::Reset);
|
||||
|
||||
// default
|
||||
assert_eq!(Style::from_termwiz(CellAttributes::default()), STYLE);
|
||||
|
||||
// foreground color
|
||||
assert_eq!(
|
||||
Style::from_termwiz(
|
||||
CellAttributes::default()
|
||||
.set_foreground(ColorAttribute::PaletteIndex(31))
|
||||
.to_owned()
|
||||
),
|
||||
STYLE.fg(Color::Indexed(31))
|
||||
);
|
||||
// background color
|
||||
assert_eq!(
|
||||
Style::from_termwiz(
|
||||
CellAttributes::default()
|
||||
.set_background(ColorAttribute::PaletteIndex(31))
|
||||
.to_owned()
|
||||
),
|
||||
STYLE.bg(Color::Indexed(31))
|
||||
);
|
||||
// underlined
|
||||
assert_eq!(
|
||||
Style::from_termwiz(
|
||||
CellAttributes::default()
|
||||
.set_underline(Underline::Single)
|
||||
.to_owned()
|
||||
),
|
||||
STYLE.underlined()
|
||||
);
|
||||
// blink
|
||||
assert_eq!(
|
||||
Style::from_termwiz(CellAttributes::default().set_blink(Blink::Slow).to_owned()),
|
||||
STYLE.slow_blink()
|
||||
);
|
||||
// intensity
|
||||
assert_eq!(
|
||||
Style::from_termwiz(
|
||||
CellAttributes::default()
|
||||
.set_intensity(Intensity::Bold)
|
||||
.to_owned()
|
||||
),
|
||||
STYLE.bold()
|
||||
);
|
||||
// italic
|
||||
assert_eq!(
|
||||
Style::from_termwiz(CellAttributes::default().set_italic(true).to_owned()),
|
||||
STYLE.italic()
|
||||
);
|
||||
// reversed
|
||||
assert_eq!(
|
||||
Style::from_termwiz(CellAttributes::default().set_reverse(true).to_owned()),
|
||||
STYLE.reversed()
|
||||
);
|
||||
// strikethrough
|
||||
assert_eq!(
|
||||
Style::from_termwiz(CellAttributes::default().set_strikethrough(true).to_owned()),
|
||||
STYLE.crossed_out()
|
||||
);
|
||||
// hidden
|
||||
assert_eq!(
|
||||
Style::from_termwiz(CellAttributes::default().set_invisible(true).to_owned()),
|
||||
STYLE.hidden()
|
||||
);
|
||||
|
||||
// underline color
|
||||
#[cfg(feature = "underline-color")]
|
||||
assert_eq!(
|
||||
Style::from_termwiz(
|
||||
CellAttributes::default()
|
||||
.set_underline_color(AnsiColor::Red)
|
||||
.to_owned()
|
||||
),
|
||||
STYLE.underline_color(Color::Indexed(9))
|
||||
);
|
||||
}
|
||||
}
|
||||
117
ratatui-widgets/Cargo.toml
Normal file
117
ratatui-widgets/Cargo.toml
Normal file
@@ -0,0 +1,117 @@
|
||||
[package]
|
||||
name = "ratatui-widgets"
|
||||
description = "A collection of Ratatui widgets for building terminal user interfaces using Ratatui."
|
||||
# Note that this started at 0.3.0 as there was a previous crate using the name `ratatui-widgets`.
|
||||
# <https://github.com/joshka/ratatui-widgets/issues/46>
|
||||
version = "0.3.0-alpha.0"
|
||||
readme = "README.md"
|
||||
authors.workspace = true
|
||||
documentation.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
keywords.workspace = true
|
||||
categories.workspace = true
|
||||
license.workspace = true
|
||||
exclude.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[features]
|
||||
default = ["all-widgets"]
|
||||
|
||||
## enables serialization and deserialization of style and color types using the [`serde`] crate.
|
||||
## This is useful if you want to save themes to a file.
|
||||
serde = ["dep:serde", "ratatui-core/serde"]
|
||||
|
||||
#! Widgets that add dependencies are gated behind feature flags to prevent unused transitive
|
||||
#! dependencies. The available features are:
|
||||
|
||||
## enables all widgets.
|
||||
all-widgets = ["calendar"]
|
||||
|
||||
## enables the [`calendar`](calendar) widget module and adds a dependency on [`time`].
|
||||
calendar = ["dep:time"]
|
||||
|
||||
## Enable all unstable features.
|
||||
unstable = ["unstable-rendered-line-info"]
|
||||
|
||||
## Enables the [`Paragraph::line_count`](paragraph::Paragraph::line_count)
|
||||
## [`Paragraph::line_width`](paragraph::Paragraph::line_width) methods
|
||||
## which are experimental and may change in the future.
|
||||
## See [Issue 293](https://github.com/ratatui/ratatui/issues/293) for more details.
|
||||
unstable-rendered-line-info = []
|
||||
|
||||
[dependencies]
|
||||
bitflags.workspace = true
|
||||
itertools.workspace = true
|
||||
indoc.workspace = true
|
||||
instability.workspace = true
|
||||
ratatui-core = { workspace = true }
|
||||
strum.workspace = true
|
||||
time = { version = "0.3.11", optional = true, features = ["local-offset"] }
|
||||
unicode-segmentation.workspace = true
|
||||
unicode-width.workspace = true
|
||||
serde = { workspace = true, optional = true }
|
||||
document-features = { workspace = true, optional = true }
|
||||
line-clipping = "0.2.1"
|
||||
|
||||
[dev-dependencies]
|
||||
color-eyre.workspace = true
|
||||
pretty_assertions.workspace = true
|
||||
ratatui = { path = "../ratatui" }
|
||||
rstest.workspace = true
|
||||
|
||||
[lints.rust]
|
||||
unsafe_code = "forbid"
|
||||
|
||||
[lints.clippy]
|
||||
cargo = { level = "warn", priority = -1 }
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
cast_possible_truncation = "allow"
|
||||
cast_possible_wrap = "allow"
|
||||
cast_precision_loss = "allow"
|
||||
cast_sign_loss = "allow"
|
||||
missing_errors_doc = "allow"
|
||||
missing_panics_doc = "allow"
|
||||
module_name_repetitions = "allow"
|
||||
must_use_candidate = "allow"
|
||||
|
||||
# we often split up a module into multiple files with the main type in a file named after the
|
||||
# module, so we want to allow this pattern
|
||||
module_inception = "allow"
|
||||
|
||||
# nursery or restricted
|
||||
as_underscore = "warn"
|
||||
deref_by_slicing = "warn"
|
||||
else_if_without_else = "warn"
|
||||
empty_line_after_doc_comments = "warn"
|
||||
equatable_if_let = "warn"
|
||||
fn_to_numeric_cast_any = "warn"
|
||||
format_push_string = "warn"
|
||||
map_err_ignore = "warn"
|
||||
missing_const_for_fn = "warn"
|
||||
mixed_read_write_in_expression = "warn"
|
||||
mod_module_files = "warn"
|
||||
needless_pass_by_ref_mut = "warn"
|
||||
needless_raw_strings = "warn"
|
||||
or_fun_call = "warn"
|
||||
redundant_type_annotations = "warn"
|
||||
rest_pat_in_fully_bound_structs = "warn"
|
||||
string_lit_chars_any = "warn"
|
||||
string_slice = "warn"
|
||||
string_to_string = "warn"
|
||||
unnecessary_self_imports = "warn"
|
||||
use_self = "warn"
|
||||
|
||||
[[example]]
|
||||
name = "barchart"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "block"
|
||||
doc-scrape-examples = true
|
||||
80
ratatui-widgets/README.md
Normal file
80
ratatui-widgets/README.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# Ratatui Widgets
|
||||
|
||||
[](https://crates.io/crates/ratatui-widgets)
|
||||
[](https://docs.rs/ratatui-widgets)
|
||||
[](../LICENSE)
|
||||
|
||||
<!-- ⚠️ DO NOT EDIT THIS FILE DIRECTLY, EDIT lib.rs AND THEN RUN `cargo rdme` to update this file. -->
|
||||
<!-- cargo-rdme start -->
|
||||
|
||||
**ratatui-widgets** contains all the widgets that were previously part of the [Ratatui] crate.
|
||||
It is meant to be used in conjunction with `ratatui`, which provides the core functionality
|
||||
for building terminal user interfaces.
|
||||
|
||||
[Ratatui]: https://crates.io/crates/ratatui
|
||||
|
||||
Most applications shouldn't need to depend directly on `ratatui-widgets`, `ratatui` crate
|
||||
re-exports all the widgets from this crate. However, if you are building a widget library that
|
||||
internally uses these widgets, or if you prefer finer grained dependencies, you may want to
|
||||
depend on this crate rather than transitively through the `ratatui` crate.
|
||||
|
||||
Previously, a crate named `ratatui-widgets` was published with some formative ideas about an
|
||||
eventual Ratatui framework. That crate is now move to [tui-framework-experiment], pending a new
|
||||
name.
|
||||
|
||||
[tui-framework-experiment]: https://crates.io/crates/tui-framework-experiment
|
||||
|
||||
## Installation
|
||||
|
||||
Run the following command to add this crate to your project:
|
||||
|
||||
```sh
|
||||
cargo add ratatui-widgets
|
||||
```
|
||||
|
||||
## Available Widgets
|
||||
|
||||
- [`BarChart`]: displays multiple datasets as bars with optional grouping.
|
||||
- [`Block`]: a basic widget that draws a block with optional borders, titles, and styles.
|
||||
- [`calendar::Monthly`]: displays a single month.
|
||||
- [`Canvas`]: draws arbitrary shapes using drawing characters.
|
||||
- [`Chart`]: displays multiple datasets as lines or scatter graphs.
|
||||
- [`Clear`]: clears the area it occupies. Useful to render over previously drawn widgets.
|
||||
- [`Gauge`]: displays progress percentage using block characters.
|
||||
- [`LineGauge`]: displays progress as a line.
|
||||
- [`List`]: displays a list of items and allows selection.
|
||||
- [`RatatuiLogo`]: displays the Ratatui logo.
|
||||
- [`Paragraph`]: displays a paragraph of optionally styled and wrapped text.
|
||||
- [`Scrollbar`]: displays a scrollbar.
|
||||
- [`Sparkline`]: displays a single dataset as a sparkline.
|
||||
- [`Table`]: displays multiple rows and columns in a grid and allows selection.
|
||||
- [`Tabs`]: displays a tab bar and allows selection.
|
||||
|
||||
[`BarChart`]: https://docs.rs/ratatui-widgets/latest/ratatui_widgets/barchart/struct.BarChart.html
|
||||
[`Block`]: https://docs.rs/ratatui-widgets/latest/ratatui_widgets/block/struct.Block.html
|
||||
[`calendar::Monthly`]: https://docs.rs/ratatui-widgets/latest/ratatui_widgets/calendar/struct.Monthly.html
|
||||
[`Canvas`]: https://docs.rs/ratatui-widgets/latest/ratatui_widgets/canvas/struct.Canvas.html
|
||||
[`Chart`]: https://docs.rs/ratatui-widgets/latest/ratatui_widgets/chart/struct.Chart.html
|
||||
[`Clear`]: https://docs.rs/ratatui-widgets/latest/ratatui_widgets/clear/struct.Clear.html
|
||||
[`Gauge`]: https://docs.rs/ratatui-widgets/latest/ratatui_widgets/gauge/struct.Gauge.html
|
||||
[`LineGauge`]: https://docs.rs/ratatui-widgets/latest/ratatui_widgets/gauge/struct.LineGauge.html
|
||||
[`List`]: https://docs.rs/ratatui-widgets/latest/ratatui_widgets/list/struct.List.html
|
||||
[`RatatuiLogo`]: https://docs.rs/ratatui-widgets/latest/ratatui_widgets/logo/struct.RatatuiLogo.html
|
||||
[`Paragraph`]: https://docs.rs/ratatui-widgets/latest/ratatui_widgets/paragraph/struct.Paragraph.html
|
||||
[`Scrollbar`]: https://docs.rs/ratatui-widgets/latest/ratatui_widgets/scrollbar/struct.Scrollbar.html
|
||||
[`Sparkline`]: https://docs.rs/ratatui-widgets/latest/ratatui_widgets/sparkline/struct.Sparkline.html
|
||||
[`Table`]: https://docs.rs/ratatui-widgets/latest/ratatui_widgets/table/struct.Table.html
|
||||
[`Tabs`]: https://docs.rs/ratatui-widgets/latest/ratatui_widgets/tabs/struct.Tabs.html
|
||||
|
||||
All these widgets are re-exported directly under `ratatui::widgets` in the `ratatui` crate.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please open an issue or submit a pull request on GitHub. For more
|
||||
details on contributing, please see the [CONTRIBUTING](CONTRIBUTING.md) document.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License. See the [LICENSE](../LICENSE) file for details.
|
||||
|
||||
<!-- cargo-rdme end -->
|
||||
91
ratatui-widgets/examples/barchart.rs
Normal file
91
ratatui-widgets/examples/barchart.rs
Normal file
@@ -0,0 +1,91 @@
|
||||
//! # [Ratatui] `BarChart` example
|
||||
//!
|
||||
//! The latest version of this example is available in the [widget examples] folder in the
|
||||
//! repository.
|
||||
//!
|
||||
//! Please note that the examples are designed to be run against the `main` branch of the Github
|
||||
//! repository. This means that you may not be able to compile with the latest release version on
|
||||
//! crates.io, or the one that you have installed locally.
|
||||
//!
|
||||
//! See the [examples readme] for more information on finding examples that match the version of the
|
||||
//! library you are using.
|
||||
//!
|
||||
//! [Ratatui]: https://github.com/ratatui/ratatui
|
||||
//! [widget examples]: https://github.com/ratatui/ratatui/blob/main/ratatui-widgets/examples
|
||||
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
||||
|
||||
use color_eyre::Result;
|
||||
use ratatui::{
|
||||
layout::{Constraint, Layout, Rect},
|
||||
style::Stylize,
|
||||
text::{Line, Span},
|
||||
widgets::{Bar, BarChart},
|
||||
DefaultTerminal, Frame,
|
||||
};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
let terminal = ratatui::init();
|
||||
let result = run(terminal);
|
||||
ratatui::restore();
|
||||
result
|
||||
}
|
||||
|
||||
/// Run the application.
|
||||
fn run(mut terminal: DefaultTerminal) -> Result<()> {
|
||||
loop {
|
||||
terminal.draw(draw)?;
|
||||
if quit_key_pressed()? {
|
||||
break Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw the UI with a title and two barcharts.
|
||||
fn draw(frame: &mut Frame) {
|
||||
let vertical = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]).spacing(1);
|
||||
let horizontal = Layout::horizontal([Constraint::Length(28), Constraint::Fill(1)]).spacing(1);
|
||||
let [top, main] = vertical.areas(frame.area());
|
||||
let [left, right] = horizontal.areas(main);
|
||||
|
||||
let title = Line::from_iter([
|
||||
Span::from("BarChart Widget").bold(),
|
||||
Span::from(" (Press 'q' to quit)"),
|
||||
]);
|
||||
frame.render_widget(title.centered(), top);
|
||||
render_vertical_barchart(frame, left);
|
||||
render_horizontal_barchart(frame, right);
|
||||
}
|
||||
|
||||
/// Render a horizontal barchart with some sample data.
|
||||
fn render_horizontal_barchart(frame: &mut Frame, area: Rect) {
|
||||
let bars = vec![
|
||||
Bar::with_label("Red", 30).red(),
|
||||
Bar::with_label("Blue", 20).blue(),
|
||||
Bar::with_label("Green", 15).green(),
|
||||
Bar::with_label("Yellow", 10).yellow(),
|
||||
];
|
||||
let chart = BarChart::horizontal(bars).bar_width(3);
|
||||
frame.render_widget(chart, area);
|
||||
}
|
||||
|
||||
/// Render a vertical barchart with some sample data.
|
||||
fn render_vertical_barchart(frame: &mut Frame, area: Rect) {
|
||||
let bars = vec![
|
||||
Bar::with_label("Red", 30).red(),
|
||||
Bar::with_label("Blue", 20).blue(),
|
||||
Bar::with_label("Green", 15).green(),
|
||||
Bar::with_label("Yellow", 10).yellow(),
|
||||
];
|
||||
let chart = BarChart::vertical(bars).bar_width(6);
|
||||
frame.render_widget(chart, area);
|
||||
}
|
||||
|
||||
/// Wait for an event and return `true` if the Esc or 'q' key is pressed.
|
||||
fn quit_key_pressed() -> Result<bool> {
|
||||
use ratatui::crossterm::event::{self, Event, KeyCode};
|
||||
match event::read()? {
|
||||
Event::Key(event) if matches!(event.code, KeyCode::Esc | KeyCode::Char('q')) => Ok(true),
|
||||
_ => Ok(false),
|
||||
}
|
||||
}
|
||||
84
ratatui-widgets/examples/block.rs
Normal file
84
ratatui-widgets/examples/block.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
//! # [Ratatui] `Block` example
|
||||
//!
|
||||
//! The latest version of this example is available in the [widget examples] folder in the
|
||||
//! repository.
|
||||
//!
|
||||
//! Please note that the examples are designed to be run against the `main` branch of the Github
|
||||
//! repository. This means that you may not be able to compile with the latest release version on
|
||||
//! crates.io, or the one that you have installed locally.
|
||||
//!
|
||||
//! See the [examples readme] for more information on finding examples that match the version of the
|
||||
//! library you are using.
|
||||
//!
|
||||
//! [Ratatui]: https://github.com/ratatui/ratatui
|
||||
//! [widget examples]: https://github.com/ratatui/ratatui/blob/main/ratatui-widgets/examples
|
||||
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
||||
|
||||
use color_eyre::Result;
|
||||
use ratatui::{
|
||||
crossterm::event::{self, Event},
|
||||
layout::{Constraint, Layout, Rect},
|
||||
style::{Style, Stylize},
|
||||
text::{Line, Span},
|
||||
widgets::{Block, BorderType},
|
||||
DefaultTerminal, Frame,
|
||||
};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
let terminal = ratatui::init();
|
||||
let result = run(terminal);
|
||||
ratatui::restore();
|
||||
result
|
||||
}
|
||||
|
||||
/// Run the application.
|
||||
fn run(mut terminal: DefaultTerminal) -> Result<()> {
|
||||
loop {
|
||||
terminal.draw(draw)?;
|
||||
if matches!(event::read()?, Event::Key(_)) {
|
||||
break Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw the UI with various blocks.
|
||||
fn draw(frame: &mut Frame) {
|
||||
let vertical = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]).spacing(1);
|
||||
let horizontal = Layout::horizontal([Constraint::Percentage(33); 3]).spacing(1);
|
||||
let [top, main] = vertical.areas(frame.area());
|
||||
let [left, middle, right] = horizontal.areas(main);
|
||||
|
||||
let title = Line::from_iter([
|
||||
Span::from("Block Widget").bold(),
|
||||
Span::from(" (Press 'q' to quit)"),
|
||||
]);
|
||||
frame.render_widget(title.centered(), top);
|
||||
|
||||
render_bordered_block(frame, left);
|
||||
render_styled_block(frame, middle);
|
||||
render_custom_bordered_block(frame, right);
|
||||
}
|
||||
|
||||
/// Render a block with borders.
|
||||
pub fn render_bordered_block(frame: &mut Frame, area: Rect) {
|
||||
let block = Block::bordered().title("Bordered block");
|
||||
frame.render_widget(block, area);
|
||||
}
|
||||
|
||||
/// Render a styled block.
|
||||
pub fn render_styled_block(frame: &mut Frame, area: Rect) {
|
||||
let block = Block::bordered()
|
||||
.style(Style::new().blue().on_black().bold().italic())
|
||||
.title("Styled block");
|
||||
frame.render_widget(block, area);
|
||||
}
|
||||
|
||||
/// Render a block with custom borders.
|
||||
pub fn render_custom_bordered_block(frame: &mut Frame, area: Rect) {
|
||||
let block = Block::bordered()
|
||||
.border_type(BorderType::Rounded)
|
||||
.border_style(Style::new().red())
|
||||
.title("Custom borders");
|
||||
frame.render_widget(block, area);
|
||||
}
|
||||
@@ -1,11 +1,20 @@
|
||||
use crate::{prelude::*, style::Styled, widgets::Block};
|
||||
//! The [`BarChart`] widget and its related types (e.g. [`Bar`], [`BarGroup`]).
|
||||
|
||||
use ratatui_core::{
|
||||
buffer::Buffer,
|
||||
layout::{Direction, Rect},
|
||||
style::{Style, Styled},
|
||||
symbols::{self},
|
||||
text::Line,
|
||||
widgets::Widget,
|
||||
};
|
||||
|
||||
pub use self::{bar::Bar, bar_group::BarGroup};
|
||||
use crate::block::{Block, BlockExt};
|
||||
|
||||
mod bar;
|
||||
mod bar_group;
|
||||
|
||||
pub use bar::Bar;
|
||||
pub use bar_group::BarGroup;
|
||||
|
||||
/// A chart showing values as [bars](Bar).
|
||||
///
|
||||
/// Here is a possible `BarChart` output.
|
||||
@@ -42,7 +51,10 @@ pub use bar_group::BarGroup;
|
||||
/// The first group is added by an array slice (`&[(&str, u64)]`).
|
||||
/// The second group is added by a [`BarGroup`] instance.
|
||||
/// ```
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::{
|
||||
/// style::{Style, Stylize},
|
||||
/// widgets::{Bar, BarChart, BarGroup, Block},
|
||||
/// };
|
||||
///
|
||||
/// BarChart::default()
|
||||
/// .block(Block::bordered().title("BarChart"))
|
||||
@@ -52,10 +64,21 @@ pub use bar_group::BarGroup;
|
||||
/// .bar_style(Style::new().yellow().on_red())
|
||||
/// .value_style(Style::new().red().bold())
|
||||
/// .label_style(Style::new().white())
|
||||
/// .data(&[("B0", 0), ("B1", 2), ("B2", 4), ("B3", 3)])
|
||||
/// .data(BarGroup::default().bars(&[Bar::default().value(10), Bar::default().value(20)]))
|
||||
/// .data(&[("A0", 0), ("A1", 2), ("A2", 4), ("A3", 3)])
|
||||
/// .data(BarGroup::new([
|
||||
/// Bar::with_label("B0", 10),
|
||||
/// Bar::with_label("B2", 20),
|
||||
/// ]))
|
||||
/// .max(4);
|
||||
/// ```
|
||||
///
|
||||
/// For simpler usages, you can also create a `BarChart` simply by
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::widgets::{Bar, BarChart};
|
||||
///
|
||||
/// BarChart::new([Bar::with_label("A", 10), Bar::with_label("B", 20)]);
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct BarChart<'a> {
|
||||
/// Block to wrap the widget in
|
||||
@@ -105,6 +128,52 @@ impl<'a> Default for BarChart<'a> {
|
||||
}
|
||||
|
||||
impl<'a> BarChart<'a> {
|
||||
/// Creates a new vertical `BarChart` widget with the given bars.
|
||||
///
|
||||
/// The `bars` parameter accepts any type that can be converted into a `Vec<Bar>`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::{
|
||||
/// layout::Direction,
|
||||
/// widgets::{Bar, BarChart},
|
||||
/// };
|
||||
///
|
||||
/// BarChart::new(vec![Bar::with_label("A", 10), Bar::with_label("B", 10)]);
|
||||
/// ```
|
||||
pub fn new<T: Into<Vec<Bar<'a>>>>(bars: T) -> Self {
|
||||
Self {
|
||||
data: vec![BarGroup::new(bars.into())],
|
||||
direction: Direction::Vertical,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new `BarChart` widget with a vertical direction.
|
||||
///
|
||||
/// This function is equivalent to `BarChart::new()`.
|
||||
pub fn vertical(bars: impl Into<Vec<Bar<'a>>>) -> Self {
|
||||
Self::new(bars)
|
||||
}
|
||||
|
||||
/// Creates a new `BarChart` widget with a horizontal direction.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::widgets::{Bar, BarChart};
|
||||
///
|
||||
/// BarChart::horizontal(vec![Bar::with_label("A", 10), Bar::with_label("B", 20)]);
|
||||
/// ```
|
||||
pub fn horizontal(bars: impl Into<Vec<Bar<'a>>>) -> Self {
|
||||
Self {
|
||||
data: vec![BarGroup::new(bars.into())],
|
||||
direction: Direction::Horizontal,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Add group of bars to the `BarChart`
|
||||
///
|
||||
/// # Examples
|
||||
@@ -113,10 +182,14 @@ impl<'a> BarChart<'a> {
|
||||
/// The first group is added by an array slice (`&[(&str, u64)]`).
|
||||
/// The second group is added by a [`BarGroup`] instance.
|
||||
/// ```
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::widgets::{Bar, BarChart, BarGroup};
|
||||
///
|
||||
/// BarChart::default()
|
||||
/// .data(&[("B0", 0), ("B1", 2), ("B2", 4), ("B3", 3)])
|
||||
/// .data(BarGroup::default().bars(&[Bar::default().value(10), Bar::default().value(20)]));
|
||||
/// .data(BarGroup::new([
|
||||
/// Bar::with_label("A", 10),
|
||||
/// Bar::with_label("B", 20),
|
||||
/// ]));
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn data(mut self, data: impl Into<BarGroup<'a>>) -> Self {
|
||||
@@ -143,7 +216,7 @@ impl<'a> BarChart<'a> {
|
||||
/// This example shows the default behavior when `max` is not set.
|
||||
/// The maximum value in the dataset is taken (here, `100`).
|
||||
/// ```
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::widgets::BarChart;
|
||||
/// BarChart::default().data(&[("foo", 1), ("bar", 2), ("baz", 100)]);
|
||||
/// // Renders
|
||||
/// // █
|
||||
@@ -154,7 +227,8 @@ impl<'a> BarChart<'a> {
|
||||
/// This example shows a custom max value.
|
||||
/// The maximum height being `2`, `bar` & `baz` render as the max.
|
||||
/// ```
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::widgets::BarChart;
|
||||
///
|
||||
/// BarChart::default()
|
||||
/// .data(&[("foo", 1), ("bar", 2), ("baz", 100)])
|
||||
/// .max(2);
|
||||
@@ -176,6 +250,8 @@ impl<'a> BarChart<'a> {
|
||||
///
|
||||
/// It is also possible to set individually the style of each [`Bar`].
|
||||
/// In this case the default style will be patched by the individual style
|
||||
///
|
||||
/// [`Color`]: ratatui_core::style::Color
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn bar_style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.bar_style = style.into();
|
||||
@@ -184,8 +260,8 @@ impl<'a> BarChart<'a> {
|
||||
|
||||
/// Set the width of the displayed bars.
|
||||
///
|
||||
/// For [`Horizontal`](crate::layout::Direction::Horizontal) bars this becomes the height of
|
||||
/// the bar.
|
||||
/// For [`Horizontal`](ratatui_core::layout::Direction::Horizontal) bars this becomes the height
|
||||
/// of the bar.
|
||||
///
|
||||
/// If not set, this defaults to `1`.
|
||||
/// The bar label also uses this value as its width.
|
||||
@@ -204,7 +280,8 @@ impl<'a> BarChart<'a> {
|
||||
///
|
||||
/// This shows two bars with a gap of `3`. Notice the labels will always stay under the bar.
|
||||
/// ```
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::widgets::BarChart;
|
||||
///
|
||||
/// BarChart::default()
|
||||
/// .data(&[("foo", 1), ("bar", 2)])
|
||||
/// .bar_gap(3);
|
||||
@@ -219,9 +296,9 @@ impl<'a> BarChart<'a> {
|
||||
self
|
||||
}
|
||||
|
||||
/// The [`bar::Set`](crate::symbols::bar::Set) to use for displaying the bars.
|
||||
/// The [`bar::Set`](ratatui_core::symbols::bar::Set) to use for displaying the bars.
|
||||
///
|
||||
/// If not set, the default is [`bar::NINE_LEVELS`](crate::symbols::bar::NINE_LEVELS).
|
||||
/// If not set, the default is [`bar::NINE_LEVELS`](ratatui_core::symbols::bar::NINE_LEVELS).
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub const fn bar_set(mut self, bar_set: symbols::bar::Set) -> Self {
|
||||
self.bar_set = bar_set;
|
||||
@@ -239,6 +316,8 @@ impl<'a> BarChart<'a> {
|
||||
/// # See also
|
||||
///
|
||||
/// [`Bar::value_style`] to set the value style individually.
|
||||
///
|
||||
/// [`Color`]: ratatui_core::style::Color
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn value_style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.value_style = style.into();
|
||||
@@ -256,6 +335,8 @@ impl<'a> BarChart<'a> {
|
||||
/// # See also
|
||||
///
|
||||
/// [`Bar::label`] to set the label style individually.
|
||||
///
|
||||
/// [`Color`]: ratatui_core::style::Color
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn label_style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.label_style = style.into();
|
||||
@@ -275,6 +356,8 @@ impl<'a> BarChart<'a> {
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// The style will be applied to everything that isn't styled (borders, bars, labels, ...).
|
||||
///
|
||||
/// [`Color`]: ratatui_core::style::Color
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.style = style.into();
|
||||
@@ -283,7 +366,7 @@ impl<'a> BarChart<'a> {
|
||||
|
||||
/// Set the direction of the bars.
|
||||
///
|
||||
/// [`Vertical`](crate::layout::Direction::Vertical) bars are the default.
|
||||
/// [`Vertical`](ratatui_core::layout::Direction::Vertical) bars are the default.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
@@ -577,15 +660,15 @@ impl BarChart<'_> {
|
||||
|
||||
impl Widget for BarChart<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.render_ref(area, buf);
|
||||
Widget::render(&self, area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetRef for BarChart<'_> {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
impl Widget for &BarChart<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
buf.set_style(area, self.style);
|
||||
|
||||
self.block.render_ref(area, buf);
|
||||
self.block.as_ref().render(area, buf);
|
||||
let inner = self.block.inner_if_some(area);
|
||||
|
||||
if inner.is_empty() || self.data.is_empty() || self.bar_width == 0 {
|
||||
@@ -613,9 +696,14 @@ impl<'a> Styled for BarChart<'a> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use itertools::iproduct;
|
||||
use ratatui_core::{
|
||||
layout::Alignment,
|
||||
style::{Color, Modifier, Stylize},
|
||||
text::Span,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
use crate::widgets::BorderType;
|
||||
use crate::borders::BorderType;
|
||||
|
||||
#[test]
|
||||
fn default() {
|
||||
@@ -846,10 +934,10 @@ mod tests {
|
||||
#[test]
|
||||
fn test_empty_group() {
|
||||
let chart = BarChart::default()
|
||||
.data(BarGroup::default().label("invisible".into()))
|
||||
.data(BarGroup::default().label("invisible"))
|
||||
.data(
|
||||
BarGroup::default()
|
||||
.label("G".into())
|
||||
.label("G")
|
||||
.bars(&[Bar::default().value(1), Bar::default().value(2)]),
|
||||
);
|
||||
|
||||
@@ -866,12 +954,12 @@ mod tests {
|
||||
|
||||
fn build_test_barchart<'a>() -> BarChart<'a> {
|
||||
BarChart::default()
|
||||
.data(BarGroup::default().label("G1".into()).bars(&[
|
||||
.data(BarGroup::default().label("G1").bars(&[
|
||||
Bar::default().value(2),
|
||||
Bar::default().value(3),
|
||||
Bar::default().value(4),
|
||||
]))
|
||||
.data(BarGroup::default().label("G2".into()).bars(&[
|
||||
.data(BarGroup::default().label("G2").bars(&[
|
||||
Bar::default().value(3),
|
||||
Bar::default().value(4),
|
||||
Bar::default().value(5),
|
||||
@@ -938,7 +1026,7 @@ mod tests {
|
||||
fn test_horizontal_bars_label_width_greater_than_bar(bar_color: Option<Color>) {
|
||||
let mut bar = Bar::default()
|
||||
.value(2)
|
||||
.text_value("label".into())
|
||||
.text_value("label")
|
||||
.value_style(Style::default().red());
|
||||
|
||||
if let Some(color) = bar_color {
|
||||
@@ -1013,7 +1101,7 @@ mod tests {
|
||||
let chart: BarChart<'_> = BarChart::default()
|
||||
.data(
|
||||
BarGroup::default()
|
||||
.label(Span::from("G1").red().into())
|
||||
.label(Span::from("G1").red())
|
||||
.bars(&[Bar::default().value(2)]),
|
||||
)
|
||||
.group_gap(1)
|
||||
@@ -1081,18 +1169,9 @@ mod tests {
|
||||
#[test]
|
||||
fn test_unicode_as_value() {
|
||||
let group = BarGroup::default().bars(&[
|
||||
Bar::default()
|
||||
.value(123)
|
||||
.label("B1".into())
|
||||
.text_value("写".into()),
|
||||
Bar::default()
|
||||
.value(321)
|
||||
.label("B2".into())
|
||||
.text_value("写".into()),
|
||||
Bar::default()
|
||||
.value(333)
|
||||
.label("B2".into())
|
||||
.text_value("写".into()),
|
||||
Bar::default().value(123).label("B1").text_value("写"),
|
||||
Bar::default().value(321).label("B2").text_value("写"),
|
||||
Bar::default().value(333).label("B2").text_value("写"),
|
||||
]);
|
||||
let chart = BarChart::default().data(group).bar_width(3).bar_gap(1);
|
||||
|
||||
@@ -1134,7 +1213,7 @@ mod tests {
|
||||
("i", 8),
|
||||
])
|
||||
.into();
|
||||
group = group.label("Group".into());
|
||||
group = group.label("Group");
|
||||
|
||||
let chart = BarChart::default()
|
||||
.data(group)
|
||||
@@ -1159,7 +1238,7 @@ mod tests {
|
||||
("i", 8),
|
||||
])
|
||||
.into();
|
||||
group = group.label("Group".into());
|
||||
group = group.label("Group");
|
||||
|
||||
let chart = BarChart::default()
|
||||
.data(group)
|
||||
@@ -1326,4 +1405,19 @@ mod tests {
|
||||
]);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_barchart_new() {
|
||||
let bars = [Bar::with_label("Red", 1), Bar::with_label("Green", 2)];
|
||||
|
||||
let chart = BarChart::new(bars.clone());
|
||||
assert_eq!(chart.data.len(), 1);
|
||||
assert_eq!(chart.data[0].bars, bars);
|
||||
|
||||
let bars2 = [("Blue", 3)];
|
||||
|
||||
let updated_chart = chart.data(&bars2);
|
||||
assert_eq!(updated_chart.data.len(), 2);
|
||||
assert_eq!(updated_chart.data[1].bars, [Bar::with_label("Blue", 3)]);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,13 @@
|
||||
use ratatui_core::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::{Style, Styled},
|
||||
text::Line,
|
||||
widgets::Widget,
|
||||
};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
/// A bar to be shown by the [`BarChart`](crate::widgets::BarChart) widget.
|
||||
/// A bar to be shown by the [`BarChart`](super::BarChart) widget.
|
||||
///
|
||||
/// Here is an explanation of a `Bar`'s components.
|
||||
/// ```plain
|
||||
@@ -17,14 +22,15 @@ use crate::prelude::*;
|
||||
/// The following example creates a bar with the label "Bar 1", a value "10",
|
||||
/// red background and a white value foreground.
|
||||
/// ```
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::{
|
||||
/// style::{Style, Stylize},
|
||||
/// widgets::Bar,
|
||||
/// };
|
||||
///
|
||||
/// Bar::default()
|
||||
/// .label("Bar 1".into())
|
||||
/// .value(10)
|
||||
/// .style(Style::default().fg(Color::Red))
|
||||
/// .value_style(Style::default().bg(Color::Red).fg(Color::White))
|
||||
/// .text_value("10°C".to_string());
|
||||
/// Bar::with_label("Bar 1", 10)
|
||||
/// .red()
|
||||
/// .value_style(Style::new().red().on_white())
|
||||
/// .text_value("10°C");
|
||||
/// ```
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Bar<'a> {
|
||||
@@ -41,14 +47,54 @@ pub struct Bar<'a> {
|
||||
}
|
||||
|
||||
impl<'a> Bar<'a> {
|
||||
/// Creates a new `Bar` with the given value.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::widgets::Bar;
|
||||
///
|
||||
/// let bar = Bar::new(42);
|
||||
/// ```
|
||||
pub const fn new(value: u64) -> Self {
|
||||
Self {
|
||||
value,
|
||||
label: None,
|
||||
style: Style::new(),
|
||||
value_style: Style::new(),
|
||||
text_value: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new `Bar` with the given `label` and value.
|
||||
///
|
||||
/// a `label` can be a [`&str`], [`String`] or anything that can be converted into [`Line`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::widgets::Bar;
|
||||
///
|
||||
/// let bar = Bar::with_label("Label", 42);
|
||||
/// ```
|
||||
pub fn with_label<T: Into<Line<'a>>>(label: T, value: u64) -> Self {
|
||||
Self {
|
||||
value,
|
||||
label: Some(label.into()),
|
||||
style: Style::new(),
|
||||
value_style: Style::new(),
|
||||
text_value: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the value of this bar.
|
||||
///
|
||||
/// The value will be displayed inside the bar.
|
||||
///
|
||||
/// # See also
|
||||
///
|
||||
/// [`Bar::value_style`] to style the value.
|
||||
/// [`Bar::text_value`] to set the displayed value.
|
||||
/// - [`Bar::value_style`] to style the value.
|
||||
/// - [`Bar::text_value`] to set the displayed value.
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub const fn value(mut self, value: u64) -> Self {
|
||||
self.value = value;
|
||||
@@ -57,14 +103,35 @@ impl<'a> Bar<'a> {
|
||||
|
||||
/// Set the label of the bar.
|
||||
///
|
||||
/// For [`Vertical`](crate::layout::Direction::Vertical) bars,
|
||||
/// `label` can be a [`&str`], [`String`] or anything that can be converted into [`Line`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// From [`&str`] and [`String`]:
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::widgets::Bar;
|
||||
///
|
||||
/// Bar::default().label("label");
|
||||
/// Bar::default().label(String::from("label"));
|
||||
/// ```
|
||||
///
|
||||
/// From a [`Line`] with red foreground color:
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::{style::Stylize, text::Line, widgets::Bar};
|
||||
///
|
||||
/// Bar::default().label(Line::from("Line").red());
|
||||
/// ```
|
||||
///
|
||||
/// For [`Vertical`](ratatui_core::layout::Direction::Vertical) bars,
|
||||
/// display the label **under** the bar.
|
||||
/// For [`Horizontal`](crate::layout::Direction::Horizontal) bars,
|
||||
/// For [`Horizontal`](ratatui_core::layout::Direction::Horizontal) bars,
|
||||
/// display the label **in** the bar.
|
||||
/// See [`BarChart::direction`](crate::widgets::BarChart::direction) to set the direction.
|
||||
/// See [`BarChart::direction`](crate::barchart::BarChart::direction) to set the direction.
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn label(mut self, label: Line<'a>) -> Self {
|
||||
self.label = Some(label);
|
||||
pub fn label<T: Into<Line<'a>>>(mut self, label: T) -> Self {
|
||||
self.label = Some(label.into());
|
||||
self
|
||||
}
|
||||
|
||||
@@ -74,6 +141,8 @@ impl<'a> Bar<'a> {
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// This will apply to every non-styled element. It can be seen and used as a default value.
|
||||
///
|
||||
/// [`Color`]: ratatui_core::style::Color
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.style = style.into();
|
||||
@@ -88,6 +157,8 @@ impl<'a> Bar<'a> {
|
||||
/// # See also
|
||||
///
|
||||
/// [`Bar::value`] to set the value.
|
||||
///
|
||||
/// [`Color`]: ratatui_core::style::Color
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn value_style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.value_style = style.into();
|
||||
@@ -96,15 +167,28 @@ impl<'a> Bar<'a> {
|
||||
|
||||
/// Set the text value printed in the bar.
|
||||
///
|
||||
/// `text_value` can be a [`&str`], `Number` or anything that can be converted into [`String`].
|
||||
///
|
||||
/// If `text_value` is not set, then the [`ToString`] representation of `value` will be shown on
|
||||
/// the bar.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// From [`&str`] and [`String`]:
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::widgets::Bar;
|
||||
///
|
||||
/// Bar::default().text_value("label");
|
||||
/// Bar::default().text_value(String::from("label"));
|
||||
/// ```
|
||||
///
|
||||
/// # See also
|
||||
///
|
||||
/// [`Bar::value`] to set the value.
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn text_value(mut self, text_value: String) -> Self {
|
||||
self.text_value = Some(text_value);
|
||||
pub fn text_value<T: Into<String>>(mut self, text_value: T) -> Self {
|
||||
self.text_value = Some(text_value.into());
|
||||
self
|
||||
}
|
||||
|
||||
@@ -200,3 +284,46 @@ impl<'a> Bar<'a> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Styled for Bar<'a> {
|
||||
type Item = Self;
|
||||
|
||||
fn style(&self) -> Style {
|
||||
self.style
|
||||
}
|
||||
|
||||
fn set_style<S: Into<Style>>(mut self, style: S) -> Self::Item {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ratatui_core::style::{Color, Modifier, Style, Stylize};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_bar_new() {
|
||||
let bar = Bar::new(42).label(Line::from("Label"));
|
||||
assert_eq!(bar.label, Some(Line::from("Label")));
|
||||
assert_eq!(bar.value, 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bar_with_label() {
|
||||
let bar = Bar::with_label("Label", 42);
|
||||
assert_eq!(bar.label, Some(Line::from("Label")));
|
||||
assert_eq!(bar.value, 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bar_stylized() {
|
||||
let bar = Bar::default().red().bold();
|
||||
assert_eq!(
|
||||
bar.style,
|
||||
Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,21 @@
|
||||
use super::Bar;
|
||||
use crate::prelude::*;
|
||||
use ratatui_core::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Rect},
|
||||
style::Style,
|
||||
text::Line,
|
||||
widgets::Widget,
|
||||
};
|
||||
|
||||
use crate::barchart::Bar;
|
||||
|
||||
/// A group of bars to be shown by the Barchart.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::widgets::{Bar, BarGroup};
|
||||
///
|
||||
/// BarGroup::default()
|
||||
/// .label("Group 1".into())
|
||||
/// .bars(&[Bar::default().value(200), Bar::default().value(150)]);
|
||||
/// let group = BarGroup::new([Bar::with_label("Red", 20), Bar::with_label("Blue", 15)]);
|
||||
/// ```
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct BarGroup<'a> {
|
||||
@@ -21,10 +26,50 @@ pub struct BarGroup<'a> {
|
||||
}
|
||||
|
||||
impl<'a> BarGroup<'a> {
|
||||
/// Creates a new `BarGroup` with the given bars.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::{
|
||||
/// style::{Style, Stylize},
|
||||
/// widgets::{Bar, BarGroup},
|
||||
/// };
|
||||
///
|
||||
/// let group = BarGroup::new(vec![Bar::with_label("A", 10), Bar::with_label("B", 20)]);
|
||||
/// ```
|
||||
pub fn new<T: Into<Vec<Bar<'a>>>>(bars: T) -> Self {
|
||||
Self {
|
||||
bars: bars.into(),
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the group label
|
||||
///
|
||||
/// `label` can be a [`&str`], [`String`] or anything that can be converted into [`Line`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// From [`&str`] and [`String`].
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::widgets::BarGroup;
|
||||
///
|
||||
/// BarGroup::default().label("label");
|
||||
/// BarGroup::default().label(String::from("label"));
|
||||
/// ```
|
||||
///
|
||||
/// From a [`Line`] with red foreground color:
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::{style::Stylize, text::Line, widgets::BarGroup};
|
||||
///
|
||||
/// BarGroup::default().label(Line::from("Line").red());
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn label(mut self, label: Line<'a>) -> Self {
|
||||
self.label = Some(label);
|
||||
pub fn label<T: Into<Line<'a>>>(mut self, label: T) -> Self {
|
||||
self.label = Some(label.into());
|
||||
self
|
||||
}
|
||||
|
||||
@@ -70,7 +115,7 @@ impl<'a> From<&[(&'a str, u64)]> for BarGroup<'a> {
|
||||
label: None,
|
||||
bars: value
|
||||
.iter()
|
||||
.map(|&(text, v)| Bar::default().value(v).label(text.into()))
|
||||
.map(|&(text, v)| Bar::with_label(text, v))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
@@ -89,3 +134,16 @@ impl<'a> From<&Vec<(&'a str, u64)>> for BarGroup<'a> {
|
||||
Self::from(array)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_bargroup_new() {
|
||||
let group = BarGroup::new([Bar::with_label("Label1", 1), Bar::with_label("Label2", 2)])
|
||||
.label(Line::from("Group1"));
|
||||
assert_eq!(group.label, Some(Line::from("Group1")));
|
||||
assert_eq!(group.bars.len(), 2);
|
||||
}
|
||||
}
|
||||
@@ -6,17 +6,25 @@
|
||||
//! [title](Block::title) and [padding](Block::padding).
|
||||
|
||||
use itertools::Itertools;
|
||||
use strum::{Display, EnumString};
|
||||
use ratatui_core::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Rect},
|
||||
style::{Style, Styled},
|
||||
symbols::border,
|
||||
text::Line,
|
||||
widgets::Widget,
|
||||
};
|
||||
|
||||
use crate::{prelude::*, style::Styled, symbols::border, widgets::Borders};
|
||||
pub use self::{
|
||||
padding::Padding,
|
||||
title::{Position, Title},
|
||||
};
|
||||
use crate::borders::{BorderType, Borders};
|
||||
|
||||
mod padding;
|
||||
pub mod title;
|
||||
|
||||
pub use padding::Padding;
|
||||
pub use title::{Position, Title};
|
||||
|
||||
/// Base widget to be used to display a box border around all [upper level ones](crate::widgets).
|
||||
/// Base widget to be used to display a box border around all other built-in widgets.
|
||||
///
|
||||
/// The borders can be configured with [`Block::borders`] and others. A block can have multiple
|
||||
/// [`Title`] using [`Block::title`]. It can also be [styled](Block::style) and
|
||||
@@ -67,7 +75,10 @@ pub use title::{Position, Title};
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::{
|
||||
/// style::{Color, Style},
|
||||
/// widgets::{Block, BorderType, Borders},
|
||||
/// };
|
||||
///
|
||||
/// Block::new()
|
||||
/// .border_type(BorderType::Rounded)
|
||||
@@ -79,12 +90,9 @@ pub use title::{Position, Title};
|
||||
///
|
||||
/// You may also use multiple titles like in the following:
|
||||
/// ```
|
||||
/// use ratatui::{
|
||||
/// prelude::*,
|
||||
/// widgets::{
|
||||
/// block::{Position, Title},
|
||||
/// Block,
|
||||
/// },
|
||||
/// use ratatui::widgets::{
|
||||
/// block::{Position, Title},
|
||||
/// Block,
|
||||
/// };
|
||||
///
|
||||
/// Block::new()
|
||||
@@ -94,10 +102,7 @@ pub use title::{Position, Title};
|
||||
///
|
||||
/// You can also pass it as parameters of another widget so that the block surrounds them:
|
||||
/// ```
|
||||
/// use ratatui::{
|
||||
/// prelude::*,
|
||||
/// widgets::{Block, Borders, List},
|
||||
/// };
|
||||
/// use ratatui::widgets::{Block, Borders, List};
|
||||
///
|
||||
/// let surrounding_block = Block::default()
|
||||
/// .borders(Borders::ALL)
|
||||
@@ -108,7 +113,7 @@ pub use title::{Position, Title};
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Block<'a> {
|
||||
/// List of titles
|
||||
titles: Vec<Title<'a>>,
|
||||
titles: Vec<(Option<Position>, Line<'a>)>,
|
||||
/// The style to be patched to all titles of the block
|
||||
titles_style: Style,
|
||||
/// The default alignment of the titles that don't have one
|
||||
@@ -128,79 +133,6 @@ pub struct Block<'a> {
|
||||
padding: Padding,
|
||||
}
|
||||
|
||||
/// The type of border of a [`Block`].
|
||||
///
|
||||
/// See the [`borders`](Block::borders) method of `Block` to configure its borders.
|
||||
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub enum BorderType {
|
||||
/// A plain, simple border.
|
||||
///
|
||||
/// This is the default
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```plain
|
||||
/// ┌───────┐
|
||||
/// │ │
|
||||
/// └───────┘
|
||||
/// ```
|
||||
#[default]
|
||||
Plain,
|
||||
/// A plain border with rounded corners.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```plain
|
||||
/// ╭───────╮
|
||||
/// │ │
|
||||
/// ╰───────╯
|
||||
/// ```
|
||||
Rounded,
|
||||
/// A doubled border.
|
||||
///
|
||||
/// Note this uses one character that draws two lines.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```plain
|
||||
/// ╔═══════╗
|
||||
/// ║ ║
|
||||
/// ╚═══════╝
|
||||
/// ```
|
||||
Double,
|
||||
/// A thick border.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```plain
|
||||
/// ┏━━━━━━━┓
|
||||
/// ┃ ┃
|
||||
/// ┗━━━━━━━┛
|
||||
/// ```
|
||||
Thick,
|
||||
/// A border with a single line on the inside of a half block.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```plain
|
||||
/// ▗▄▄▄▄▄▄▄▖
|
||||
/// ▐ ▌
|
||||
/// ▐ ▌
|
||||
/// ▝▀▀▀▀▀▀▀▘
|
||||
QuadrantInside,
|
||||
|
||||
/// A border with a single line on the outside of a half block.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```plain
|
||||
/// ▛▀▀▀▀▀▀▀▜
|
||||
/// ▌ ▐
|
||||
/// ▌ ▐
|
||||
/// ▙▄▄▄▄▄▄▄▟
|
||||
QuadrantOutside,
|
||||
}
|
||||
|
||||
impl<'a> Block<'a> {
|
||||
/// Creates a new block with no [`Borders`] or [`Padding`].
|
||||
pub const fn new() -> Self {
|
||||
@@ -220,7 +152,8 @@ impl<'a> Block<'a> {
|
||||
/// Create a new block with [all borders](Borders::ALL) shown
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::widgets::{Block, Borders};
|
||||
/// use ratatui::widgets::{Block, Borders};
|
||||
///
|
||||
/// assert_eq!(Block::bordered(), Block::new().borders(Borders::ALL));
|
||||
/// ```
|
||||
pub const fn bordered() -> Self {
|
||||
@@ -239,13 +172,13 @@ impl<'a> Block<'a> {
|
||||
/// space is calculated based on the full width of the block, rather than the leftover width.
|
||||
///
|
||||
/// You can provide any type that can be converted into [`Title`] including: strings, string
|
||||
/// slices (`&str`), borrowed strings (`Cow<str>`), [spans](crate::text::Span), or vectors of
|
||||
/// [spans](crate::text::Span) (`Vec<Span>`).
|
||||
/// slices (`&str`), borrowed strings (`Cow<str>`), [spans](ratatui_core::text::Span), or
|
||||
/// vectors of [spans](ratatui_core::text::Span) (`Vec<Span>`).
|
||||
///
|
||||
/// By default, the titles will avoid being rendered in the corners of the block but will align
|
||||
/// against the left or right edge of the block if there is no border on that edge.
|
||||
/// The following demonstrates this behavior, notice the second title is one character off to
|
||||
/// the left.
|
||||
/// against the left or right edge of the block if there is no border on that edge. The
|
||||
/// following demonstrates this behavior, notice the second title is one character off to the
|
||||
/// left.
|
||||
///
|
||||
/// ```plain
|
||||
/// ┌With at least a left border───
|
||||
@@ -268,15 +201,15 @@ impl<'a> Block<'a> {
|
||||
/// - Two titles with the same alignment (notice the left titles are separated)
|
||||
/// ```
|
||||
/// use ratatui::{
|
||||
/// prelude::*,
|
||||
/// widgets::{block::*, *},
|
||||
/// text::Line,
|
||||
/// widgets::{Block, Borders},
|
||||
/// };
|
||||
///
|
||||
/// Block::new()
|
||||
/// .title("Title") // By default in the top left corner
|
||||
/// .title(Title::from("Left").alignment(Alignment::Left)) // also on the left
|
||||
/// .title(Title::from("Right").alignment(Alignment::Right))
|
||||
/// .title(Title::from("Center").alignment(Alignment::Center));
|
||||
/// .title(Line::from("Left").left_aligned()) // also on the left
|
||||
/// .title(Line::from("Right").right_aligned())
|
||||
/// .title(Line::from("Center").centered());
|
||||
/// // Renders
|
||||
/// // ┌Title─Left────Center─────────Right┐
|
||||
/// ```
|
||||
@@ -288,26 +221,40 @@ impl<'a> Block<'a> {
|
||||
/// - [`Block::title_alignment`]
|
||||
/// - [`Block::title_position`]
|
||||
///
|
||||
/// [Block example]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md#block
|
||||
/// # Future improvements
|
||||
///
|
||||
/// In a future release of Ratatui this method will be changed to accept `Into<Line>` instead of
|
||||
/// `Into<Title>`. This allows us to remove the unnecessary `Title` struct and store the
|
||||
/// position in the block itself. For more information see
|
||||
/// <https://github.com/ratatui/ratatui/issues/738>.
|
||||
///
|
||||
/// [Block example]: https://github.com/ratatui/ratatui/blob/main/examples/README.md#block
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn title<T>(mut self, title: T) -> Self
|
||||
where
|
||||
T: Into<Title<'a>>,
|
||||
{
|
||||
self.titles.push(title.into());
|
||||
let title = title.into();
|
||||
let position = title.position;
|
||||
let mut content = title.content;
|
||||
if let Some(alignment) = title.alignment {
|
||||
content = content.alignment(alignment);
|
||||
}
|
||||
self.titles.push((position, content));
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a title to the top of the block.
|
||||
///
|
||||
/// You can provide any type that can be converted into [`Line`] including: strings, string
|
||||
/// slices (`&str`), borrowed strings (`Cow<str>`), [spans](crate::text::Span), or vectors of
|
||||
/// [spans](crate::text::Span) (`Vec<Span>`).
|
||||
/// slices (`&str`), borrowed strings (`Cow<str>`), [spans](ratatui_core::text::Span), or
|
||||
/// vectors of [spans](ratatui_core::text::Span) (`Vec<Span>`).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::{ prelude::*, widgets::* };
|
||||
/// use ratatui::{ widgets::Block, text::Line };
|
||||
///
|
||||
/// Block::bordered()
|
||||
/// .title_top("Left1") // By default in the top left corner
|
||||
/// .title_top(Line::from("Left2").left_aligned())
|
||||
@@ -321,21 +268,22 @@ impl<'a> Block<'a> {
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn title_top<T: Into<Line<'a>>>(mut self, title: T) -> Self {
|
||||
let title = Title::from(title).position(Position::Top);
|
||||
self.titles.push(title);
|
||||
let line = title.into();
|
||||
self.titles.push((Some(Position::Top), line));
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a title to the bottom of the block.
|
||||
///
|
||||
/// You can provide any type that can be converted into [`Line`] including: strings, string
|
||||
/// slices (`&str`), borrowed strings (`Cow<str>`), [spans](crate::text::Span), or vectors of
|
||||
/// [spans](crate::text::Span) (`Vec<Span>`).
|
||||
/// slices (`&str`), borrowed strings (`Cow<str>`), [spans](ratatui_core::text::Span), or
|
||||
/// vectors of [spans](ratatui_core::text::Span) (`Vec<Span>`).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::{ prelude::*, widgets::* };
|
||||
/// use ratatui::{ widgets::Block, text::Line };
|
||||
///
|
||||
/// Block::bordered()
|
||||
/// .title_bottom("Left1") // By default in the top left corner
|
||||
/// .title_bottom(Line::from("Left2").left_aligned())
|
||||
@@ -349,8 +297,8 @@ impl<'a> Block<'a> {
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn title_bottom<T: Into<Line<'a>>>(mut self, title: T) -> Self {
|
||||
let title = Title::from(title).position(Position::Bottom);
|
||||
self.titles.push(title);
|
||||
let line = title.into();
|
||||
self.titles.push((Some(Position::Bottom), line));
|
||||
self
|
||||
}
|
||||
|
||||
@@ -364,6 +312,8 @@ impl<'a> Block<'a> {
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// [`Color`]: ratatui_core::style::Color
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn title_style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.titles_style = style.into();
|
||||
@@ -379,15 +329,12 @@ impl<'a> Block<'a> {
|
||||
/// This example aligns all titles in the center except the "right" title which explicitly sets
|
||||
/// [`Alignment::Right`].
|
||||
/// ```
|
||||
/// use ratatui::{
|
||||
/// prelude::*,
|
||||
/// widgets::{block::*, *},
|
||||
/// };
|
||||
/// use ratatui::{layout::Alignment, text::Line, widgets::Block};
|
||||
///
|
||||
/// Block::new()
|
||||
/// .title_alignment(Alignment::Center)
|
||||
/// // This title won't be aligned in the center
|
||||
/// .title(Title::from("right").alignment(Alignment::Right))
|
||||
/// .title(Line::from("right").right_aligned())
|
||||
/// .title("foo")
|
||||
/// .title("bar");
|
||||
/// ```
|
||||
@@ -406,18 +353,12 @@ impl<'a> Block<'a> {
|
||||
/// This example positions all titles on the bottom except the "top" title which explicitly sets
|
||||
/// [`Position::Top`].
|
||||
/// ```
|
||||
/// use ratatui::{
|
||||
/// prelude::*,
|
||||
/// widgets::{
|
||||
/// block::{Position, Title},
|
||||
/// Block,
|
||||
/// },
|
||||
/// };
|
||||
/// use ratatui::widgets::{block::Position, Block};
|
||||
///
|
||||
/// Block::new()
|
||||
/// .title_position(Position::Bottom)
|
||||
/// // This title won't be aligned in the center
|
||||
/// .title(Title::from("top").position(Position::Top))
|
||||
/// .title_top("top")
|
||||
/// .title("foo")
|
||||
/// .title("bar");
|
||||
/// ```
|
||||
@@ -441,9 +382,14 @@ impl<'a> Block<'a> {
|
||||
///
|
||||
/// This example shows a `Block` with blue borders.
|
||||
/// ```
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::{
|
||||
/// style::{Style, Stylize},
|
||||
/// widgets::Block,
|
||||
/// };
|
||||
/// Block::bordered().border_style(Style::new().blue());
|
||||
/// ```
|
||||
///
|
||||
/// [`Color`]: ratatui_core::style::Color
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn border_style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.border_style = style.into();
|
||||
@@ -466,7 +412,11 @@ impl<'a> Block<'a> {
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::{
|
||||
/// style::{Color, Style, Stylize},
|
||||
/// widgets::{Block, Paragraph},
|
||||
/// };
|
||||
///
|
||||
/// let block = Block::new().style(Style::new().red().on_black());
|
||||
///
|
||||
/// // For border and title you can additionally apply styles on top of the block level style.
|
||||
@@ -482,7 +432,8 @@ impl<'a> Block<'a> {
|
||||
/// .style(Style::new().white().not_bold()); // will be white, and italic
|
||||
/// ```
|
||||
///
|
||||
/// [`Paragraph`]: crate::widgets::Paragraph
|
||||
/// [`Paragraph`]: crate::paragraph::Paragraph
|
||||
/// [`Color`]: ratatui_core::style::Color
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.style = style.into();
|
||||
@@ -497,7 +448,7 @@ impl<'a> Block<'a> {
|
||||
///
|
||||
/// Display left and right borders.
|
||||
/// ```
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::widgets::{Block, Borders};
|
||||
/// Block::new().borders(Borders::LEFT | Borders::RIGHT);
|
||||
/// ```
|
||||
///
|
||||
@@ -518,7 +469,7 @@ impl<'a> Block<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::widgets::{Block, BorderType};
|
||||
/// Block::bordered()
|
||||
/// .border_type(BorderType::Rounded)
|
||||
/// .title("Block");
|
||||
@@ -533,14 +484,15 @@ impl<'a> Block<'a> {
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the symbols used to display the border as a [`crate::symbols::border::Set`].
|
||||
/// Sets the symbols used to display the border as a [`ratatui_core::symbols::border::Set`].
|
||||
///
|
||||
/// Setting this overwrites any [`border_type`](Block::border_type) that was set.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::{widgets::Block, symbols};
|
||||
///
|
||||
/// Block::bordered().border_set(symbols::border::DOUBLE).title("Block");
|
||||
/// // Renders
|
||||
/// // ╔Block╗
|
||||
@@ -560,7 +512,8 @@ impl<'a> Block<'a> {
|
||||
///
|
||||
/// This renders a `Block` with no padding (the default).
|
||||
/// ```
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::widgets::{Block, Padding};
|
||||
///
|
||||
/// Block::bordered().padding(Padding::ZERO);
|
||||
/// // Renders
|
||||
/// // ┌───────┐
|
||||
@@ -571,7 +524,8 @@ impl<'a> Block<'a> {
|
||||
/// This example shows a `Block` with padding left and right ([`Padding::horizontal`]).
|
||||
/// Notice the two spaces before and after the content.
|
||||
/// ```
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::widgets::{Block, Padding};
|
||||
///
|
||||
/// Block::bordered().padding(Padding::horizontal(2));
|
||||
/// // Renders
|
||||
/// // ┌───────────┐
|
||||
@@ -590,12 +544,13 @@ impl<'a> Block<'a> {
|
||||
///
|
||||
/// Draw a block nested within another block
|
||||
/// ```
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::{widgets::Block, Frame};
|
||||
///
|
||||
/// # fn render_nested_block(frame: &mut Frame) {
|
||||
/// let outer_block = Block::bordered().title("Outer");
|
||||
/// let inner_block = Block::bordered().title("Inner");
|
||||
///
|
||||
/// let outer_area = frame.size();
|
||||
/// let outer_area = frame.area();
|
||||
/// let inner_area = outer_block.inner(outer_area);
|
||||
///
|
||||
/// frame.render_widget(outer_block, outer_area);
|
||||
@@ -642,37 +597,18 @@ impl<'a> Block<'a> {
|
||||
fn has_title_at_position(&self, position: Position) -> bool {
|
||||
self.titles
|
||||
.iter()
|
||||
.any(|title| title.position.unwrap_or(self.titles_position) == position)
|
||||
}
|
||||
}
|
||||
|
||||
impl BorderType {
|
||||
/// Convert this `BorderType` into the corresponding [`Set`](border::Set) of border symbols.
|
||||
pub const fn border_symbols(border_type: Self) -> border::Set {
|
||||
match border_type {
|
||||
Self::Plain => border::PLAIN,
|
||||
Self::Rounded => border::ROUNDED,
|
||||
Self::Double => border::DOUBLE,
|
||||
Self::Thick => border::THICK,
|
||||
Self::QuadrantInside => border::QUADRANT_INSIDE,
|
||||
Self::QuadrantOutside => border::QUADRANT_OUTSIDE,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert this `BorderType` into the corresponding [`Set`](border::Set) of border symbols.
|
||||
pub const fn to_border_set(self) -> border::Set {
|
||||
Self::border_symbols(self)
|
||||
.any(|(pos, _)| pos.unwrap_or(self.titles_position) == position)
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Block<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.render_ref(area, buf);
|
||||
Widget::render(&self, area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetRef for Block<'_> {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
impl Widget for &Block<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let area = area.intersection(buf.area);
|
||||
if area.is_empty() {
|
||||
return;
|
||||
@@ -787,7 +723,7 @@ impl Block<'_> {
|
||||
/// Currently (due to the way lines are truncated), the right side of the leftmost title will
|
||||
/// be cut off if the block is too small to fit all titles. This is not ideal and should be
|
||||
/// the left side of that leftmost that is cut off. This is due to the line being truncated
|
||||
/// incorrectly. See <https://github.com/ratatui-org/ratatui/issues/932>
|
||||
/// incorrectly. See <https://github.com/ratatui/ratatui/issues/932>
|
||||
#[allow(clippy::similar_names)]
|
||||
fn render_right_titles(&self, position: Position, area: Rect, buf: &mut Buffer) {
|
||||
let titles = self.filtered_titles(position, Alignment::Right);
|
||||
@@ -798,7 +734,7 @@ impl Block<'_> {
|
||||
if titles_area.is_empty() {
|
||||
break;
|
||||
}
|
||||
let title_width = title.content.width() as u16;
|
||||
let title_width = title.width() as u16;
|
||||
let title_area = Rect {
|
||||
x: titles_area
|
||||
.right()
|
||||
@@ -808,7 +744,7 @@ impl Block<'_> {
|
||||
..titles_area
|
||||
};
|
||||
buf.set_style(title_area, self.titles_style);
|
||||
title.content.render_ref(title_area, buf);
|
||||
title.render(title_area, buf);
|
||||
|
||||
// bump the width of the titles area to the left
|
||||
titles_area.width = titles_area
|
||||
@@ -830,7 +766,7 @@ impl Block<'_> {
|
||||
.collect_vec();
|
||||
let total_width = titles
|
||||
.iter()
|
||||
.map(|title| title.content.width() as u16 + 1) // space between titles
|
||||
.map(|title| title.width() as u16 + 1) // space between titles
|
||||
.sum::<u16>()
|
||||
.saturating_sub(1); // no space for the last title
|
||||
|
||||
@@ -843,13 +779,13 @@ impl Block<'_> {
|
||||
if titles_area.is_empty() {
|
||||
break;
|
||||
}
|
||||
let title_width = title.content.width() as u16;
|
||||
let title_width = title.width() as u16;
|
||||
let title_area = Rect {
|
||||
width: title_width.min(titles_area.width),
|
||||
..titles_area
|
||||
};
|
||||
buf.set_style(title_area, self.titles_style);
|
||||
title.content.render_ref(title_area, buf);
|
||||
title.render(title_area, buf);
|
||||
|
||||
// bump the titles area to the right and reduce its width
|
||||
titles_area.x = titles_area.x.saturating_add(title_width + 1);
|
||||
@@ -866,13 +802,13 @@ impl Block<'_> {
|
||||
if titles_area.is_empty() {
|
||||
break;
|
||||
}
|
||||
let title_width = title.content.width() as u16;
|
||||
let title_width = title.width() as u16;
|
||||
let title_area = Rect {
|
||||
width: title_width.min(titles_area.width),
|
||||
..titles_area
|
||||
};
|
||||
buf.set_style(title_area, self.titles_style);
|
||||
title.content.render_ref(title_area, buf);
|
||||
title.render(title_area, buf);
|
||||
|
||||
// bump the titles area to the right and reduce its width
|
||||
titles_area.x = titles_area.x.saturating_add(title_width + 1);
|
||||
@@ -885,11 +821,12 @@ impl Block<'_> {
|
||||
&self,
|
||||
position: Position,
|
||||
alignment: Alignment,
|
||||
) -> impl DoubleEndedIterator<Item = &Title> {
|
||||
self.titles.iter().filter(move |title| {
|
||||
title.position.unwrap_or(self.titles_position) == position
|
||||
&& title.alignment.unwrap_or(self.titles_alignment) == alignment
|
||||
})
|
||||
) -> impl DoubleEndedIterator<Item = &Line> {
|
||||
self.titles
|
||||
.iter()
|
||||
.filter(move |(pos, _)| pos.unwrap_or(self.titles_position) == position)
|
||||
.filter(move |(_, line)| line.alignment.unwrap_or(self.titles_alignment) == alignment)
|
||||
.map(|(_, line)| line)
|
||||
}
|
||||
|
||||
/// An area that is one line tall and spans the width of the block excluding the borders and
|
||||
@@ -972,6 +909,7 @@ impl<'a> Styled for Block<'a> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ratatui_core::style::{Color, Modifier, Stylize};
|
||||
use rstest::rstest;
|
||||
use strum::ParseError;
|
||||
|
||||
@@ -1023,24 +961,17 @@ mod tests {
|
||||
let area = Rect::new(0, 0, 0, 1);
|
||||
let expected = Rect::new(0, 1, 0, 0);
|
||||
|
||||
let block = Block::new().title(Title::from("Test").alignment(alignment));
|
||||
let block = Block::new().title(Line::from("Test").alignment(alignment));
|
||||
assert_eq!(block.inner(area), expected);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::top_top(Borders::TOP, Position::Top, Rect::new(0, 1, 0, 1))]
|
||||
#[case::top_bot(Borders::BOTTOM, Position::Top, Rect::new(0, 1, 0, 0))]
|
||||
#[case::bot_top(Borders::TOP, Position::Bottom, Rect::new(0, 1, 0, 0))]
|
||||
#[case::top_top(Borders::BOTTOM, Position::Bottom, Rect::new(0, 0, 0, 1))]
|
||||
fn inner_takes_into_account_border_and_title(
|
||||
#[case] borders: Borders,
|
||||
#[case] position: Position,
|
||||
#[case] expected: Rect,
|
||||
) {
|
||||
#[case::top_top(Block::new().title_top("Test").borders(Borders::TOP), Rect::new(0, 1, 0, 1))]
|
||||
#[case::top_bot(Block::new().title_top("Test").borders(Borders::BOTTOM), Rect::new(0, 1, 0, 0))]
|
||||
#[case::bot_top(Block::new().title_bottom("Test").borders(Borders::TOP), Rect::new(0, 1, 0, 0))]
|
||||
#[case::bot_bot(Block::new().title_bottom("Test").borders(Borders::BOTTOM), Rect::new(0, 0, 0, 1))]
|
||||
fn inner_takes_into_account_border_and_title(#[case] block: Block, #[case] expected: Rect) {
|
||||
let area = Rect::new(0, 0, 0, 2);
|
||||
let block = Block::new()
|
||||
.borders(borders)
|
||||
.title(Title::from("Test").position(position));
|
||||
assert_eq!(block.inner(area), expected);
|
||||
}
|
||||
|
||||
@@ -1050,32 +981,33 @@ mod tests {
|
||||
assert!(!block.has_title_at_position(Position::Top));
|
||||
assert!(!block.has_title_at_position(Position::Bottom));
|
||||
|
||||
let block = Block::new().title(Title::from("Test").position(Position::Top));
|
||||
let block = Block::new().title_top("test");
|
||||
assert!(block.has_title_at_position(Position::Top));
|
||||
assert!(!block.has_title_at_position(Position::Bottom));
|
||||
|
||||
let block = Block::new().title(Title::from("Test").position(Position::Bottom));
|
||||
let block = Block::new().title_bottom("test");
|
||||
assert!(!block.has_title_at_position(Position::Top));
|
||||
assert!(block.has_title_at_position(Position::Bottom));
|
||||
|
||||
#[allow(deprecated)] // until Title is removed
|
||||
let block = Block::new()
|
||||
.title(Title::from("Test").position(Position::Top))
|
||||
.title_position(Position::Bottom);
|
||||
assert!(block.has_title_at_position(Position::Top));
|
||||
assert!(!block.has_title_at_position(Position::Bottom));
|
||||
|
||||
#[allow(deprecated)] // until Title is removed
|
||||
let block = Block::new()
|
||||
.title(Title::from("Test").position(Position::Bottom))
|
||||
.title_position(Position::Top);
|
||||
assert!(!block.has_title_at_position(Position::Top));
|
||||
assert!(block.has_title_at_position(Position::Bottom));
|
||||
|
||||
let block = Block::new()
|
||||
.title(Title::from("Test").position(Position::Top))
|
||||
.title(Title::from("Test").position(Position::Bottom));
|
||||
let block = Block::new().title_top("test").title_bottom("test");
|
||||
assert!(block.has_title_at_position(Position::Top));
|
||||
assert!(block.has_title_at_position(Position::Bottom));
|
||||
|
||||
#[allow(deprecated)] // until Title is removed
|
||||
let block = Block::new()
|
||||
.title(Title::from("Test").position(Position::Top))
|
||||
.title(Title::from("Test"))
|
||||
@@ -1083,6 +1015,7 @@ mod tests {
|
||||
assert!(block.has_title_at_position(Position::Top));
|
||||
assert!(block.has_title_at_position(Position::Bottom));
|
||||
|
||||
#[allow(deprecated)] // until Title is removed
|
||||
let block = Block::new()
|
||||
.title(Title::from("Test"))
|
||||
.title(Title::from("Test").position(Position::Bottom))
|
||||
@@ -1130,16 +1063,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn vertical_space_takes_into_account_titles() {
|
||||
let block = Block::new()
|
||||
.title_position(Position::Top)
|
||||
.title(Title::from("Test"));
|
||||
|
||||
let block = Block::new().title_top("Test");
|
||||
assert_eq!(block.vertical_space(), (1, 0));
|
||||
|
||||
let block = Block::new()
|
||||
.title_position(Position::Bottom)
|
||||
.title(Title::from("Test"));
|
||||
|
||||
let block = Block::new().title_bottom("Test");
|
||||
assert_eq!(block.vertical_space(), (0, 1));
|
||||
}
|
||||
|
||||
@@ -1158,10 +1085,7 @@ mod tests {
|
||||
#[case] pos: Position,
|
||||
#[case] vertical_space: (u16, u16),
|
||||
) {
|
||||
let block = block
|
||||
.borders(borders)
|
||||
.title_position(pos)
|
||||
.title(Title::from("Test"));
|
||||
let block = block.borders(borders).title_position(pos).title("Test");
|
||||
assert_eq!(block.vertical_space(), vertical_space);
|
||||
}
|
||||
|
||||
@@ -1310,6 +1234,7 @@ mod tests {
|
||||
use Alignment::*;
|
||||
use Position::*;
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 11, 3));
|
||||
#[allow(deprecated)] // until Title is removed
|
||||
Block::bordered()
|
||||
.title(Title::from("A").position(Top).alignment(Left))
|
||||
.title(Title::from("B").position(Top).alignment(Center))
|
||||
@@ -1375,13 +1300,13 @@ mod tests {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 8, 1));
|
||||
Block::new()
|
||||
.title_alignment(block_title_alignment)
|
||||
.title(Title::from("test").alignment(alignment))
|
||||
.title(Line::from("test").alignment(alignment))
|
||||
.render(buffer.area, &mut buffer);
|
||||
assert_eq!(buffer, Buffer::with_lines([expected]));
|
||||
}
|
||||
}
|
||||
|
||||
/// This is a regression test for bug <https://github.com/ratatui-org/ratatui/issues/929>
|
||||
/// This is a regression test for bug <https://github.com/ratatui/ratatui/issues/929>
|
||||
#[test]
|
||||
fn render_right_aligned_empty_title() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
|
||||
@@ -10,7 +10,7 @@
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::widgets::Padding;
|
||||
///
|
||||
/// Padding::uniform(1);
|
||||
/// Padding::horizontal(2);
|
||||
@@ -19,8 +19,8 @@
|
||||
/// Padding::symmetric(5, 6);
|
||||
/// ```
|
||||
///
|
||||
/// [`Block`]: crate::widgets::Block
|
||||
/// [`padding`]: crate::widgets::Block::padding
|
||||
/// [`Block`]: crate::block::Block
|
||||
/// [`padding`]: crate::block::Block::padding
|
||||
/// [CSS padding]: https://developer.mozilla.org/en-US/docs/Web/CSS/padding
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
pub struct Padding {
|
||||
@@ -1,14 +1,24 @@
|
||||
//! This module holds the [`Title`] element and its related configuration types.
|
||||
//! A title is a piece of [`Block`](crate::widgets::Block) configuration.
|
||||
//! A title is a piece of [`Block`](crate::block::Block) configuration.
|
||||
|
||||
use ratatui_core::{layout::Alignment, text::Line};
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
use crate::{layout::Alignment, text::Line};
|
||||
|
||||
/// A [`Block`](crate::widgets::Block) title.
|
||||
/// A [`Block`](crate::block::Block) title.
|
||||
///
|
||||
/// It can be aligned (see [`Alignment`]) and positioned (see [`Position`]).
|
||||
///
|
||||
/// # Future Deprecation
|
||||
///
|
||||
/// This type is deprecated and will be removed in a future release. The reason for this is that the
|
||||
/// position of the title should be stored in the block itself, not in the title. The `Line` type
|
||||
/// has an alignment method that can be used to align the title. For more information see
|
||||
/// <https://github.com/ratatui/ratatui/issues/738>.
|
||||
///
|
||||
/// Use [`Line`] instead, when the position is not defined as part of the title. When a specific
|
||||
/// position is needed, use [`Block::title_top`](crate::block::Block::title_top) or
|
||||
/// [`Block::title_bottom`](crate::block::Block::title_bottom) instead.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// Title with no style.
|
||||
@@ -18,16 +28,16 @@ use crate::{layout::Alignment, text::Line};
|
||||
/// Title::from("Title");
|
||||
/// ```
|
||||
///
|
||||
/// Blue title on a white background (via [`Stylize`](crate::style::Stylize) trait).
|
||||
/// Blue title on a white background (via [`Stylize`](ratatui_core::style::Stylize) trait).
|
||||
/// ```
|
||||
/// use ratatui::{prelude::*, widgets::block::*};
|
||||
/// use ratatui::{style::Stylize, widgets::block::Title};
|
||||
///
|
||||
/// Title::from("Title".blue().on_white());
|
||||
/// ```
|
||||
///
|
||||
/// Title with multiple styles (see [`Line`] and [`Stylize`](crate::style::Stylize)).
|
||||
/// Title with multiple styles (see [`Line`] and [`Stylize`](ratatui_core::style::Stylize)).
|
||||
/// ```
|
||||
/// use ratatui::{prelude::*, widgets::block::*};
|
||||
/// use ratatui::{style::Stylize, text::Line, widgets::block::Title};
|
||||
///
|
||||
/// Title::from(Line::from(vec!["Q".white().underlined(), "uit".gray()]));
|
||||
/// ```
|
||||
@@ -35,7 +45,7 @@ use crate::{layout::Alignment, text::Line};
|
||||
/// Complete example
|
||||
/// ```
|
||||
/// use ratatui::{
|
||||
/// prelude::*,
|
||||
/// layout::Alignment,
|
||||
/// widgets::{
|
||||
/// block::{Position, Title},
|
||||
/// Block,
|
||||
@@ -53,19 +63,19 @@ pub struct Title<'a> {
|
||||
/// Title alignment
|
||||
///
|
||||
/// If [`None`], defaults to the alignment defined with
|
||||
/// [`Block::title_alignment`](crate::widgets::Block::title_alignment) in the associated
|
||||
/// [`Block`](crate::widgets::Block).
|
||||
/// [`Block::title_alignment`](crate::block::Block::title_alignment) in the associated
|
||||
/// [`Block`](crate::block::Block).
|
||||
pub alignment: Option<Alignment>,
|
||||
|
||||
/// Title position
|
||||
///
|
||||
/// If [`None`], defaults to the position defined with
|
||||
/// [`Block::title_position`](crate::widgets::Block::title_position) in the associated
|
||||
/// [`Block`](crate::widgets::Block).
|
||||
/// [`Block::title_position`](crate::block::Block::title_position) in the associated
|
||||
/// [`Block`](crate::block::Block).
|
||||
pub position: Option<Position>,
|
||||
}
|
||||
|
||||
/// Defines the [title](crate::widgets::block::Title) position.
|
||||
/// Defines the [title](crate::block::Title) position.
|
||||
///
|
||||
/// The title can be positioned on top or at the bottom of the block.
|
||||
/// Defaults to [`Position::Top`].
|
||||
@@ -73,7 +83,10 @@ pub struct Title<'a> {
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::widgets::{block::*, *};
|
||||
/// use ratatui::widgets::{
|
||||
/// block::{Position, Title},
|
||||
/// Block,
|
||||
/// };
|
||||
///
|
||||
/// Block::new().title(Title::from("title").position(Position::Bottom));
|
||||
/// ```
|
||||
@@ -88,6 +101,7 @@ pub enum Position {
|
||||
Bottom,
|
||||
}
|
||||
|
||||
#[deprecated = "use Block::title_top() or Block::title_bottom() instead. This will be removed in a future release."]
|
||||
impl<'a> Title<'a> {
|
||||
/// Set the title content.
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
@@ -1,6 +1,9 @@
|
||||
//! Border related types ([`Borders`], [`BorderType`]) and a macro to create borders ([`border`]).
|
||||
use std::fmt;
|
||||
|
||||
use bitflags::bitflags;
|
||||
use ratatui_core::symbols::border;
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
bitflags! {
|
||||
/// Bitflags that can be composed to set the visible borders essentially on the block widget.
|
||||
@@ -21,6 +24,98 @@ bitflags! {
|
||||
}
|
||||
}
|
||||
|
||||
/// The type of border of a [`Block`](crate::block::Block).
|
||||
///
|
||||
/// See the [`borders`](crate::block::Block::borders) method of `Block` to configure its borders.
|
||||
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub enum BorderType {
|
||||
/// A plain, simple border.
|
||||
///
|
||||
/// This is the default
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```plain
|
||||
/// ┌───────┐
|
||||
/// │ │
|
||||
/// └───────┘
|
||||
/// ```
|
||||
#[default]
|
||||
Plain,
|
||||
/// A plain border with rounded corners.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```plain
|
||||
/// ╭───────╮
|
||||
/// │ │
|
||||
/// ╰───────╯
|
||||
/// ```
|
||||
Rounded,
|
||||
/// A doubled border.
|
||||
///
|
||||
/// Note this uses one character that draws two lines.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```plain
|
||||
/// ╔═══════╗
|
||||
/// ║ ║
|
||||
/// ╚═══════╝
|
||||
/// ```
|
||||
Double,
|
||||
/// A thick border.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```plain
|
||||
/// ┏━━━━━━━┓
|
||||
/// ┃ ┃
|
||||
/// ┗━━━━━━━┛
|
||||
/// ```
|
||||
Thick,
|
||||
/// A border with a single line on the inside of a half block.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```plain
|
||||
/// ▗▄▄▄▄▄▄▄▖
|
||||
/// ▐ ▌
|
||||
/// ▐ ▌
|
||||
/// ▝▀▀▀▀▀▀▀▘
|
||||
QuadrantInside,
|
||||
|
||||
/// A border with a single line on the outside of a half block.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```plain
|
||||
/// ▛▀▀▀▀▀▀▀▜
|
||||
/// ▌ ▐
|
||||
/// ▌ ▐
|
||||
/// ▙▄▄▄▄▄▄▄▟
|
||||
QuadrantOutside,
|
||||
}
|
||||
|
||||
impl BorderType {
|
||||
/// Convert this `BorderType` into the corresponding [`Set`](border::Set) of border symbols.
|
||||
pub const fn border_symbols(border_type: Self) -> border::Set {
|
||||
match border_type {
|
||||
Self::Plain => border::PLAIN,
|
||||
Self::Rounded => border::ROUNDED,
|
||||
Self::Double => border::DOUBLE,
|
||||
Self::Thick => border::THICK,
|
||||
Self::QuadrantInside => border::QUADRANT_INSIDE,
|
||||
Self::QuadrantOutside => border::QUADRANT_OUTSIDE,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert this `BorderType` into the corresponding [`Set`](border::Set) of border symbols.
|
||||
pub const fn to_border_set(self) -> border::Set {
|
||||
Self::border_symbols(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Implement the `Debug` trait for the `Borders` bitflags. This is a manual implementation to
|
||||
/// display the flags in a more readable way. The default implementation would display the
|
||||
/// flags as 'Border(0x0)' for `Borders::NONE` for example.
|
||||
@@ -55,12 +150,16 @@ impl fmt::Debug for Borders {
|
||||
/// and RIGHT.
|
||||
///
|
||||
/// When used with NONE you should consider omitting this completely. For ALL you should consider
|
||||
/// [`Block::bordered()`](crate::widgets::Block::bordered) instead.
|
||||
/// [`Block::bordered()`](crate::block::Block::bordered) instead.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::{border, prelude::*, widgets::*};
|
||||
/// use ratatui::{
|
||||
/// border,
|
||||
/// widgets::{Block, Borders},
|
||||
/// };
|
||||
///
|
||||
/// Block::new()
|
||||
/// .title("Construct Borders and use them in place")
|
||||
/// .borders(border!(TOP, BOTTOM));
|
||||
@@ -69,7 +168,7 @@ impl fmt::Debug for Borders {
|
||||
/// `border!` can be called with any number of individual sides:
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::{border, prelude::*, widgets::*};
|
||||
/// use ratatui::{border, widgets::Borders};
|
||||
/// let right_open = border!(TOP, LEFT, BOTTOM);
|
||||
/// assert_eq!(right_open, Borders::TOP | Borders::LEFT | Borders::BOTTOM);
|
||||
/// ```
|
||||
@@ -77,12 +176,12 @@ impl fmt::Debug for Borders {
|
||||
/// Single borders work but using `Borders::` directly would be simpler.
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::{border, prelude::*, widgets::*};
|
||||
/// use ratatui::{border, widgets::Borders};
|
||||
///
|
||||
/// assert_eq!(border!(TOP), Borders::TOP);
|
||||
/// assert_eq!(border!(ALL), Borders::ALL);
|
||||
/// assert_eq!(border!(), Borders::NONE);
|
||||
/// ```
|
||||
#[cfg(feature = "macros")]
|
||||
#[macro_export]
|
||||
macro_rules! border {
|
||||
() => {
|
||||
@@ -119,11 +218,6 @@ mod tests {
|
||||
"TOP | BOTTOM"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "macros"))]
|
||||
mod macro_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn can_be_const() {
|
||||
@@ -10,9 +10,16 @@
|
||||
//! [`Monthly`] has several controls for what should be displayed
|
||||
use std::collections::HashMap;
|
||||
|
||||
use ratatui_core::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Constraint, Layout, Rect},
|
||||
style::Style,
|
||||
text::{Line, Span},
|
||||
widgets::Widget,
|
||||
};
|
||||
use time::{Date, Duration, OffsetDateTime};
|
||||
|
||||
use crate::{prelude::*, widgets::Block};
|
||||
use crate::block::{Block, BlockExt};
|
||||
|
||||
/// Display a month calendar for the month containing `display_date`
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
@@ -46,6 +53,8 @@ impl<'a, DS: DateStyler> Monthly<'a, DS> {
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// [`Color`]: ratatui_core::style::Color
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn show_surrounding<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.show_surrounding = Some(style.into());
|
||||
@@ -56,6 +65,8 @@ impl<'a, DS: DateStyler> Monthly<'a, DS> {
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// [`Color`]: ratatui_core::style::Color
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn show_weekdays_header<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.show_weekday = Some(style.into());
|
||||
@@ -66,6 +77,8 @@ impl<'a, DS: DateStyler> Monthly<'a, DS> {
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// [`Color`]: ratatui_core::style::Color
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn show_month_header<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.show_month = Some(style.into());
|
||||
@@ -76,6 +89,8 @@ impl<'a, DS: DateStyler> Monthly<'a, DS> {
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// [`Color`]: ratatui_core::style::Color
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn default_style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.default_style = style.into();
|
||||
@@ -121,13 +136,13 @@ impl<'a, DS: DateStyler> Monthly<'a, DS> {
|
||||
|
||||
impl<DS: DateStyler> Widget for Monthly<'_, DS> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.render_ref(area, buf);
|
||||
Widget::render(&self, area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl<DS: DateStyler> WidgetRef for Monthly<'_, DS> {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
self.block.render_ref(area, buf);
|
||||
impl<DS: DateStyler> Widget for &Monthly<'_, DS> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.block.as_ref().render(area, buf);
|
||||
let inner = self.block.inner_if_some(area);
|
||||
self.render_monthly(inner, buf);
|
||||
}
|
||||
@@ -177,7 +192,9 @@ impl<DS: DateStyler> Monthly<'_, DS> {
|
||||
spans.push(self.format_date(curr_day));
|
||||
curr_day += Duration::DAY;
|
||||
}
|
||||
buf.set_line(days_area.x, y, &spans.into(), area.width);
|
||||
if buf.area.height > y {
|
||||
buf.set_line(days_area.x, y, &spans.into(), area.width);
|
||||
}
|
||||
y += 1;
|
||||
}
|
||||
}
|
||||
@@ -199,6 +216,8 @@ impl CalendarEventStore {
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// [`Color`]: ratatui_core::style::Color
|
||||
pub fn today<S: Into<Style>>(style: S) -> Self {
|
||||
let mut res = Self::default();
|
||||
res.add(
|
||||
@@ -214,6 +233,8 @@ impl CalendarEventStore {
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// [`Color`]: ratatui_core::style::Color
|
||||
pub fn add<S: Into<Style>>(&mut self, date: Date, style: S) {
|
||||
// to simplify style nonsense, last write wins
|
||||
let _ = self.0.insert(date, style.into());
|
||||
@@ -245,6 +266,7 @@ impl Default for CalendarEventStore {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ratatui_core::style::Color;
|
||||
use time::Month;
|
||||
|
||||
use super::*;
|
||||
@@ -12,16 +12,18 @@
|
||||
//! - [`Rectangle`]: A basic rectangle
|
||||
//!
|
||||
//! You can also implement your own custom [`Shape`]s.
|
||||
mod circle;
|
||||
mod line;
|
||||
mod map;
|
||||
mod points;
|
||||
mod rectangle;
|
||||
mod world;
|
||||
|
||||
use std::{fmt, iter::zip};
|
||||
|
||||
use itertools::Itertools;
|
||||
use ratatui_core::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::{Color, Style},
|
||||
symbols::{self, Marker},
|
||||
text::Line as TextLine,
|
||||
widgets::Widget,
|
||||
};
|
||||
|
||||
pub use self::{
|
||||
circle::Circle,
|
||||
@@ -30,7 +32,14 @@ pub use self::{
|
||||
points::Points,
|
||||
rectangle::Rectangle,
|
||||
};
|
||||
use crate::{prelude::*, symbols::Marker, text::Line as TextLine, widgets::Block};
|
||||
use crate::block::{Block, BlockExt};
|
||||
|
||||
mod circle;
|
||||
mod line;
|
||||
mod map;
|
||||
mod points;
|
||||
mod rectangle;
|
||||
mod world;
|
||||
|
||||
/// Something that can be drawn on a [`Canvas`].
|
||||
///
|
||||
@@ -353,10 +362,16 @@ impl<'a, 'b> Painter<'a, 'b> {
|
||||
/// and `[0, height - 1]` respectively. The resolution of the grid is used to convert the
|
||||
/// `(x, y)` coordinates to the location of a point on the grid.
|
||||
///
|
||||
/// Points are rounded to the nearest grid cell (with points exactly in the center of a cell
|
||||
/// rounding up).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::{prelude::*, widgets::canvas::*};
|
||||
/// use ratatui::{
|
||||
/// symbols,
|
||||
/// widgets::canvas::{Context, Painter},
|
||||
/// };
|
||||
///
|
||||
/// let mut ctx = Context::new(2, 2, [1.0, 2.0], [0.0, 2.0], symbols::Marker::Braille);
|
||||
/// let mut painter = Painter::from(&mut ctx);
|
||||
@@ -365,7 +380,7 @@ impl<'a, 'b> Painter<'a, 'b> {
|
||||
/// assert_eq!(point, Some((0, 7)));
|
||||
///
|
||||
/// let point = painter.get_point(1.5, 1.0);
|
||||
/// assert_eq!(point, Some((1, 3)));
|
||||
/// assert_eq!(point, Some((2, 4)));
|
||||
///
|
||||
/// let point = painter.get_point(0.0, 0.0);
|
||||
/// assert_eq!(point, None);
|
||||
@@ -377,20 +392,18 @@ impl<'a, 'b> Painter<'a, 'b> {
|
||||
/// assert_eq!(point, Some((0, 0)));
|
||||
/// ```
|
||||
pub fn get_point(&self, x: f64, y: f64) -> Option<(usize, usize)> {
|
||||
let left = self.context.x_bounds[0];
|
||||
let right = self.context.x_bounds[1];
|
||||
let top = self.context.y_bounds[1];
|
||||
let bottom = self.context.y_bounds[0];
|
||||
let [left, right] = self.context.x_bounds;
|
||||
let [bottom, top] = self.context.y_bounds;
|
||||
if x < left || x > right || y < bottom || y > top {
|
||||
return None;
|
||||
}
|
||||
let width = (self.context.x_bounds[1] - self.context.x_bounds[0]).abs();
|
||||
let height = (self.context.y_bounds[1] - self.context.y_bounds[0]).abs();
|
||||
if width == 0.0 || height == 0.0 {
|
||||
let width = right - left;
|
||||
let height = top - bottom;
|
||||
if width <= 0.0 || height <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
let x = ((x - left) * (self.resolution.0 - 1.0) / width) as usize;
|
||||
let y = ((top - y) * (self.resolution.1 - 1.0) / height) as usize;
|
||||
let x = ((x - left) * (self.resolution.0 - 1.0) / width).round() as usize;
|
||||
let y = ((top - y) * (self.resolution.1 - 1.0) / height).round() as usize;
|
||||
Some((x, y))
|
||||
}
|
||||
|
||||
@@ -399,7 +412,11 @@ impl<'a, 'b> Painter<'a, 'b> {
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::{prelude::*, widgets::canvas::*};
|
||||
/// use ratatui::{
|
||||
/// style::Color,
|
||||
/// symbols,
|
||||
/// widgets::canvas::{Context, Painter},
|
||||
/// };
|
||||
///
|
||||
/// let mut ctx = Context::new(1, 1, [0.0, 2.0], [0.0, 2.0], symbols::Marker::Braille);
|
||||
/// let mut painter = Painter::from(&mut ctx);
|
||||
@@ -408,6 +425,25 @@ impl<'a, 'b> Painter<'a, 'b> {
|
||||
pub fn paint(&mut self, x: usize, y: usize, color: Color) {
|
||||
self.context.grid.paint(x, y, color);
|
||||
}
|
||||
|
||||
/// Canvas context bounds by axis.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::{
|
||||
/// style::Color,
|
||||
/// symbols,
|
||||
/// widgets::canvas::{Context, Painter},
|
||||
/// };
|
||||
///
|
||||
/// let mut ctx = Context::new(1, 1, [0.0, 2.0], [0.0, 2.0], symbols::Marker::Braille);
|
||||
/// let mut painter = Painter::from(&mut ctx);
|
||||
/// assert_eq!(painter.bounds(), (&[0.0, 2.0], &[0.0, 2.0]));
|
||||
/// ```
|
||||
pub fn bounds(&self) -> (&[f64; 2], &[f64; 2]) {
|
||||
(&self.context.x_bounds, &self.context.y_bounds)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> From<&'a mut Context<'b>> for Painter<'a, 'b> {
|
||||
@@ -423,9 +459,7 @@ impl<'a, 'b> From<&'a mut Context<'b>> for Painter<'a, 'b> {
|
||||
/// Holds the state of the [`Canvas`] when painting to it.
|
||||
///
|
||||
/// This is used by the [`Canvas`] widget to draw shapes on the grid. It can be useful to think of
|
||||
/// this as similar to the [`Frame`] struct that is used to draw widgets on the terminal.
|
||||
///
|
||||
/// [`Frame`]: crate::prelude::Frame
|
||||
/// this as similar to the `Frame` struct that is used to draw widgets on the terminal.
|
||||
#[derive(Debug)]
|
||||
pub struct Context<'a> {
|
||||
x_bounds: [f64; 2],
|
||||
@@ -449,7 +483,7 @@ impl<'a> Context<'a> {
|
||||
/// example, if you want to draw a map of the world, you might want to use the following bounds:
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::{prelude::*, widgets::canvas::*};
|
||||
/// use ratatui::{symbols, widgets::canvas::Context};
|
||||
///
|
||||
/// let ctx = Context::new(
|
||||
/// 100,
|
||||
@@ -513,6 +547,8 @@ impl<'a> Context<'a> {
|
||||
///
|
||||
/// Note that the text is always printed on top of the canvas and is **not** affected by the
|
||||
/// layers.
|
||||
///
|
||||
/// [`Text`]: ratatui_core::text::Text
|
||||
pub fn print<T>(&mut self, x: f64, y: f64, line: T)
|
||||
where
|
||||
T: Into<TextLine<'a>>,
|
||||
@@ -563,7 +599,10 @@ impl<'a> Context<'a> {
|
||||
/// ```
|
||||
/// use ratatui::{
|
||||
/// style::Color,
|
||||
/// widgets::{canvas::*, *},
|
||||
/// widgets::{
|
||||
/// canvas::{Canvas, Line, Map, MapResolution, Rectangle},
|
||||
/// Block,
|
||||
/// },
|
||||
/// };
|
||||
///
|
||||
/// Canvas::default()
|
||||
@@ -690,15 +729,15 @@ where
|
||||
/// cell. This allows for more flexibility than the `BrailleGrid` which only supports a single
|
||||
/// foreground color for each 2x4 dots cell.
|
||||
///
|
||||
/// [`Braille`]: crate::symbols::Marker::Braille
|
||||
/// [`HalfBlock`]: crate::symbols::Marker::HalfBlock
|
||||
/// [`Dot`]: crate::symbols::Marker::Dot
|
||||
/// [`Block`]: crate::symbols::Marker::Block
|
||||
/// [`Braille`]: ratatui_core::symbols::Marker::Braille
|
||||
/// [`HalfBlock`]: ratatui_core::symbols::Marker::HalfBlock
|
||||
/// [`Dot`]: ratatui_core::symbols::Marker::Dot
|
||||
/// [`Block`]: ratatui_core::symbols::Marker::Block
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::{prelude::*, widgets::canvas::*};
|
||||
/// use ratatui::{symbols, widgets::canvas::Canvas};
|
||||
///
|
||||
/// Canvas::default()
|
||||
/// .marker(symbols::Marker::Braille)
|
||||
@@ -728,16 +767,16 @@ where
|
||||
F: Fn(&mut Context),
|
||||
{
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.render_ref(area, buf);
|
||||
Widget::render(&self, area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl<F> WidgetRef for Canvas<'_, F>
|
||||
impl<F> Widget for &Canvas<'_, F>
|
||||
where
|
||||
F: Fn(&mut Context),
|
||||
{
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
self.block.render_ref(area, buf);
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.block.as_ref().render(area, buf);
|
||||
let canvas_area = self.block.inner_if_some(area);
|
||||
if canvas_area.is_empty() {
|
||||
return;
|
||||
@@ -809,9 +848,9 @@ where
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use indoc::indoc;
|
||||
use ratatui_core::buffer::Cell;
|
||||
|
||||
use super::*;
|
||||
use crate::buffer::Cell;
|
||||
|
||||
// helper to test the canvas checks that drawing a vertical and horizontal line
|
||||
// results in the expected output
|
||||
@@ -1,7 +1,6 @@
|
||||
use crate::{
|
||||
style::Color,
|
||||
widgets::canvas::{Painter, Shape},
|
||||
};
|
||||
use ratatui_core::style::Color;
|
||||
|
||||
use crate::canvas::{Painter, Shape};
|
||||
|
||||
/// A circle with a given center and radius and with a given color
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
@@ -31,17 +30,12 @@ impl Shape for Circle {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::Color,
|
||||
symbols::Marker,
|
||||
widgets::{
|
||||
canvas::{Canvas, Circle},
|
||||
Widget,
|
||||
},
|
||||
use ratatui_core::{
|
||||
buffer::Buffer, layout::Rect, style::Color, symbols::Marker, widgets::Widget,
|
||||
};
|
||||
|
||||
use crate::canvas::{Canvas, Circle};
|
||||
|
||||
#[test]
|
||||
fn test_it_draws_a_circle() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 5));
|
||||
@@ -59,10 +53,10 @@ mod tests {
|
||||
.y_bounds([-10.0, 10.0]);
|
||||
canvas.render(buffer.area, &mut buffer);
|
||||
let expected = Buffer::with_lines([
|
||||
" ⢀⣠⢤⣀ ",
|
||||
" ⢰⠋ ⠈⣇",
|
||||
" ⠘⣆⡀ ⣠⠇",
|
||||
" ⠉⠉⠁ ",
|
||||
" ⣀⣀⣀ ",
|
||||
" ⡞⠁ ⠈⢣",
|
||||
" ⢇⡀ ⢀⡼",
|
||||
" ⠉⠉⠉ ",
|
||||
" ",
|
||||
]);
|
||||
assert_eq!(buffer, expected);
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
style::Color,
|
||||
widgets::canvas::{Painter, Shape},
|
||||
};
|
||||
use line_clipping::{cohen_sutherland, LineSegment, Point, Window};
|
||||
use ratatui_core::style::Color;
|
||||
|
||||
use crate::canvas::{Painter, Shape};
|
||||
|
||||
/// A line from `(x1, y1)` to `(x2, y2)` with the given color
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
@@ -32,13 +32,21 @@ impl Line {
|
||||
}
|
||||
|
||||
impl Shape for Line {
|
||||
#[allow(clippy::similar_names)]
|
||||
fn draw(&self, painter: &mut Painter) {
|
||||
let Some((x1, y1)) = painter.get_point(self.x1, self.y1) else {
|
||||
let (x_bounds, y_bounds) = painter.bounds();
|
||||
let Some((world_x1, world_y1, world_x2, world_y2)) =
|
||||
clip_line(x_bounds, y_bounds, self.x1, self.y1, self.x2, self.y2)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some((x2, y2)) = painter.get_point(self.x2, self.y2) else {
|
||||
let Some((x1, y1)) = painter.get_point(world_x1, world_y1) else {
|
||||
return;
|
||||
};
|
||||
let Some((x2, y2)) = painter.get_point(world_x2, world_y2) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let (dx, x_range) = if x2 >= x1 {
|
||||
(x2 - x1, x1..=x2)
|
||||
} else {
|
||||
@@ -72,6 +80,27 @@ impl Shape for Line {
|
||||
}
|
||||
}
|
||||
|
||||
fn clip_line(
|
||||
&[xmin, xmax]: &[f64; 2],
|
||||
&[ymin, ymax]: &[f64; 2],
|
||||
x1: f64,
|
||||
y1: f64,
|
||||
x2: f64,
|
||||
y2: f64,
|
||||
) -> Option<(f64, f64, f64, f64)> {
|
||||
if let Some(LineSegment {
|
||||
p1: Point { x: x1, y: y1 },
|
||||
p2: Point { x: x2, y: y2 },
|
||||
}) = cohen_sutherland::clip_line(
|
||||
LineSegment::new(Point::new(x1, y1), Point::new(x2, y2)),
|
||||
Window::new(xmin, xmax, ymin, ymax),
|
||||
) {
|
||||
Some((x1, y1, x2, y2))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_line_low(painter: &mut Painter, x1: usize, y1: usize, x2: usize, y2: usize, color: Color) {
|
||||
let dx = (x2 - x1) as isize;
|
||||
let dy = (y2 as isize - y1 as isize).abs();
|
||||
@@ -112,21 +141,24 @@ fn draw_line_high(painter: &mut Painter, x1: usize, y1: usize, x2: usize, y2: us
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
use ratatui_core::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::{Style, Stylize},
|
||||
symbols::Marker,
|
||||
widgets::{canvas::Canvas, Widget},
|
||||
widgets::Widget,
|
||||
};
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
use crate::canvas::Canvas;
|
||||
|
||||
#[rstest]
|
||||
#[case::off_grid(&Line::new(-1.0, -1.0, 10.0, 10.0, Color::Red), [" "; 10])]
|
||||
#[case::off_grid(&Line::new(0.0, 0.0, 11.0, 11.0, Color::Red), [" "; 10])]
|
||||
#[case::horizontal(&Line::new(0.0, 0.0, 10.0, 0.0, Color::Red), [
|
||||
#[case::off_grid1(&Line::new(-1.0, 0.0, -1.0, 10.0, Color::Red), [" "; 10])]
|
||||
#[case::off_grid2(&Line::new(0.0, -1.0, 10.0, -1.0, Color::Red), [" "; 10])]
|
||||
#[case::off_grid3(&Line::new(-10.0, 5.0, -1.0, 5.0, Color::Red), [" "; 10])]
|
||||
#[case::off_grid4(&Line::new(5.0, 11.0, 5.0, 20.0, Color::Red), [" "; 10])]
|
||||
#[case::off_grid5(&Line::new(-10.0, 0.0, 5.0, 0.0, Color::Red), [
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
@@ -136,50 +168,99 @@ mod tests {
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
"••••••••••",
|
||||
"•••••• ",
|
||||
])]
|
||||
#[case::horizontal(&Line::new(10.0, 10.0, 0.0, 10.0, Color::Red), [
|
||||
"••••••••••",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
])]
|
||||
#[case::vertical(&Line::new(0.0, 0.0, 0.0, 10.0, Color::Red), ["• "; 10])]
|
||||
#[case::vertical(&Line::new(10.0, 10.0, 10.0, 0.0, Color::Red), [" •"; 10])]
|
||||
// dy < dx, x1 < x2
|
||||
#[case::diagonal(&Line::new(0.0, 0.0, 10.0, 5.0, Color::Red), [
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
#[case::off_grid6(&Line::new(-1.0, -1.0, 10.0, 10.0, Color::Red), [
|
||||
" •",
|
||||
" •• ",
|
||||
" •• ",
|
||||
" •• ",
|
||||
" •• ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
"• ",
|
||||
])]
|
||||
#[case::off_grid7(&Line::new(0.0, 0.0, 11.0, 11.0, Color::Red), [
|
||||
" •",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
"• ",
|
||||
])]
|
||||
#[case::off_grid8(&Line::new(-1.0, -1.0, 11.0, 11.0, Color::Red), [
|
||||
" •",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
"• ",
|
||||
])]
|
||||
#[case::horizontal1(&Line::new(0.0, 0.0, 10.0, 0.0, Color::Red), [
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
"••••••••••",
|
||||
])]
|
||||
#[case::horizontal2(&Line::new(10.0, 10.0, 0.0, 10.0, Color::Red), [
|
||||
"••••••••••",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
])]
|
||||
#[case::vertical1(&Line::new(0.0, 0.0, 0.0, 10.0, Color::Red), ["• "; 10])]
|
||||
#[case::vertical2(&Line::new(10.0, 10.0, 10.0, 0.0, Color::Red), [" •"; 10])]
|
||||
// dy < dx, x1 < x2
|
||||
#[case::diagonal1(&Line::new(0.0, 0.0, 10.0, 5.0, Color::Red), [
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ••",
|
||||
" •• ",
|
||||
" •• ",
|
||||
" •• ",
|
||||
"•• ",
|
||||
])]
|
||||
// dy < dx, x1 > x2
|
||||
#[case::diagonal(&Line::new(10.0, 0.0, 0.0, 5.0, Color::Red), [
|
||||
#[case::diagonal2(&Line::new(10.0, 0.0, 0.0, 5.0, Color::Red), [
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
"• ",
|
||||
" •• ",
|
||||
" •• ",
|
||||
" •• ",
|
||||
" •• ",
|
||||
" •",
|
||||
" ",
|
||||
"•• ",
|
||||
" •• ",
|
||||
" •• ",
|
||||
" •• ",
|
||||
" ••",
|
||||
])]
|
||||
// dy > dx, y1 < y2
|
||||
#[case::diagonal(&Line::new(0.0, 0.0, 5.0, 10.0, Color::Red), [
|
||||
#[case::diagonal3(&Line::new(0.0, 0.0, 5.0, 10.0, Color::Red), [
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
@@ -189,11 +270,9 @@ mod tests {
|
||||
" • ",
|
||||
" • ",
|
||||
"• ",
|
||||
"• ",
|
||||
])]
|
||||
// dy > dx, y1 > y2
|
||||
#[case::diagonal(&Line::new(0.0, 10.0, 5.0, 0.0, Color::Red), [
|
||||
"• ",
|
||||
#[case::diagonal4(&Line::new(0.0, 10.0, 5.0, 0.0, Color::Red), [
|
||||
"• ",
|
||||
" • ",
|
||||
" • ",
|
||||
@@ -203,11 +282,12 @@ mod tests {
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
])]
|
||||
fn tests<'expected_line, ExpectedLines>(#[case] line: &Line, #[case] expected: ExpectedLines)
|
||||
where
|
||||
ExpectedLines: IntoIterator,
|
||||
ExpectedLines::Item: Into<crate::text::Line<'expected_line>>,
|
||||
ExpectedLines::Item: Into<ratatui_core::text::Line<'expected_line>>,
|
||||
{
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 10));
|
||||
let canvas = Canvas::default()
|
||||
206
ratatui-widgets/src/canvas/map.rs
Normal file
206
ratatui-widgets/src/canvas/map.rs
Normal file
@@ -0,0 +1,206 @@
|
||||
use ratatui_core::style::Color;
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
use crate::canvas::{
|
||||
world::{WORLD_HIGH_RESOLUTION, WORLD_LOW_RESOLUTION},
|
||||
Painter, Shape,
|
||||
};
|
||||
|
||||
/// Defines how many points are going to be used to draw a [`Map`].
|
||||
///
|
||||
/// You generally want a [high](MapResolution::High) resolution map.
|
||||
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub enum MapResolution {
|
||||
/// A lesser resolution for the [`Map`] [`Shape`].
|
||||
///
|
||||
/// Contains about 1000 points.
|
||||
#[default]
|
||||
Low,
|
||||
/// A higher resolution for the [`Map`] [`Shape`].
|
||||
///
|
||||
/// Contains about 5000 points, you likely want to use [`Marker::Braille`] with this.
|
||||
///
|
||||
/// [`Marker::Braille`]: (ratatui_core::symbols::Marker::Braille)
|
||||
High,
|
||||
}
|
||||
|
||||
impl MapResolution {
|
||||
const fn data(self) -> &'static [(f64, f64)] {
|
||||
match self {
|
||||
Self::Low => &WORLD_LOW_RESOLUTION,
|
||||
Self::High => &WORLD_HIGH_RESOLUTION,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A world map
|
||||
///
|
||||
/// A world map can be rendered with different [resolutions](MapResolution) and [colors](Color).
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Map {
|
||||
/// The resolution of the map.
|
||||
///
|
||||
/// This is the number of points used to draw the map.
|
||||
pub resolution: MapResolution,
|
||||
/// Map color
|
||||
///
|
||||
/// This is the color of the points of the map.
|
||||
pub color: Color,
|
||||
}
|
||||
|
||||
impl Shape for Map {
|
||||
fn draw(&self, painter: &mut Painter) {
|
||||
for (x, y) in self.resolution.data() {
|
||||
if let Some((x, y)) = painter.get_point(*x, *y) {
|
||||
painter.paint(x, y, self.color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ratatui_core::{buffer::Buffer, layout::Rect, symbols::Marker, widgets::Widget};
|
||||
use strum::ParseError;
|
||||
|
||||
use super::*;
|
||||
use crate::canvas::Canvas;
|
||||
|
||||
#[test]
|
||||
fn map_resolution_to_string() {
|
||||
assert_eq!(MapResolution::Low.to_string(), "Low");
|
||||
assert_eq!(MapResolution::High.to_string(), "High");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn map_resolution_from_str() {
|
||||
assert_eq!("Low".parse(), Ok(MapResolution::Low));
|
||||
assert_eq!("High".parse(), Ok(MapResolution::High));
|
||||
assert_eq!(
|
||||
"".parse::<MapResolution>(),
|
||||
Err(ParseError::VariantNotFound)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default() {
|
||||
let map = Map::default();
|
||||
assert_eq!(map.resolution, MapResolution::Low);
|
||||
assert_eq!(map.color, Color::Reset);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn draw_low() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 80, 40));
|
||||
let canvas = Canvas::default()
|
||||
.marker(Marker::Dot)
|
||||
.x_bounds([-180.0, 180.0])
|
||||
.y_bounds([-90.0, 90.0])
|
||||
.paint(|context| {
|
||||
context.draw(&Map::default());
|
||||
});
|
||||
canvas.render(buffer.area, &mut buffer);
|
||||
let expected = Buffer::with_lines([
|
||||
" ",
|
||||
" • ",
|
||||
" • •• •••••••• •• •••• ••••• ••• •• ••• ",
|
||||
" ••••••••••••••• • •••• • • ••••••• ••• ",
|
||||
" • •••• ••••••••••••••• •• •• • ••• •• •••• •• ••••••• ••• ",
|
||||
"••••• •••••••••••• •••• • •••••• •••• • ••• ••••• •",
|
||||
" •• • • •••• •••••••• •••• •• • •• • ••• •• •••",
|
||||
" •••• ••• •••••• ••••• • •• •••••• • ••••• ",
|
||||
"••••• ••• • •• •• ••••••• •• •• •• ",
|
||||
" •• •••• ••••• •• • • • •• ",
|
||||
" • • ••••••• •• •••• ••• •• • •• • •• ",
|
||||
" • •• ••••••••• • •• •••• • ",
|
||||
" •• •• • • • •• • ••••• ",
|
||||
" •• ••• • •••• • • • ",
|
||||
" • • •• • •• •• • • ",
|
||||
" •• • ••••••• • • • • • •• • ",
|
||||
" ••••••••• • •• • • • •• • •• ",
|
||||
" •• •• • ••• • ••• •• ",
|
||||
" ••• • • • • • •• ••• ••• ",
|
||||
" • • •• • •• ",
|
||||
" • • ••• • • ••• ••• ",
|
||||
" • • • • • ••• ",
|
||||
" • • • • • • • ",
|
||||
" • • • • ••• •• •",
|
||||
" • • • • •• • • • ",
|
||||
" • • • • • ",
|
||||
" • • • • • ",
|
||||
" •• •• •• •• • • ",
|
||||
" • • ••• •• ",
|
||||
" • • •• •• ",
|
||||
" • • ",
|
||||
" ••••• ",
|
||||
" ",
|
||||
" •• ",
|
||||
" ••• • • ••••• • •••• • • •• •• •• ",
|
||||
" • • • •••••• ••••••••• • •• •• ••• ",
|
||||
"• ••• •••• •••• • • • ••• • • ••• •",
|
||||
" •• • • •• • •• •• ",
|
||||
"• • • ",
|
||||
" ",
|
||||
]);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn draw_high() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 80, 40));
|
||||
let canvas = Canvas::default()
|
||||
.marker(Marker::Braille)
|
||||
.x_bounds([-180.0, 180.0])
|
||||
.y_bounds([-90.0, 90.0])
|
||||
.paint(|context| {
|
||||
context.draw(&Map {
|
||||
resolution: MapResolution::High,
|
||||
..Default::default()
|
||||
});
|
||||
});
|
||||
canvas.render(buffer.area, &mut buffer);
|
||||
let expected = Buffer::with_lines([
|
||||
" ",
|
||||
" ⢀⣀⣤⠄⠤⠤⣤⣀⡀⣀⣀⡄⠄⢄⣀⣄⡄⢀⡀ ",
|
||||
" ⢀⣀⣤⠰⢤⣼⡯⢽⡟⣀⢶⣺⡛⠁ ⠈⢰⠃⠁ ⢖⣒⣾⡟⠂ ⠈⠛⠁ ⠺⢩⢖⡄ ",
|
||||
" ⡬⢍⣿⣟⣿⣻⣿⣿⣿⡾⣯⡀⠈⠁⠁⢦ ⢀⡿ ⠈ ⢠⢶⠘⠋⡁⣀⢠⠤⠖⠘⠉⠁⠈⠼⡧⡄⣄⡀ ⢫⣗⠒⠆ ",
|
||||
"⣓ ⣠⠖⠓⠒⠢⠤⢄⠤⠶⠽⠽⣶⣃⣽⡮⣿⡷⣗⣤⡭⣍⢓⡄ ⠸⣷ ⢀⣀⠿⠇ ⢀⠔⠒⠲⠄⢄⢀⡀⢙⣑⡄⠴⡍⣟⠉ ⠑⠉⠉ ⠑⠐⠦⠤⣤⠤⢞",
|
||||
"⠶⢧⣗⢾⡆ ⠈⠈⠁⠈⠉⢀⣹⣶⣩⣽⣐⢮⠃ ⣇ ⢀⡔⠊ ⢰⣖⣲ ⢀⡐⠁⣰⠦ ⢲⣶⠛⠋ ⠐⠋ ⡤",
|
||||
" ⠉⣮⣀⣀⣴⡤⣠⡀ ⡎ ⠛⢫⠙⢫⢫ ⠈⠦⠼ ⡃⡀⢸⠼⣤⡄ ⡀⣀⣀⡐⡶⣣⢤⠖⠉",
|
||||
" ⢀⡽⠟⠃ ⠈⠱⡀ ⠙⠢⣀⣨⠆⠈⠁⢧⡀ ⣸⣷ ⢹⣷⣼⣸⠃ ⢀⡐⢀ ⠁⡚⣨⠆ ",
|
||||
" ⠘⢳⡀ ⠈⠾ ⣀⣀⣽ ⠸⢼⣇⡧⠋⠉⠁ ⠉⣿ ⠢⠂ ",
|
||||
" ⠈⢻ ⠜⢹⣵⠻⠇ ⠈⢻ ⢀⡀ ⢠⣠⡤ ⢀⢤ ⢰⣯ ",
|
||||
" ⢼ ⢀⣾⠛⠉ ⠐⡖⠒⡰⢺⣞⣵⡄⢀⣏⡭⣙⡄⢕⢫⡀ ⢀ ⢠⠖⢱⡿⠃ ",
|
||||
" ⠸ ⠠⡎ ⠰⣅⣰⣃⣘⡣⡿⢻⡿⣁⣀ ⠸⣽ ⠐⣿⣽⣫ ⡸⡇ ",
|
||||
" ⠳⣄ ⡰⠃ ⢀⠎⠉ ⢧⡀⣠⣛⠈⢻ ⢻⠘⢺⡿⠚⠁ ",
|
||||
" ⢻⣇ ⣠⠲⠖⢲⡇ ⡸ ⠉⠃⠈⠉⣿ ⢰⣆ ⢸ ⠈⠁ ",
|
||||
" ⠈⢿⣆ ⡟ ⣘⣻ ⡸ ⢸⢇ ⠈⠯⢿⡒⠲⡀ ⢀⡀ ⣀⢾ ",
|
||||
" ⠈⢳ ⠸⡀⢳⣠⢾⠉⢹⣦⣤⣀ ⡇ ⡿⡄ ⢰⠃ ⠑⡂ ⢠⠏⢣ ⣼⡮⠁⢈⡀ ",
|
||||
" ⠙⠲⢆⡿⢦⠈⠉⠁⠁ ⡇ ⠱⣇⣀⠼⠃ ⡃⢰⠃ ⠸⢶ ⠘⠄ ⢾⡁ ",
|
||||
" ⠙⣾⣀⡴⡶⢤⣤ ⢳ ⠻⠵⡆ ⠸⣸ ⢸⡳⡤⠃⢀⡾⣿ ",
|
||||
" ⠘⢻⠁ ⠈⠦⣄ ⢧⣀⣀⠤⣀ ⢐⠁ ⠈⠩⠆ ⣘⣧⠁ ⡸⡔⢿ ",
|
||||
" ⡸ ⢨ ⠁ ⠉⡇ ⢀⠎ ⢻⢿⠄⡴⢑⣧⡠⡄ ",
|
||||
" ⡇ ⠈⠋⠦⡄ ⠈⡆ ⢠⠃ ⢏⡇⢧⣼⣾⣧⣽⣿⠶⢤⡀⣤ ",
|
||||
" ⣇ ⠈⡇ ⢸ ⢸ ⠈⠶⣦⣄⣋⣁⡀⠸⣵⢠⣻⠋⠷⣄ ",
|
||||
" ⠰⡀ ⣰⠁ ⢘⠆ ⢸ ⢠⡀ ⠙⠋⢠⠦⡄⣷⠙⠃ ⠙ ",
|
||||
"⠄ ⠣⡀ ⡃ ⢸ ⣸⢡⢾⠆ ⡞⠛⠘⢧⡏⡆ ⠸⠄ ⡤",
|
||||
" ⠱ ⢠⠃ ⠸⡀ ⢸⠁⢸⢨ ⡤⠚ ⠱⡀ ⢦ ⠁",
|
||||
" ⠅ ⡖⠉ ⡇ ⡜ ⠸⠔ ⡇ ⢳ ",
|
||||
" ⡇ ⢀⠃ ⢱⡀ ⢰⠃ ⣇ ⢀⡀ ⢸ ",
|
||||
" ⢀⠃ ⡦⠏ ⠈⠷⠖⠃ ⠾⠴⠊⠁⠹⣦ ⡞ ⣄ ",
|
||||
" ⢸ ⡤⠃ ⠘⢲⠖⠃ ⣽⡆",
|
||||
" ⢸ ⣸⠁ ⠈⠿ ⢀⢼⠏ ",
|
||||
" ⠞ ⡗ ⣄ ⠈⠋ ",
|
||||
" ⢧⡼⡁⠲⠂ ",
|
||||
" ⠙⠉ ",
|
||||
" ⡀ ",
|
||||
" ⣴⠏⠁ ⣀⡤⢤⣀⣀ ⢀⣀⣤⣀⣀⡴⣄⡤⢤⣀⠤⠤⠴⣄⣀⡀ ",
|
||||
" ⣀⣀ ⣠⣿⡍⣆ ⣠⣤⣤⠤⠴⠶⠖⠲⠤⠔⠛⠒⠉ ⠈⠨⣇⠖⠋ ⠈⠉⠓⠢⠤⢄ ",
|
||||
" ⡀ ⣠⠤⠴⠒⠚⠛⠛⠒⠢⠤⠿⠙⠉⠉⠑⢋⣚⣉⠥⠚ ⢀⣀⡠⠟⠁ ⡴⠋ ",
|
||||
" ⠐⠶⣛⣫⡤ ⠐⢏⣀⣤⣤ ⣴⣋⢇⢀⣮⡥ ⣴⠓ ",
|
||||
"⠤⠤⠤⠤⡀⣈⢣⣠⡄ ⠉⠊⠉⠉⠉ ⠈⠓⠆⠤⠤",
|
||||
" ",
|
||||
]);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user