Compare commits
163 Commits
v0.26.3
...
jm/modular
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26163effdf | ||
|
|
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 | ||
|
|
730dfd4940 | ||
|
|
097ee86e39 | ||
|
|
3fdb5e8987 | ||
|
|
ec88bb81e5 | ||
|
|
f04bf855cb | ||
|
|
4753b7241b | ||
|
|
36fa3c11c1 | ||
|
|
69e8ed7db8 | ||
|
|
5f7a7fbe19 | ||
|
|
e6d2e04bcf | ||
|
|
45fcab7497 | ||
|
|
1b9bdd425c | ||
|
|
c68ee6c64a | ||
|
|
afe15349c8 | ||
|
|
fe4eeab676 | ||
|
|
a23ecd9b45 | ||
|
|
bb71e5ffd4 | ||
|
|
f2fa1ae9aa | ||
|
|
f687af7c0d | ||
|
|
f97e07c08a | ||
|
|
bb68bc6968 | ||
|
|
ffeb8e46b9 | ||
|
|
2fd5ae64bf | ||
|
|
82b70fd329 | ||
|
|
94328a2977 | ||
|
|
d468463fc6 | ||
|
|
6e7b4e4d55 | ||
|
|
864cd9ffef | ||
|
|
c08b522d34 | ||
|
|
716c93136e | ||
|
|
5eeb1ccbc4 | ||
|
|
f77503050f | ||
|
|
cd0d31c2dc | ||
|
|
41a910004d | ||
|
|
8433d0958b | ||
|
|
8d4a1026ab | ||
|
|
edc2af9822 | ||
|
|
a80a8a6a47 | ||
|
|
29c8c84fd0 | ||
|
|
c2d38509b4 | ||
|
|
b70cd03c02 | ||
|
|
476ac87c99 | ||
|
|
8857037bff | ||
|
|
e707ff11d1 | ||
|
|
a9fe4284ac | ||
|
|
ffc4300558 | ||
|
|
84cb16483a | ||
|
|
5b89bd04a8 | ||
|
|
32d0695cc2 | ||
|
|
cd93547db8 | ||
|
|
c245c13cc1 | ||
|
|
b2aa843b31 | ||
|
|
3ca920e881 | ||
|
|
b344f95b7c | ||
|
|
b304bb99bd | ||
|
|
be3eb75ea5 | ||
|
|
efef0d0dc0 | ||
|
|
663486f1e8 | ||
|
|
7ddfbc0010 | ||
|
|
3725262ca3 | ||
|
|
84f334163b | ||
|
|
03f3124c1d | ||
|
|
c34fb77818 | ||
|
|
6ce447c4f3 | ||
|
|
272d0591a7 | ||
|
|
e81663bec0 | ||
|
|
7e1bab049b | ||
|
|
379dab9cdb | ||
|
|
5b51018501 | ||
|
|
7bab9f0d80 | ||
|
|
6d210b3b6b | ||
|
|
935a7187c2 | ||
|
|
3bb374df88 | ||
|
|
50e5674a20 | ||
|
|
810da72f26 | ||
|
|
7c0665cb0e | ||
|
|
ccf83e6d76 | ||
|
|
60bd7f4814 | ||
|
|
55e0880d2f | ||
|
|
3e7458fdb8 | ||
|
|
32a0b26525 | ||
|
|
36d49e549b | ||
|
|
0a18dcb329 | ||
|
|
7ef2daee06 | ||
|
|
46977d8851 | ||
|
|
38bb196404 | ||
|
|
1908b06b4a | ||
|
|
3f2f2cd6ab | ||
|
|
efa965e1e8 | ||
|
|
127d706ee4 | ||
|
|
1365620606 | ||
|
|
cd64367e24 | ||
|
|
07efde5233 | ||
|
|
308c1df649 | ||
|
|
4bfdc15b80 | ||
|
|
7d175f85c1 | ||
|
|
d370aa75af | ||
|
|
7fdccafd52 | ||
|
|
10d778866e | ||
|
|
e6871b9e21 | ||
|
|
4f307e69db | ||
|
|
7f3efb02e6 | ||
|
|
7b45f74b71 | ||
|
|
1520ed9d10 | ||
|
|
2a74f9d8c1 | ||
|
|
d7ed6c8bad | ||
|
|
313135c68e | ||
|
|
8061813f32 | ||
|
|
74a32afbae | ||
|
|
4ce67fc84e | ||
|
|
df4b706674 | ||
|
|
8b447ec4d6 | ||
|
|
7a48c5b11b | ||
|
|
8cfc316bcc | ||
|
|
2f8a9363fc | ||
|
|
d92997105b | ||
|
|
42cda6d287 | ||
|
|
4f7791079e | ||
|
|
cf67ed9b88 | ||
|
|
8a60a561c9 | ||
|
|
35941809e1 | ||
|
|
73fd367a74 | ||
|
|
1de9a82b7a | ||
|
|
d6587bc6b0 | ||
|
|
f429f688da | ||
|
|
4770e71581 | ||
|
|
eef1afe915 | ||
|
|
257db6257f | ||
|
|
70df102de0 | ||
|
|
bf2036987f | ||
|
|
0b5fd6bf8e |
10
.github/CODEOWNERS
vendored
10
.github/CODEOWNERS
vendored
@@ -1,11 +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
|
||||
* @orhun @joshka @kdheepak @Valentin271 @EdJoPaTo
|
||||
|
||||
# Past maintainers
|
||||
# @mindoodoo @sayanarijit
|
||||
* @ratatui/maintainers
|
||||
|
||||
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -2,7 +2,7 @@
|
||||
name: Bug report
|
||||
about: Create an issue about a bug you encountered
|
||||
title: ''
|
||||
labels: bug
|
||||
labels: 'Type: Bug'
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
@@ -17,26 +17,22 @@ A detailed and complete issue is more likely to be processed quickly.
|
||||
A clear and concise description of what the bug is.
|
||||
-->
|
||||
|
||||
|
||||
## To Reproduce
|
||||
<!--
|
||||
Try to reduce the issue to a simple code sample exhibiting the problem.
|
||||
Ideally, fork the project and add a test or an example.
|
||||
-->
|
||||
|
||||
|
||||
## Expected behavior
|
||||
<!--
|
||||
A clear and concise description of what you expected to happen.
|
||||
-->
|
||||
|
||||
|
||||
## Screenshots
|
||||
<!--
|
||||
If applicable, add screenshots, gifs or videos to help explain your problem.
|
||||
-->
|
||||
|
||||
|
||||
## Environment
|
||||
<!--
|
||||
Add a description of the systems where you are observing the issue. For example:
|
||||
|
||||
6
.github/ISSUE_TEMPLATE/config.yml
vendored
6
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +1,11 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Frequently Asked Questions
|
||||
url: https://ratatui.rs/faq/
|
||||
about: Check the website FAQ section to see if your question has already been answered
|
||||
- name: Ratatui Forum
|
||||
url: https://forum.ratatui.rs
|
||||
about: Ask questions about ratatui on our Forum
|
||||
- name: Discord Chat
|
||||
url: https://discord.gg/pMCEU9hNEj
|
||||
about: Ask questions about ratatui on Discord
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -2,7 +2,7 @@
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
labels: 'Type: Enhancement'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
25
.github/workflows/bench_base.yml
vendored
Normal file
25
.github/workflows/bench_base.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
name: Run Benchmarks
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
benchmark_base_branch:
|
||||
name: Continuous Benchmarking with Bencher
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- uses: bencherdev/bencher@main
|
||||
- name: Track base branch benchmarks with Bencher
|
||||
run: |
|
||||
bencher run \
|
||||
--project ratatui-org \
|
||||
--token '${{ secrets.BENCHER_API_TOKEN }}' \
|
||||
--branch main \
|
||||
--testbed ubuntu-latest \
|
||||
--adapter rust_criterion \
|
||||
--err \
|
||||
cargo bench
|
||||
25
.github/workflows/bench_run_fork_pr.yml
vendored
Normal file
25
.github/workflows/bench_run_fork_pr.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
name: Run and Cache Benchmarks
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, edited, synchronize]
|
||||
|
||||
jobs:
|
||||
benchmark_fork_pr_branch:
|
||||
name: Run Fork PR Benchmarks
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Run Benchmarks
|
||||
run: cargo bench > benchmark_results.txt
|
||||
- name: Upload Benchmark Results
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: benchmark_results.txt
|
||||
path: ./benchmark_results.txt
|
||||
- name: Upload GitHub Pull Request Event
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: event.json
|
||||
path: ${{ github.event_path }}
|
||||
75
.github/workflows/bench_track_fork_pr.yml
vendored
Normal file
75
.github/workflows/bench_track_fork_pr.yml
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
name: Track Benchmarks with Bencher
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [Run and Cache Benchmarks]
|
||||
types: [completed]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
track_fork_pr_branch:
|
||||
if: github.event.workflow_run.conclusion == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
BENCHMARK_RESULTS: benchmark_results.txt
|
||||
PR_EVENT: event.json
|
||||
steps:
|
||||
- name: Download Benchmark Results
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
async function downloadArtifact(artifactName) {
|
||||
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: context.payload.workflow_run.id,
|
||||
});
|
||||
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
|
||||
return artifact.name == artifactName
|
||||
})[0];
|
||||
if (!matchArtifact) {
|
||||
core.setFailed(`Failed to find artifact: ${artifactName}`);
|
||||
}
|
||||
let download = await github.rest.actions.downloadArtifact({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
artifact_id: matchArtifact.id,
|
||||
archive_format: 'zip',
|
||||
});
|
||||
let fs = require('fs');
|
||||
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/${artifactName}.zip`, Buffer.from(download.data));
|
||||
}
|
||||
await downloadArtifact(process.env.BENCHMARK_RESULTS);
|
||||
await downloadArtifact(process.env.PR_EVENT);
|
||||
- name: Unzip Benchmark Results
|
||||
run: |
|
||||
unzip $BENCHMARK_RESULTS.zip
|
||||
unzip $PR_EVENT.zip
|
||||
- name: 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_BASE", prEvent.pull_request.base.ref);
|
||||
core.exportVariable("PR_BASE_SHA", prEvent.pull_request.base.sha);
|
||||
core.exportVariable("PR_NUMBER", prEvent.number);
|
||||
- uses: bencherdev/bencher@main
|
||||
- name: Track Benchmarks with Bencher
|
||||
run: |
|
||||
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 }}' \
|
||||
--testbed ubuntu-latest \
|
||||
--adapter rust_criterion \
|
||||
--err \
|
||||
--github-actions '${{ secrets.GITHUB_TOKEN }}' \
|
||||
--ci-number '${{ env.PR_NUMBER }}' \
|
||||
--file "$BENCHMARK_RESULTS"
|
||||
2
.github/workflows/check-pr.yml
vendored
2
.github/workflows/check-pr.yml
vendored
@@ -71,7 +71,7 @@ jobs:
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
labels: ['breaking change']
|
||||
labels: ['Type: Breaking Change']
|
||||
})
|
||||
|
||||
do-not-merge:
|
||||
|
||||
16
.github/workflows/check-semver.yml
vendored
Normal file
16
.github/workflows/check-semver.yml
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
name: Check Semver
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
check-semver:
|
||||
name: Check semver
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Check semver
|
||||
uses: obi1kenobi/cargo-semver-checks-action@v2
|
||||
156
.github/workflows/ci.yml
vendored
156
.github/workflows/ci.yml
vendored
@@ -17,71 +17,72 @@ 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:
|
||||
lint:
|
||||
rustfmt:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Install Rust nightly
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@nightly
|
||||
with:
|
||||
components: rustfmt
|
||||
- name: Install cargo-make
|
||||
uses: taiki-e/install-action@cargo-make
|
||||
- name: Cache Cargo dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
- name: Check formatting
|
||||
run: cargo make lint-format
|
||||
- name: Check documentation
|
||||
run: cargo make lint-docs
|
||||
- name: Check typos
|
||||
uses: crate-ci/typos@master
|
||||
- name: Lint dependencies
|
||||
uses: EmbarkStudios/cargo-deny-action@v1
|
||||
- run: cargo +nightly fmt --all --check
|
||||
|
||||
typos:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: crate-ci/typos@master
|
||||
|
||||
dependencies:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: EmbarkStudios/cargo-deny-action@v2
|
||||
|
||||
cargo-machete:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: bnjbvr/cargo-machete@v0.6.2
|
||||
|
||||
clippy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: clippy
|
||||
- name: Install cargo-make
|
||||
uses: taiki-e/install-action@cargo-make
|
||||
- name: Cache Cargo dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
- name: Run cargo make clippy-all
|
||||
run: cargo make clippy
|
||||
- uses: taiki-e/install-action@cargo-make
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: cargo make clippy
|
||||
|
||||
markdownlint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: DavidAnson/markdownlint-cli2-action@v17
|
||||
with:
|
||||
globs: |
|
||||
'**/*.md'
|
||||
'!target'
|
||||
|
||||
coverage:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: llvm-tools
|
||||
- name: Install cargo-llvm-cov and cargo-make
|
||||
uses: taiki-e/install-action@v2
|
||||
- uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-llvm-cov,cargo-make
|
||||
- name: Cache Cargo dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
- name: Generate coverage
|
||||
run: cargo make coverage
|
||||
- name: Upload to codecov.io
|
||||
uses: codecov/codecov-action@v4
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: cargo make coverage
|
||||
- uses: codecov/codecov-action@v4
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: true
|
||||
@@ -94,21 +95,30 @@ jobs:
|
||||
toolchain: ["1.74.0", "stable"]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Install Rust {{ matrix.toolchain }}
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ matrix.toolchain }}
|
||||
- name: Install cargo-make
|
||||
uses: taiki-e/install-action@cargo-make
|
||||
- name: Cache Cargo dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
- name: Run cargo make check
|
||||
run: cargo make check
|
||||
- uses: taiki-e/install-action@cargo-make
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: cargo make check
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
|
||||
lint-docs:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUSTDOCFLAGS: -Dwarnings
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@nightly
|
||||
- uses: dtolnay/install@cargo-docs-rs
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
# Run cargo rustdoc with the same options that would be used by docs.rs, taking into account
|
||||
# the package.metadata.docs.rs configured in Cargo.toml.
|
||||
# https://github.com/dtolnay/cargo-docs-rs
|
||||
- run: cargo +nightly docs-rs
|
||||
|
||||
test-doc:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -116,16 +126,11 @@ jobs:
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: Install cargo-make
|
||||
uses: taiki-e/install-action@cargo-make
|
||||
- name: Cache Cargo dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
- name: Test docs
|
||||
run: cargo make test-doc
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: taiki-e/install-action@cargo-make
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: cargo make test-doc
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
|
||||
@@ -142,19 +147,14 @@ jobs:
|
||||
backend: termion
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Install Rust ${{ matrix.toolchain }}}
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
- uses: actions/checkout@v4
|
||||
- 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
|
||||
- name: Cache Cargo dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
- name: Test ${{ matrix.backend }}
|
||||
run: cargo make test-backend ${{ matrix.backend }}
|
||||
- uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-make,nextest
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: cargo make test-backend ${{ matrix.backend }}
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
|
||||
@@ -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 }}
|
||||
@@ -4,12 +4,30 @@ 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:
|
||||
|
||||
- [v0.28.0](#v0280)
|
||||
⁻ `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>>`
|
||||
- `Layout::init_cache` no longer returns bool and takes a `NonZeroUsize` instead of `usize`
|
||||
- `ratatui::terminal` module is now private
|
||||
- `ToText` no longer has a lifetime
|
||||
- `Frame::size` is deprecated and renamed to `Frame::area`
|
||||
- [v0.27.0](#v0270)
|
||||
- List no clamps the selected index to list
|
||||
- Prelude items added / removed
|
||||
- 'termion' updated to 4.0
|
||||
- `Rect::inner` takes `Margin` directly instead of reference
|
||||
- `Buffer::filled` takes `Cell` directly instead of reference
|
||||
- `Stylize::bg()` now accepts `Into<Color>`
|
||||
- Removed deprecated `List::start_corner`
|
||||
- `LineGauge::gauge_style` is deprecated
|
||||
- [v0.26.0](#v0260)
|
||||
- `Flex::Start` is the new default flex mode for `Layout`
|
||||
- `patch_style` & `reset_style` now consume and return `Self`
|
||||
@@ -47,11 +65,221 @@ This is a quick summary of the sections below:
|
||||
- MSRV is now 1.63.0
|
||||
- `List` no longer ignores empty strings
|
||||
|
||||
## [v0.26.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.26.0)
|
||||
## v0.28.0
|
||||
|
||||
### `Backend::size` returns `Size` instead of `Rect` ([#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/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.
|
||||
|
||||
If you implement the Backend trait yourself, you need to update the implementation and add the
|
||||
`get/set_cursor_position` method. You can remove the `get/set_cursor` methods as they are deprecated
|
||||
and a default implementation for them exists.
|
||||
|
||||
### Ratatui now requires Crossterm 0.28.0 ([#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
|
||||
`ratatui::crossterm`, which can be used to avoid incompatible versions in your dependency list.
|
||||
|
||||
### `Axis::labels()` now accepts `IntoIterator<Into<Line>>` ([#1273] and [#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.
|
||||
|
||||
```diff
|
||||
- Axis::default().labels(vec!["a".into(), "b".into()])
|
||||
+ Axis::default().labels(["a", "b"])
|
||||
```
|
||||
|
||||
### `Layout::init_cache` no longer returns bool and takes a `NonZeroUsize` instead of `usize` ([#1245])
|
||||
|
||||
[#1245]: https://github.com/ratatui/ratatui/pull/1245
|
||||
|
||||
```diff
|
||||
- let is_initialized = Layout::init_cache(100);
|
||||
+ Layout::init_cache(NonZeroUsize::new(100).unwrap());
|
||||
```
|
||||
|
||||
### `ratatui::terminal` module is now private ([#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
|
||||
are also named terminal, and confusion about module exports for newer Rust users.
|
||||
|
||||
```diff
|
||||
- use ratatui::terminal::{CompletedFrame, Frame, Terminal, TerminalOptions, ViewPort};
|
||||
+ use ratatui::{CompletedFrame, Frame, Terminal, TerminalOptions, ViewPort};
|
||||
```
|
||||
|
||||
### `ToText` no longer has a lifetime ([#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`
|
||||
|
||||
[#1293]: https://github.com/ratatui/ratatui/pull/1293
|
||||
|
||||
`Frame::size` is renamed to `Frame::area` as it's the more correct name.
|
||||
|
||||
## [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/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`.
|
||||
|
||||
Previously selecting an index past the end of the list would show treat the list as having a
|
||||
selection which was not visible. Now the last item in the list will be selected instead.
|
||||
|
||||
### Prelude items added / removed ([#1149])
|
||||
|
||||
The following items have been removed from the prelude:
|
||||
|
||||
- `style::Styled` - this trait is useful for widgets that want to
|
||||
support the Stylize trait, but it adds complexity as widgets have two
|
||||
`style` methods and a `set_style` method.
|
||||
- `symbols::Marker` - this item is used by code that needs to draw to
|
||||
the `Canvas` widget, but it's not a common item that would be used by
|
||||
most users of the library.
|
||||
- `terminal::{CompletedFrame, TerminalOptions, Viewport}` - these items
|
||||
are rarely used by code that needs to interact with the terminal, and
|
||||
they're generally only ever used once in any app.
|
||||
|
||||
The following items have been added to the prelude:
|
||||
|
||||
- `layout::{Position, Size}` - these items are used by code that needs
|
||||
to interact with the layout system. These are newer items that were
|
||||
added in the last few releases, which should be used more liberally.
|
||||
This may cause conflicts for types defined elsewhere with a similar
|
||||
name.
|
||||
|
||||
To update your app:
|
||||
|
||||
```diff
|
||||
// if your app uses Styled::style() or Styled::set_style():
|
||||
-use ratatui::prelude::*;
|
||||
+use ratatui::{prelude::*, style::Styled};
|
||||
|
||||
// if your app uses symbols::Marker:
|
||||
-use ratatui::prelude::*;
|
||||
+use ratatui::{prelude::*, symbols::Marker}
|
||||
|
||||
// if your app uses terminal::{CompletedFrame, TerminalOptions, Viewport}
|
||||
-use ratatui::prelude::*;
|
||||
+use ratatui::{prelude::*, terminal::{CompletedFrame, TerminalOptions, Viewport}};
|
||||
|
||||
// to disambiguate existing types named Position or Size:
|
||||
- use some_crate::{Position, Size};
|
||||
- let size: Size = ...;
|
||||
- let position: Position = ...;
|
||||
+ let size: some_crate::Size = ...;
|
||||
+ let position: some_crate::Position = ...;
|
||||
```
|
||||
|
||||
### Termion is updated to 4.0 [#1106]
|
||||
|
||||
Changelog: <https://gitlab.redox-os.org/redox-os/termion/-/blob/master/CHANGELOG.md>
|
||||
|
||||
A change is only necessary if you were matching on all variants of the `MouseEvent` enum without a
|
||||
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/ratatui/pull/1106
|
||||
|
||||
### `Rect::inner` takes `Margin` directly instead of reference ([#1008])
|
||||
|
||||
[#1008]: https://github.com/ratatui/ratatui/pull/1008
|
||||
|
||||
`Margin` needs to be passed without reference now.
|
||||
|
||||
```diff
|
||||
-let area = area.inner(&Margin {
|
||||
+let area = area.inner(Margin {
|
||||
vertical: 0,
|
||||
horizontal: 2,
|
||||
});
|
||||
```
|
||||
|
||||
### `Buffer::filled` takes `Cell` directly instead of reference ([#1148])
|
||||
|
||||
[#1148]: https://github.com/ratatui/ratatui/pull/1148
|
||||
|
||||
`Buffer::filled` moves the `Cell` instead of taking a reference.
|
||||
|
||||
```diff
|
||||
-Buffer::filled(area, &Cell::new("X"));
|
||||
+Buffer::filled(area, Cell::new("X"));
|
||||
```
|
||||
|
||||
### `Stylize::bg()` now accepts `Into<Color>` ([#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/ratatui/pull/759
|
||||
|
||||
`List::start_corner` was deprecated in v0.25. Use `List::direction` and `ListDirection` instead.
|
||||
|
||||
```diff
|
||||
- list.start_corner(Corner::TopLeft);
|
||||
- list.start_corner(Corner::TopRight);
|
||||
// This is not an error, BottomRight rendered top to bottom previously
|
||||
- list.start_corner(Corner::BottomRight);
|
||||
// all becomes
|
||||
+ list.direction(ListDirection::TopToBottom);
|
||||
```
|
||||
|
||||
```diff
|
||||
- list.start_corner(Corner::BottomLeft);
|
||||
// becomes
|
||||
+ list.direction(ListDirection::BottomToTop);
|
||||
```
|
||||
|
||||
`layout::Corner` was removed entirely.
|
||||
|
||||
### `LineGauge::gauge_style` is deprecated ([#565])
|
||||
|
||||
[#565]: https://github.com/ratatui/ratatui/pull/1148
|
||||
|
||||
`LineGauge::gauge_style` is deprecated and replaced with `LineGauge::filled_style` and `LineGauge::unfilled_style`:
|
||||
|
||||
```diff
|
||||
let gauge = LineGauge::default()
|
||||
- .gauge_style(Style::default().fg(Color::Red).bg(Color::Blue)
|
||||
+ .filled_style(Style::default().fg(Color::Green))
|
||||
+ .unfilled_style(Style::default().fg(Color::White));
|
||||
```
|
||||
|
||||
## [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.
|
||||
@@ -72,9 +300,9 @@ 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
|
||||
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
|
||||
can some break type inference in the calling scope for empty containers.
|
||||
|
||||
@@ -89,9 +317,9 @@ 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
|
||||
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.
|
||||
|
||||
This typically occurs when collecting an iterator prior to calling `Tabs::new`, and can be resolved
|
||||
@@ -105,7 +333,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.
|
||||
@@ -115,7 +343,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
|
||||
@@ -132,8 +360,6 @@ The following example shows how to migrate for `Line`, but the same applies for
|
||||
|
||||
### Remove deprecated `Block::title_on_bottom` ([#757])
|
||||
|
||||
[#757]: https://github.com/ratatui-org/ratatui/pull/757
|
||||
|
||||
`Block::title_on_bottom` was deprecated in v0.22. Use `Block::title` and `Title::position` instead.
|
||||
|
||||
```diff
|
||||
@@ -143,7 +369,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
|
||||
@@ -151,7 +377,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
|
||||
@@ -175,11 +401,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.
|
||||
|
||||
@@ -193,7 +419,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).
|
||||
@@ -208,7 +434,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.
|
||||
@@ -220,7 +446,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:
|
||||
@@ -246,7 +472,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
|
||||
@@ -262,7 +488,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.
|
||||
@@ -279,18 +505,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
|
||||
@@ -304,7 +530,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
|
||||
@@ -318,7 +544,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
|
||||
@@ -336,7 +562,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`.
|
||||
@@ -349,11 +575,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.
|
||||
|
||||
@@ -365,7 +591,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
|
||||
@@ -379,31 +605,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`
|
||||
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.
|
||||
@@ -420,7 +646,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
|
||||
@@ -434,7 +660,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
|
||||
@@ -446,20 +672,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.
|
||||
|
||||
11852
CHANGELOG.md
11852
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
128
CODE_OF_CONDUCT.md
Normal file
128
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
https://forum.ratatui.rs/ or https://discord.gg/pMCEU9hNEj.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
@@ -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.
|
||||
@@ -17,10 +17,10 @@ found.
|
||||
|
||||
All contributions are obviously welcome. Please include as many details as possible in your PR
|
||||
description to help the reviewer (follow the provided template). Make sure to highlight changes
|
||||
which may need additional attention or you are uncertain about. Any idea with a large scale impact
|
||||
which may need additional attention, or you are uncertain about. Any idea with a large scale impact
|
||||
on the crate or its users should ideally be discussed in a "Feature Request" issue beforehand.
|
||||
|
||||
### Keep PRs small, intentional and focused
|
||||
### Keep PRs small, intentional, and focused
|
||||
|
||||
Try to do one pull request per change. The time taken to review a PR grows exponential with the size
|
||||
of the change. Small focused PRs will generally be much more faster to review. PRs that include both
|
||||
@@ -32,7 +32,7 @@ guarantee that the behavior is unchanged.
|
||||
### Code formatting
|
||||
|
||||
Run `cargo make format` before committing to ensure that code is consistently formatted with
|
||||
rustfmt. Configuration is in [rustfmt.toml](./rustfmt.toml).
|
||||
rustfmt. Configuration is in [`rustfmt.toml`](./rustfmt.toml).
|
||||
|
||||
### Search `tui-rs` for similar work
|
||||
|
||||
@@ -79,14 +79,14 @@ defaults depending on your platform of choice. Building the project should be as
|
||||
`cargo make build`.
|
||||
|
||||
```shell
|
||||
git clone https://github.com/ratatui-org/ratatui.git
|
||||
git clone https://github.com/ratatui/ratatui.git
|
||||
cd ratatui
|
||||
cargo make 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
|
||||
@@ -147,7 +147,7 @@ fn foo() {}
|
||||
```
|
||||
|
||||
- Max line length is 100 characters
|
||||
See [vscode rewrap extension](https://marketplace.visualstudio.com/items?itemName=stkb.rewrap)
|
||||
See [VS Code rewrap extension](https://marketplace.visualstudio.com/items?itemName=stkb.rewrap)
|
||||
|
||||
- Doc comments are above macros
|
||||
i.e.
|
||||
@@ -163,19 +163,19 @@ i.e. ``[`Block`]``, **NOT** ``[Block]``
|
||||
|
||||
### Deprecation notice
|
||||
|
||||
We generally want to wait at least two versions before removing deprecated items so users have
|
||||
We generally want to wait at least two versions before removing deprecated items, so users have
|
||||
time to update. However, if a deprecation is blocking for us to implement a new feature we may
|
||||
*consider* removing it in a one version notice.
|
||||
|
||||
### Use of unsafe for optimization purposes
|
||||
|
||||
We don't currently use any unsafe code in Ratatui, and would like to keep it that way. However there
|
||||
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
|
||||
|
||||
We use Github Actions for the CI where we perform the following checks:
|
||||
We use GitHub Actions for the CI where we perform the following checks:
|
||||
|
||||
- The code should compile on `stable` and the Minimum Supported Rust Version (MSRV).
|
||||
- The tests (docs, lib, tests and examples) should pass.
|
||||
@@ -191,12 +191,12 @@ This project was forked from [`tui-rs`](https://github.com/fdehau/tui-rs/) in Fe
|
||||
[blessing of the original author](https://github.com/fdehau/tui-rs/issues/654), Florian Dehau
|
||||
([@fdehau](https://github.com/fdehau)).
|
||||
|
||||
The original repository contains all the issues, PRs and discussion that were raised originally, and
|
||||
The original repository contains all the issues, PRs, and discussion that were raised originally, and
|
||||
it is useful to refer to when contributing code, documentation, or issues with Ratatui.
|
||||
|
||||
We imported all the PRs from the original repository and implemented many of the smaller ones and
|
||||
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.
|
||||
|
||||
|
||||
357
Cargo.toml
357
Cargo.toml
@@ -1,338 +1,57 @@
|
||||
[package]
|
||||
name = "ratatui"
|
||||
version = "0.26.3" # crate version
|
||||
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"
|
||||
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",
|
||||
]
|
||||
autoexamples = true
|
||||
edition = "2021"
|
||||
rust-version = "1.74.0"
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["ratatui*"]
|
||||
# disabled for now because of the orphan rule on conversions
|
||||
# <https://github.com/ratatui/ratatui/issues/1388#issuecomment-2379895747>
|
||||
exclude = ["ratatui-termion", "ratatui-termwiz"]
|
||||
|
||||
[badges]
|
||||
[workspace.dependencies]
|
||||
ratatui = { path = "ratatui" }
|
||||
ratatui-core = { path = "ratatui-core" }
|
||||
ratatui-crossterm = { path = "ratatui-crossterm" }
|
||||
ratatui-termion = { path = "ratatui-termion" }
|
||||
ratatui-termwiz = { path = "ratatui-termwiz" }
|
||||
ratatui-widgets = { path = "ratatui-widgets" }
|
||||
|
||||
[dependencies]
|
||||
bitflags = "2.3"
|
||||
cassowary = "0.3"
|
||||
compact_str = "0.7.1"
|
||||
crossterm = { version = "0.27", optional = true }
|
||||
document-features = { version = "0.2.7", optional = true }
|
||||
itertools = "0.12"
|
||||
compact_str = "0.8.0"
|
||||
crossterm = { version = "0.28.1" }
|
||||
document-features = { version = "0.2.7" }
|
||||
instability = "0.3.1"
|
||||
itertools = "0.13"
|
||||
lru = "0.12.0"
|
||||
paste = "1.0.2"
|
||||
serde = { version = "1", optional = true, features = ["derive"] }
|
||||
stability = "0.2.0"
|
||||
strum = { version = "0.26", features = ["derive"] }
|
||||
termion = { version = "3.0", optional = true }
|
||||
termwiz = { version = "0.22.0", optional = true }
|
||||
time = { version = "0.3.11", optional = true, features = ["local-offset"] }
|
||||
palette = { version = "0.7.6" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
strum = { version = "0.26.3", features = ["derive"] }
|
||||
termion = { version = "4.0.0" }
|
||||
termwiz = { version = "0.22.0" }
|
||||
time = { version = "0.3.11", features = ["local-offset"] }
|
||||
unicode-segmentation = "1.10"
|
||||
unicode-truncate = "1"
|
||||
unicode-width = "0.1"
|
||||
unicode-width = "=0.1.13"
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = "1.0.71"
|
||||
# dev-dependencies
|
||||
argh = "0.1.12"
|
||||
better-panic = "0.3.0"
|
||||
color-eyre = "0.6.2"
|
||||
criterion = { version = "0.5.1", features = ["html_reports"] }
|
||||
derive_builder = "0.20.0"
|
||||
fakeit = "1.1"
|
||||
font8x8 = "0.3.1"
|
||||
futures = "0.3.30"
|
||||
indoc = "2"
|
||||
palette = "0.7.3"
|
||||
octocrab = "0.40.0"
|
||||
pretty_assertions = "1.4.0"
|
||||
rand = "0.8.5"
|
||||
rand_chacha = "0.3.1"
|
||||
rstest = "0.19.0"
|
||||
rstest = "0.22.0"
|
||||
serde_json = "1.0.109"
|
||||
|
||||
[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"
|
||||
wildcard_imports = "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_raw_strings = "warn"
|
||||
redundant_type_annotations = "warn"
|
||||
rest_pat_in_fully_bound_structs = "warn"
|
||||
string_lit_chars_any = "warn"
|
||||
string_to_string = "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 and adds a dependency on the [Crossterm crate].
|
||||
crossterm = ["dep:crossterm"]
|
||||
## enables the [`TermionBackend`] backend and adds a dependency on the [Termion crate].
|
||||
termion = ["dep:termion"]
|
||||
## enables the [`TermwizBackend`] backend and adds a dependency on the [Termwiz crate].
|
||||
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 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`] widget module and adds a dependency on the [Time crate].
|
||||
widget-calendar = ["dep:time"]
|
||||
|
||||
#! Underline color is only supported by the [`CrosstermBackend`] backend, and is not supported
|
||||
#! on Windows 7.
|
||||
## enables the backend code that sets the underline color.
|
||||
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`](crate::widgets::Paragraph::line_count)
|
||||
## [`Paragraph::line_width`](crate::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` and `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"]
|
||||
|
||||
[[bench]]
|
||||
name = "barchart"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "block"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "line"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "list"
|
||||
harness = false
|
||||
|
||||
[lib]
|
||||
bench = false
|
||||
|
||||
[[bench]]
|
||||
name = "paragraph"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "sparkline"
|
||||
harness = false
|
||||
|
||||
|
||||
[[example]]
|
||||
name = "barchart"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "block"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "canvas"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "calendar"
|
||||
required-features = ["crossterm", "widget-calendar"]
|
||||
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"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[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", "widget-calendar"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "docsrs"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = false
|
||||
|
||||
[[example]]
|
||||
name = "gauge"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "hello_world"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "layout"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "constraints"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = false
|
||||
|
||||
[[example]]
|
||||
name = "flex"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "constraint-explorer"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "list"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "minimal"
|
||||
required-features = ["crossterm"]
|
||||
# prefer to show the more featureful examples in the docs
|
||||
doc-scrape-examples = false
|
||||
|
||||
[[example]]
|
||||
name = "modifiers"
|
||||
required-features = ["crossterm"]
|
||||
# this example is a bit verbose, so we don't want to include it in the docs
|
||||
doc-scrape-examples = false
|
||||
|
||||
[[example]]
|
||||
name = "panic"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "paragraph"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "popup"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "ratatui-logo"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "scrollbar"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "sparkline"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "table"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "tabs"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "user_input"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "inline"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[test]]
|
||||
name = "state_serde"
|
||||
required-features = ["serde"]
|
||||
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"] }
|
||||
|
||||
15
MAINTAINERS.md
Normal file
15
MAINTAINERS.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# Maintainers
|
||||
|
||||
This file documents current and past maintainers.
|
||||
|
||||
- [orhun](https://github.com/orhun)
|
||||
- [joshka](https://github.com/joshka)
|
||||
- [kdheepak](https://github.com/kdheepak)
|
||||
- [Valentin271](https://github.com/Valentin271)
|
||||
|
||||
## Past Maintainers
|
||||
|
||||
- [fdehau](https://github.com/fdehau)
|
||||
- [mindoodoo](https://github.com/mindoodoo)
|
||||
- [sayanarijit](https://github.com/sayanarijit)
|
||||
- [EdJoPaTo](https://github.com/EdJoPaTo)
|
||||
@@ -5,24 +5,17 @@ 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" }
|
||||
NON_BACKEND_FEATURES = "all-widgets,macros,serde"
|
||||
|
||||
[tasks.default]
|
||||
alias = "ci"
|
||||
|
||||
[tasks.ci]
|
||||
description = "Run continuous integration tasks"
|
||||
dependencies = ["lint-style", "clippy", "check", "test"]
|
||||
dependencies = ["lint", "clippy", "check", "test"]
|
||||
|
||||
[tasks.lint-style]
|
||||
description = "Lint code style (formatting, typos, docs)"
|
||||
[tasks.lint]
|
||||
description = "Lint code style (formatting, typos, docs, markdown)"
|
||||
dependencies = ["lint-format", "lint-typos", "lint-docs"]
|
||||
|
||||
[tasks.lint-format]
|
||||
@@ -48,33 +41,27 @@ toolchain = "nightly"
|
||||
command = "cargo"
|
||||
args = [
|
||||
"rustdoc",
|
||||
"--no-default-features",
|
||||
"${ALL_FEATURES_FLAG}",
|
||||
"--all-features",
|
||||
"--",
|
||||
"-Zunstable-options",
|
||||
"--check",
|
||||
"-Dwarnings",
|
||||
]
|
||||
|
||||
[tasks.lint-markdown]
|
||||
description = "Check markdown files for errors and warnings"
|
||||
command = "markdownlint-cli2"
|
||||
args = ["**/*.md", "!target"]
|
||||
|
||||
[tasks.check]
|
||||
description = "Check code for errors and warnings"
|
||||
command = "cargo"
|
||||
args = [
|
||||
"check",
|
||||
"--all-targets",
|
||||
"--no-default-features",
|
||||
"${ALL_FEATURES_FLAG}",
|
||||
]
|
||||
args = ["check", "--all-targets", "--all-features"]
|
||||
|
||||
[tasks.build]
|
||||
description = "Compile the project"
|
||||
command = "cargo"
|
||||
args = [
|
||||
"build",
|
||||
"--all-targets",
|
||||
"--no-default-features",
|
||||
"${ALL_FEATURES_FLAG}",
|
||||
]
|
||||
args = ["build", "--all-targets", "--all-features"]
|
||||
|
||||
[tasks.clippy]
|
||||
description = "Run Clippy for linting"
|
||||
@@ -82,10 +69,9 @@ command = "cargo"
|
||||
args = [
|
||||
"clippy",
|
||||
"--all-targets",
|
||||
"--all-features",
|
||||
"--tests",
|
||||
"--benches",
|
||||
"--no-default-features",
|
||||
"${ALL_FEATURES_FLAG}",
|
||||
"--",
|
||||
"-D",
|
||||
"warnings",
|
||||
@@ -103,18 +89,12 @@ run_task = { name = ["test-lib", "test-doc"] }
|
||||
description = "Run default tests"
|
||||
dependencies = ["install-nextest"]
|
||||
command = "cargo"
|
||||
args = [
|
||||
"nextest",
|
||||
"run",
|
||||
"--all-targets",
|
||||
"--no-default-features",
|
||||
"${ALL_FEATURES_FLAG}",
|
||||
]
|
||||
args = ["nextest", "run", "--all-targets", "--all-features"]
|
||||
|
||||
[tasks.test-doc]
|
||||
description = "Run documentation tests"
|
||||
command = "cargo"
|
||||
args = ["test", "--doc", "--no-default-features", "${ALL_FEATURES_FLAG}"]
|
||||
args = ["test", "--doc", "--all-features"]
|
||||
|
||||
[tasks.test-backend]
|
||||
# takes a command line parameter to specify the backend to test (e.g. "crossterm")
|
||||
@@ -127,7 +107,7 @@ args = [
|
||||
"--all-targets",
|
||||
"--no-default-features",
|
||||
"--features",
|
||||
"${ALL_FEATURES},${@}",
|
||||
"${NON_BACKEND_FEATURES},${@}",
|
||||
]
|
||||
|
||||
[tasks.coverage]
|
||||
@@ -138,8 +118,7 @@ args = [
|
||||
"--lcov",
|
||||
"--output-path",
|
||||
"target/lcov.info",
|
||||
"--no-default-features",
|
||||
"${ALL_FEATURES_FLAG}",
|
||||
"--all-features",
|
||||
]
|
||||
|
||||
[tasks.run-example]
|
||||
|
||||
229
README.md
229
README.md
@@ -4,14 +4,18 @@
|
||||
- [Ratatui](#ratatui)
|
||||
- [Installation](#installation)
|
||||
- [Introduction](#introduction)
|
||||
- [Other Documentation](#other-documentation)
|
||||
- [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)
|
||||
- [Example](#example)
|
||||
- [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,14 +25,14 @@
|
||||
|
||||
<!-- cargo-rdme start -->
|
||||
|
||||

|
||||

|
||||
|
||||
<div align="center">
|
||||
|
||||
[![Crate Badge]][Crate] [![Docs Badge]][API Docs] [![CI Badge]][CI Workflow] [![License
|
||||
Badge]](./LICENSE) [![Sponsors Badge]][GitHub Sponsors]<br>
|
||||
[![Codecov Badge]][Codecov] [![Deps.rs Badge]][Deps.rs] [![Discord Badge]][Discord Server]
|
||||
[![Matrix Badge]][Matrix]<br>
|
||||
[![Crate Badge]][Crate] [![Docs Badge]][API Docs] [![CI Badge]][CI Workflow] [![Deps.rs
|
||||
Badge]][Deps.rs]<br> [![Codecov Badge]][Codecov] [![License Badge]](./LICENSE) [![Sponsors
|
||||
Badge]][GitHub Sponsors]<br> [![Discord Badge]][Discord Server] [![Matrix Badge]][Matrix]
|
||||
[![Forum Badge]][Forum]<br>
|
||||
|
||||
[Ratatui Website] · [API Docs] · [Examples] · [Changelog] · [Breaking Changes]<br>
|
||||
[Contributing] · [Report a bug] · [Request a Feature] · [Create a Pull Request]
|
||||
@@ -43,10 +47,10 @@ Ratatui was forked from the [tui-rs] crate in 2023 in order to continue its deve
|
||||
|
||||
## Installation
|
||||
|
||||
Add `ratatui` and `crossterm` as dependencies to your cargo.toml:
|
||||
Add `ratatui` as a dependency to your cargo.toml:
|
||||
|
||||
```shell
|
||||
cargo add ratatui crossterm
|
||||
cargo add ratatui
|
||||
```
|
||||
|
||||
Ratatui uses [Crossterm] by default as it works on most platforms. See the [Installation]
|
||||
@@ -67,6 +71,7 @@ terminal user interfaces and showcases the features of Ratatui, along with a hel
|
||||
## Other documentation
|
||||
|
||||
- [Ratatui Website] - explains the library's concepts and provides step-by-step tutorials
|
||||
- [Ratatui Forum][Forum] - a place to ask questions and discuss the library
|
||||
- [API Docs] - the full API documentation for the library on docs.rs.
|
||||
- [Examples] - a collection of examples that demonstrate how to use the library.
|
||||
- [Contributing] - Please read this if you are interested in contributing to the project.
|
||||
@@ -110,7 +115,8 @@ module] and the [Backends] section of the [Ratatui Website] for more info.
|
||||
|
||||
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. See the [Widgets] section of the [Ratatui Website]
|
||||
using the provided [`render_widget`] method. After this closure returns, a diff is performed and
|
||||
only the changes are drawn to the terminal. See the [Widgets] section of the [Ratatui Website]
|
||||
for more info.
|
||||
|
||||
### Handling events
|
||||
@@ -125,12 +131,18 @@ Website] for more info. For example, if you are using [Crossterm], you can use t
|
||||
```rust
|
||||
use std::io::{self, stdout};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
use ratatui::{
|
||||
backend::CrosstermBackend,
|
||||
crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
terminal::{
|
||||
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
|
||||
},
|
||||
ExecutableCommand,
|
||||
},
|
||||
widgets::{Block, Paragraph},
|
||||
Frame, Terminal,
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
fn main() -> io::Result<()> {
|
||||
enable_raw_mode()?;
|
||||
@@ -161,9 +173,8 @@ fn handle_events() -> io::Result<bool> {
|
||||
|
||||
fn ui(frame: &mut Frame) {
|
||||
frame.render_widget(
|
||||
Paragraph::new("Hello World!")
|
||||
.block(Block::bordered().title("Greeting")),
|
||||
frame.size(),
|
||||
Paragraph::new("Hello World!").block(Block::bordered().title("Greeting")),
|
||||
frame.area(),
|
||||
);
|
||||
}
|
||||
```
|
||||
@@ -180,40 +191,27 @@ area. This lets you describe a responsive terminal UI by nesting layouts. See th
|
||||
section of the [Ratatui Website] for more info.
|
||||
|
||||
```rust
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
use ratatui::{
|
||||
layout::{Constraint, Layout},
|
||||
widgets::Block,
|
||||
Frame,
|
||||
};
|
||||
|
||||
fn ui(frame: &mut Frame) {
|
||||
let main_layout = Layout::new(
|
||||
Direction::Vertical,
|
||||
[
|
||||
Constraint::Length(1),
|
||||
Constraint::Min(0),
|
||||
Constraint::Length(1),
|
||||
],
|
||||
)
|
||||
.split(frame.size());
|
||||
frame.render_widget(
|
||||
Block::new().borders(Borders::TOP).title("Title Bar"),
|
||||
main_layout[0],
|
||||
);
|
||||
frame.render_widget(
|
||||
Block::new().borders(Borders::TOP).title("Status Bar"),
|
||||
main_layout[2],
|
||||
);
|
||||
let [title_area, main_area, status_area] = Layout::vertical([
|
||||
Constraint::Length(1),
|
||||
Constraint::Min(0),
|
||||
Constraint::Length(1),
|
||||
])
|
||||
.areas(frame.area());
|
||||
let [left_area, right_area] =
|
||||
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
.areas(main_area);
|
||||
|
||||
let inner_layout = Layout::new(
|
||||
Direction::Horizontal,
|
||||
[Constraint::Percentage(50), Constraint::Percentage(50)],
|
||||
)
|
||||
.split(main_layout[1]);
|
||||
frame.render_widget(
|
||||
Block::bordered().title("Left"),
|
||||
inner_layout[0],
|
||||
);
|
||||
frame.render_widget(
|
||||
Block::bordered().title("Right"),
|
||||
inner_layout[1],
|
||||
);
|
||||
frame.render_widget(Block::bordered().title("Title Bar"), title_area);
|
||||
frame.render_widget(Block::bordered().title("Status Bar"), status_area);
|
||||
frame.render_widget(Block::bordered().title("Left"), left_area);
|
||||
frame.render_widget(Block::bordered().title("Right"), right_area);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -234,48 +232,41 @@ short-hand syntax to apply a style to widgets and text. See the [Styling Text] s
|
||||
[Ratatui Website] for more info.
|
||||
|
||||
```rust
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
use ratatui::{
|
||||
layout::{Constraint, Layout},
|
||||
style::{Color, Modifier, Style, Stylize},
|
||||
text::{Line, Span},
|
||||
widgets::{Block, Paragraph},
|
||||
Frame,
|
||||
};
|
||||
|
||||
fn ui(frame: &mut Frame) {
|
||||
let areas = Layout::new(
|
||||
Direction::Vertical,
|
||||
[
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
Constraint::Min(0),
|
||||
],
|
||||
)
|
||||
.split(frame.size());
|
||||
let areas = Layout::vertical([Constraint::Length(1); 4]).split(frame.area());
|
||||
|
||||
let span1 = Span::raw("Hello ");
|
||||
let span2 = Span::styled(
|
||||
"World",
|
||||
Style::new()
|
||||
.fg(Color::Green)
|
||||
.bg(Color::White)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
);
|
||||
let span3 = "!".red().on_light_yellow().italic();
|
||||
let line = Line::from(vec![
|
||||
Span::raw("Hello "),
|
||||
Span::styled(
|
||||
"World",
|
||||
Style::new()
|
||||
.fg(Color::Green)
|
||||
.bg(Color::White)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
"!".red().on_light_yellow().italic(),
|
||||
]);
|
||||
frame.render_widget(line, areas[0]);
|
||||
|
||||
let line = Line::from(vec![span1, span2, span3]);
|
||||
let text: Text = Text::from(vec![line]);
|
||||
// using the short-hand syntax and implicit conversions
|
||||
let paragraph = Paragraph::new("Hello World!".red().on_white().bold());
|
||||
frame.render_widget(paragraph, areas[1]);
|
||||
|
||||
frame.render_widget(Paragraph::new(text), areas[0]);
|
||||
// or using the short-hand syntax and implicit conversions
|
||||
frame.render_widget(
|
||||
Paragraph::new("Hello World!".red().on_white().bold()),
|
||||
areas[1],
|
||||
);
|
||||
// style the whole widget instead of just the text
|
||||
let paragraph = Paragraph::new("Hello World!").style(Style::new().red().on_white());
|
||||
frame.render_widget(paragraph, areas[2]);
|
||||
|
||||
// to style the whole widget instead of just the text
|
||||
frame.render_widget(
|
||||
Paragraph::new("Hello World!").style(Style::new().red().on_white()),
|
||||
areas[2],
|
||||
);
|
||||
// or using the short-hand syntax
|
||||
frame.render_widget(Paragraph::new("Hello World!").blue().on_yellow(), areas[3]);
|
||||
// use the simpler short-hand syntax
|
||||
let paragraph = Paragraph::new("Hello World!").blue().on_yellow();
|
||||
frame.render_widget(paragraph, areas[3]);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -293,21 +284,21 @@ 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/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
|
||||
[docsrs-hello]: https://github.com/ratatui/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-hello.png?raw=true
|
||||
[docsrs-layout]: https://github.com/ratatui/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-layout.png?raw=true
|
||||
[docsrs-styling]: https://github.com/ratatui/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-styling.png?raw=true
|
||||
[`Frame`]: terminal::Frame
|
||||
[`render_widget`]: terminal::Frame::render_widget
|
||||
[`Widget`]: widgets::Widget
|
||||
@@ -326,24 +317,23 @@ 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
|
||||
[Discord Badge]:
|
||||
https://img.shields.io/discord/1070692720437383208?label=discord&logo=discord&style=flat-square&color=1370D3&logoColor=1370D3
|
||||
[CI Badge]: https://img.shields.io/github/actions/workflow/status/ratatui/ratatui/ci.yml?style=flat-square&logo=github
|
||||
[CI Workflow]: https://github.com/ratatui/ratatui/actions/workflows/ci.yml
|
||||
[Codecov Badge]: https://img.shields.io/codecov/c/github/ratatui/ratatui?logo=codecov&style=flat-square&token=BAQ8SOKEST&color=C43AC3&logoColor=C43AC3
|
||||
[Codecov]: https://app.codecov.io/gh/ratatui/ratatui
|
||||
[Deps.rs Badge]: https://deps.rs/repo/github/ratatui/ratatui/status.svg?style=flat-square
|
||||
[Deps.rs]: https://deps.rs/repo/github/ratatui/ratatui
|
||||
[Discord Badge]: https://img.shields.io/discord/1070692720437383208?label=discord&logo=discord&style=flat-square&color=1370D3&logoColor=1370D3
|
||||
[Discord Server]: https://discord.gg/pMCEU9hNEj
|
||||
[Docs Badge]: https://img.shields.io/docsrs/ratatui?logo=rust&style=flat-square&logoColor=E05D44
|
||||
[Matrix Badge]:
|
||||
https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix&color=C43AC3
|
||||
[Matrix Badge]: https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix&color=C43AC3
|
||||
[Matrix]: https://matrix.to/#/#ratatui:matrix.org
|
||||
[Sponsors Badge]: https://img.shields.io/github/sponsors/ratatui-org?logo=github&style=flat-square&color=1370D3
|
||||
[Forum Badge]: https://img.shields.io/discourse/likes?server=https%3A%2F%2Fforum.ratatui.rs&style=flat-square&logo=discourse&label=forum&color=C43AC3
|
||||
[Forum]: https://forum.ratatui.rs
|
||||
[Sponsors Badge]: https://img.shields.io/github/sponsors/ratatui?logo=github&style=flat-square&color=1370D3
|
||||
|
||||
<!-- cargo-rdme end -->
|
||||
|
||||
@@ -358,17 +348,14 @@ In order to organize ourselves, we currently use a [Discord server](https://disc
|
||||
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.
|
||||
Our primary open-source workflow is centered around GitHub.
|
||||
For significant discussions, we rely on GitHub — please open an issue, a discussion or a PR.
|
||||
While we do utilize Discord for coordinating, it's not essential for contributing. We have recently
|
||||
launched the [Ratatui Forum][Forum], and our primary open-source workflow is centered around GitHub.
|
||||
For bugs and features, we rely on GitHub. Please [Report a bug], [Request a Feature] or [Create a
|
||||
Pull Request].
|
||||
|
||||
Please make sure you read the updated [contributing](./CONTRIBUTING.md) guidelines, especially if
|
||||
you are interested in working on a PR or issue opened in the previous repository.
|
||||
|
||||
## Rust version requirements
|
||||
|
||||
Since version 0.23.0, The Minimum Supported Rust Version (MSRV) of `ratatui` is 1.67.0.
|
||||
|
||||
## Widgets
|
||||
|
||||
### Built in
|
||||
@@ -404,7 +391,7 @@ be installed with `cargo install cargo-make`).
|
||||
`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
|
||||
- [templates](https://github.com/ratatui/templates) — Starter templates for
|
||||
bootstrapping a Rust TUI application with Ratatui & crossterm
|
||||
- [tui-builder](https://github.com/jkelleyrtp/tui-builder) — Batteries-included MVC framework for
|
||||
Tui-rs + Crossterm apps
|
||||
@@ -428,7 +415,7 @@ be installed with `cargo install cargo-make`).
|
||||
|
||||
## Apps
|
||||
|
||||
Check out [awesome-ratatui](https://github.com/ratatui-org/awesome-ratatui) for a curated list of
|
||||
Check out [awesome-ratatui](https://github.com/ratatui/awesome-ratatui) for a curated list of
|
||||
awesome apps/libraries built with `ratatui`!
|
||||
|
||||
## Alternatives
|
||||
@@ -439,7 +426,7 @@ to build text user interfaces in Rust.
|
||||
## Acknowledgments
|
||||
|
||||
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>
|
||||
|
||||
28
cliff.toml
28
cliff.toml
@@ -2,7 +2,7 @@
|
||||
# https://git-cliff.org/docs/configuration
|
||||
|
||||
[remote.github]
|
||||
owner = "ratatui-org"
|
||||
owner = "ratatui"
|
||||
repo = "ratatui"
|
||||
|
||||
[changelog]
|
||||
@@ -11,6 +11,8 @@ header = """
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
<!-- ignore lint rules that are often triggered by content generated from commits / git-cliff -->
|
||||
<!-- markdownlint-disable line-length no-bare-urls ul-style emphasis-style -->
|
||||
"""
|
||||
# template for the changelog body
|
||||
# https://keats.github.io/tera/docs/#introduction
|
||||
@@ -22,25 +24,23 @@ 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.breaking %} [**breaking**]{% endif %}
|
||||
{%- if commit.body %}
|
||||
|
||||
````text {#- 4 backticks escape any backticks in body #}
|
||||
{{commit.body | indent(prefix=" ") }}
|
||||
````
|
||||
{%- if commit.body %}\n\n{{ commit.body | indent(prefix=" > ", first=true, blank=true) }}
|
||||
{%- endif %}
|
||||
{%- for footer in commit.footers %}
|
||||
{%- for footer in commit.footers %}\n
|
||||
{%- if footer.token != "Signed-off-by" and footer.token != "Co-authored-by" %}
|
||||
|
||||
{{ footer.token | indent(prefix=" ") }}{{ footer.separator }}{{ footer.value }}
|
||||
>
|
||||
{{ footer.token | indent(prefix=" > ", first=true, blank=true) }}
|
||||
{{- footer.separator }}
|
||||
{{- footer.value| indent(prefix=" > ", first=false, blank=true) }}
|
||||
{%- endif %}
|
||||
{%- endfor %}
|
||||
{% endmacro -%}
|
||||
@@ -87,6 +87,11 @@ trim = false
|
||||
footer = """
|
||||
<!-- generated by git-cliff -->
|
||||
"""
|
||||
postprocessors = [
|
||||
{ pattern = '<!-- Please read CONTRIBUTING.md before submitting any pull request. -->', replace = "" },
|
||||
{ pattern = '>---+\n', replace = '' },
|
||||
{ pattern = ' +\n', replace = "\n" },
|
||||
]
|
||||
|
||||
[git]
|
||||
# parse the commits based on https://www.conventionalcommits.org
|
||||
@@ -126,6 +131,7 @@ commit_parsers = [
|
||||
{ message = "^(Buffer|buffer|Frame|frame|Gauge|gauge|Paragraph|paragraph):", group = "<!-- 07 -->Miscellaneous Tasks" },
|
||||
{ message = "^\\[", group = "<!-- 07 -->Miscellaneous Tasks" },
|
||||
]
|
||||
|
||||
# protect breaking changes from being skipped due to matching a skipping commit_parser
|
||||
protect_breaking_commits = false
|
||||
# filter out the commits that are not matched by commit parsers
|
||||
|
||||
@@ -3,7 +3,7 @@ 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
|
||||
# crossterm -> all the windows- deps https://github.com/ratatui/ratatui/pull/1064#issuecomment-2078848980
|
||||
allowed-duplicate-crates = [
|
||||
"bitflags",
|
||||
"windows-targets",
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
# configuration for https://github.com/EmbarkStudios/cargo-deny
|
||||
|
||||
[licenses]
|
||||
default = "deny"
|
||||
unlicensed = "deny"
|
||||
copyleft = "deny"
|
||||
version = 2
|
||||
confidence-threshold = 0.8
|
||||
allow = [
|
||||
"Apache-2.0",
|
||||
@@ -16,8 +14,7 @@ allow = [
|
||||
]
|
||||
|
||||
[advisories]
|
||||
unmaintained = "deny"
|
||||
yanked = "deny"
|
||||
version = 2
|
||||
|
||||
[bans]
|
||||
multiple-versions = "allow"
|
||||
|
||||
@@ -1,298 +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 std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{Bar, BarChart, BarGroup, Block, Paragraph},
|
||||
};
|
||||
|
||||
struct Company<'a> {
|
||||
revenue: [u64; 4],
|
||||
label: &'a str,
|
||||
bar_style: Style,
|
||||
}
|
||||
|
||||
struct App<'a> {
|
||||
data: Vec<(&'a str, u64)>,
|
||||
months: [&'a str; 4],
|
||||
companies: [Company<'a>; 3],
|
||||
}
|
||||
|
||||
const TOTAL_REVENUE: &str = "Total Revenue";
|
||||
|
||||
impl<'a> App<'a> {
|
||||
fn new() -> Self {
|
||||
App {
|
||||
data: vec![
|
||||
("B1", 9),
|
||||
("B2", 12),
|
||||
("B3", 5),
|
||||
("B4", 8),
|
||||
("B5", 2),
|
||||
("B6", 4),
|
||||
("B7", 5),
|
||||
("B8", 9),
|
||||
("B9", 14),
|
||||
("B10", 15),
|
||||
("B11", 1),
|
||||
("B12", 0),
|
||||
("B13", 4),
|
||||
("B14", 6),
|
||||
("B15", 4),
|
||||
("B16", 6),
|
||||
("B17", 4),
|
||||
("B18", 7),
|
||||
("B19", 13),
|
||||
("B20", 8),
|
||||
("B21", 11),
|
||||
("B22", 9),
|
||||
("B23", 3),
|
||||
("B24", 5),
|
||||
],
|
||||
companies: [
|
||||
Company {
|
||||
label: "Comp.A",
|
||||
revenue: [9500, 12500, 5300, 8500],
|
||||
bar_style: Style::default().fg(Color::Green),
|
||||
},
|
||||
Company {
|
||||
label: "Comp.B",
|
||||
revenue: [1500, 2500, 3000, 500],
|
||||
bar_style: Style::default().fg(Color::Yellow),
|
||||
},
|
||||
Company {
|
||||
label: "Comp.C",
|
||||
revenue: [10500, 10600, 9000, 4200],
|
||||
bar_style: Style::default().fg(Color::White),
|
||||
},
|
||||
],
|
||||
months: ["Mars", "Apr", "May", "Jun"],
|
||||
}
|
||||
}
|
||||
|
||||
fn on_tick(&mut self) {
|
||||
let value = self.data.pop().unwrap();
|
||||
self.data.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(frame: &mut Frame, app: &App) {
|
||||
let vertical = Layout::vertical([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)]);
|
||||
let horizontal = Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]);
|
||||
let [top, bottom] = vertical.areas(frame.size());
|
||||
let [left, right] = horizontal.areas(bottom);
|
||||
|
||||
let barchart = BarChart::default()
|
||||
.block(Block::bordered().title("Data1"))
|
||||
.data(&app.data)
|
||||
.bar_width(9)
|
||||
.bar_style(Style::default().fg(Color::Yellow))
|
||||
.value_style(Style::default().fg(Color::Black).bg(Color::Yellow));
|
||||
|
||||
frame.render_widget(barchart, top);
|
||||
draw_bar_with_group_labels(frame, app, left);
|
||||
draw_horizontal_bars(frame, app, right);
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
fn create_groups<'a>(app: &'a App, combine_values_and_labels: bool) -> Vec<BarGroup<'a>> {
|
||||
app.months
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, &month)| {
|
||||
let bars: Vec<Bar> = app
|
||||
.companies
|
||||
.iter()
|
||||
.map(|c| {
|
||||
let mut bar = Bar::default()
|
||||
.value(c.revenue[i])
|
||||
.style(c.bar_style)
|
||||
.value_style(
|
||||
Style::default()
|
||||
.bg(c.bar_style.fg.unwrap())
|
||||
.fg(Color::Black),
|
||||
);
|
||||
|
||||
if combine_values_and_labels {
|
||||
bar = bar.text_value(format!(
|
||||
"{} ({:.1} M)",
|
||||
c.label,
|
||||
(c.revenue[i] as f64) / 1000.
|
||||
));
|
||||
} else {
|
||||
bar = bar
|
||||
.text_value(format!("{:.1}", (c.revenue[i] as f64) / 1000.))
|
||||
.label(c.label.into());
|
||||
}
|
||||
bar
|
||||
})
|
||||
.collect();
|
||||
BarGroup::default()
|
||||
.label(Line::from(month).centered())
|
||||
.bars(&bars)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
fn draw_bar_with_group_labels(f: &mut Frame, app: &App, area: Rect) {
|
||||
const LEGEND_HEIGHT: u16 = 6;
|
||||
|
||||
let groups = create_groups(app, false);
|
||||
|
||||
let mut barchart = BarChart::default()
|
||||
.block(Block::bordered().title("Data1"))
|
||||
.bar_width(7)
|
||||
.group_gap(3);
|
||||
|
||||
for group in groups {
|
||||
barchart = barchart.data(group);
|
||||
}
|
||||
|
||||
f.render_widget(barchart, area);
|
||||
|
||||
if area.height >= LEGEND_HEIGHT && area.width >= TOTAL_REVENUE.len() as u16 + 2 {
|
||||
let legend_width = TOTAL_REVENUE.len() as u16 + 2;
|
||||
let legend_area = Rect {
|
||||
height: LEGEND_HEIGHT,
|
||||
width: legend_width,
|
||||
y: area.y,
|
||||
x: area.right() - legend_width,
|
||||
};
|
||||
draw_legend(f, legend_area);
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
fn draw_horizontal_bars(f: &mut Frame, app: &App, area: Rect) {
|
||||
const LEGEND_HEIGHT: u16 = 6;
|
||||
|
||||
let groups = create_groups(app, true);
|
||||
|
||||
let mut barchart = BarChart::default()
|
||||
.block(Block::bordered().title("Data1"))
|
||||
.bar_width(1)
|
||||
.group_gap(1)
|
||||
.bar_gap(0)
|
||||
.direction(Direction::Horizontal);
|
||||
|
||||
for group in groups {
|
||||
barchart = barchart.data(group);
|
||||
}
|
||||
|
||||
f.render_widget(barchart, area);
|
||||
|
||||
if area.height >= LEGEND_HEIGHT && area.width >= TOTAL_REVENUE.len() as u16 + 2 {
|
||||
let legend_width = TOTAL_REVENUE.len() as u16 + 2;
|
||||
let legend_area = Rect {
|
||||
height: LEGEND_HEIGHT,
|
||||
width: legend_width,
|
||||
y: area.y,
|
||||
x: area.right() - legend_width,
|
||||
};
|
||||
draw_legend(f, legend_area);
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_legend(f: &mut Frame, area: Rect) {
|
||||
let text = vec![
|
||||
Line::from(Span::styled(
|
||||
TOTAL_REVENUE,
|
||||
Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.fg(Color::White),
|
||||
)),
|
||||
Line::from(Span::styled(
|
||||
"- Company A",
|
||||
Style::default().fg(Color::Green),
|
||||
)),
|
||||
Line::from(Span::styled(
|
||||
"- Company B",
|
||||
Style::default().fg(Color::Yellow),
|
||||
)),
|
||||
Line::from(Span::styled(
|
||||
"- Company C",
|
||||
Style::default().fg(Color::White),
|
||||
)),
|
||||
];
|
||||
|
||||
let block = Block::bordered().style(Style::default().fg(Color::White));
|
||||
let paragraph = Paragraph::new(text).block(block);
|
||||
f.render_widget(paragraph, area);
|
||||
}
|
||||
@@ -1,815 +0,0 @@
|
||||
//! [tui-big-text] is a rust crate that renders large pixel text as a [Ratatui] widget using the
|
||||
//! glyphs from the [font8x8] crate.
|
||||
//!
|
||||
//! 
|
||||
//!
|
||||
//! # Installation
|
||||
//!
|
||||
//! ```shell
|
||||
//! cargo add ratatui tui-big-text
|
||||
//! ```
|
||||
//!
|
||||
//! # Usage
|
||||
//!
|
||||
//! Create a [`BigText`] widget using `BigTextBuilder` and pass it to [`Frame::render_widget`] to
|
||||
//! render be rendered. The builder allows you to customize the [`Style`] of the widget and the
|
||||
//! [`PixelSize`] of the glyphs. The [`PixelSize`] can be used to control how many character cells
|
||||
//! are used to represent a single pixel of the 8x8 font.
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```rust
|
||||
//! use anyhow::Result;
|
||||
//! use ratatui::prelude::*;
|
||||
//! use tui_big_text::{BigTextBuilder, PixelSize};
|
||||
//!
|
||||
//! fn render(frame: &mut Frame) -> Result<()> {
|
||||
//! let big_text = BigTextBuilder::default()
|
||||
//! .pixel_size(PixelSize::Full)
|
||||
//! .style(Style::new().blue())
|
||||
//! .lines(vec![
|
||||
//! "Hello".red().into(),
|
||||
//! "World".white().into(),
|
||||
//! "~~~~~".into(),
|
||||
//! ])
|
||||
//! .build()?;
|
||||
//! frame.render_widget(big_text, frame.size());
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! [tui-big-text]: https://crates.io/crates/tui-big-text
|
||||
//! [Ratatui]: https://crates.io/crates/ratatui
|
||||
//! [font8x8]: https://crates.io/crates/font8x8
|
||||
//! [`BigText`]: crate::BigText
|
||||
//! [`PixelSize`]: crate::PixelSize
|
||||
//! [`Frame::render_widget`]: ratatui::Frame::render_widget
|
||||
//! [`Style`]: ratatui::style::Style
|
||||
|
||||
use std::cmp::min;
|
||||
|
||||
use derive_builder::Builder;
|
||||
use font8x8::UnicodeFonts;
|
||||
use ratatui::{prelude::*, text::StyledGrapheme};
|
||||
|
||||
#[allow(unused)]
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Default)]
|
||||
pub enum PixelSize {
|
||||
#[default]
|
||||
/// A pixel from the 8x8 font is represented by a full character cell in the terminal.
|
||||
Full,
|
||||
/// A pixel from the 8x8 font is represented by a half (upper/lower) character cell in the
|
||||
/// terminal.
|
||||
HalfHeight,
|
||||
/// A pixel from the 8x8 font is represented by a half (left/right) character cell in the
|
||||
/// terminal.
|
||||
HalfWidth,
|
||||
/// A pixel from the 8x8 font is represented by a quadrant of a character cell in the terminal.
|
||||
Quadrant,
|
||||
}
|
||||
|
||||
/// Displays one or more lines of text using 8x8 pixel characters.
|
||||
///
|
||||
/// The text is rendered using the [font8x8](https://crates.io/crates/font8x8) crate.
|
||||
///
|
||||
/// Using the `pixel_size` method, you can also chose, how 'big' a pixel should be.
|
||||
/// Currently a pixel of the 8x8 font can be represented by one full or half
|
||||
/// (horizontal/vertical/both) character cell of the terminal.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::prelude::*;
|
||||
/// use tui_big_text::{BigTextBuilder, PixelSize};
|
||||
///
|
||||
/// BigText::builder()
|
||||
/// .pixel_size(PixelSize::Full)
|
||||
/// .style(Style::new().white())
|
||||
/// .lines(vec![
|
||||
/// "Hello".red().into(),
|
||||
/// "World".blue().into(),
|
||||
/// "=====".into(),
|
||||
/// ])
|
||||
/// .build();
|
||||
/// ```
|
||||
///
|
||||
/// Renders:
|
||||
///
|
||||
/// ```plain
|
||||
/// ██ ██ ███ ███
|
||||
/// ██ ██ ██ ██
|
||||
/// ██ ██ ████ ██ ██ ████
|
||||
/// ██████ ██ ██ ██ ██ ██ ██
|
||||
/// ██ ██ ██████ ██ ██ ██ ██
|
||||
/// ██ ██ ██ ██ ██ ██ ██
|
||||
/// ██ ██ ████ ████ ████ ████
|
||||
///
|
||||
/// ██ ██ ███ ███
|
||||
/// ██ ██ ██ ██
|
||||
/// ██ ██ ████ ██ ███ ██ ██
|
||||
/// ██ █ ██ ██ ██ ███ ██ ██ █████
|
||||
/// ███████ ██ ██ ██ ██ ██ ██ ██
|
||||
/// ███ ███ ██ ██ ██ ██ ██ ██
|
||||
/// ██ ██ ████ ████ ████ ███ ██
|
||||
///
|
||||
/// ███ ██ ███ ██ ███ ██ ███ ██ ███ ██
|
||||
/// ██ ███ ██ ███ ██ ███ ██ ███ ██ ███
|
||||
/// ```
|
||||
#[derive(Debug, Builder, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct BigText<'a> {
|
||||
/// The text to display
|
||||
#[builder(setter(into))]
|
||||
lines: Vec<Line<'a>>,
|
||||
|
||||
/// The style of the widget
|
||||
///
|
||||
/// Defaults to `Style::default()`
|
||||
#[builder(default)]
|
||||
style: Style,
|
||||
|
||||
/// The size of single glyphs
|
||||
///
|
||||
/// Defaults to `BigTextSize::default()` (=> `BigTextSize::Full`)
|
||||
#[builder(default)]
|
||||
pixel_size: PixelSize,
|
||||
}
|
||||
|
||||
impl Widget for BigText<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let layout = layout(area, self.pixel_size);
|
||||
for (line, line_layout) in self.lines.iter().zip(layout) {
|
||||
for (g, cell) in line.styled_graphemes(self.style).zip(line_layout) {
|
||||
render_symbol(&g, cell, buf, self.pixel_size);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns how many cells are needed to display a full 8x8 glyphe using the given font size
|
||||
const fn cells_per_glyph(size: PixelSize) -> (u16, u16) {
|
||||
match size {
|
||||
PixelSize::Full => (8, 8),
|
||||
PixelSize::HalfHeight => (8, 4),
|
||||
PixelSize::HalfWidth => (4, 8),
|
||||
PixelSize::Quadrant => (4, 4),
|
||||
}
|
||||
}
|
||||
|
||||
/// Chunk the area into as many x*y cells as possible returned as a 2D iterator of `Rect`s
|
||||
/// representing the rows of cells.
|
||||
/// The size of each cell depends on given font size
|
||||
fn layout(
|
||||
area: Rect,
|
||||
pixel_size: PixelSize,
|
||||
) -> impl IntoIterator<Item = impl IntoIterator<Item = Rect>> {
|
||||
let (width, height) = cells_per_glyph(pixel_size);
|
||||
(area.top()..area.bottom())
|
||||
.step_by(height as usize)
|
||||
.map(move |y| {
|
||||
(area.left()..area.right())
|
||||
.step_by(width as usize)
|
||||
.map(move |x| {
|
||||
let width = min(area.right() - x, width);
|
||||
let height = min(area.bottom() - y, height);
|
||||
Rect::new(x, y, width, height)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Render a single grapheme into a cell by looking up the corresponding 8x8 bitmap in the
|
||||
/// `BITMAPS` array and setting the corresponding cells in the buffer.
|
||||
fn render_symbol(grapheme: &StyledGrapheme, area: Rect, buf: &mut Buffer, pixel_size: PixelSize) {
|
||||
buf.set_style(area, grapheme.style);
|
||||
let c = grapheme.symbol.chars().next().unwrap(); // TODO: handle multi-char graphemes
|
||||
if let Some(glyph) = font8x8::BASIC_FONTS.get(c) {
|
||||
render_glyph(glyph, area, buf, pixel_size);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the correct unicode symbol for two vertical "pixels"
|
||||
const fn get_symbol_half_height(top: u8, bottom: u8) -> char {
|
||||
match top {
|
||||
0 => match bottom {
|
||||
0 => ' ',
|
||||
_ => '▄',
|
||||
},
|
||||
_ => match bottom {
|
||||
0 => '▀',
|
||||
_ => '█',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the correct unicode symbol for two horizontal "pixels"
|
||||
const fn get_symbol_half_width(left: u8, right: u8) -> char {
|
||||
match left {
|
||||
0 => match right {
|
||||
0 => ' ',
|
||||
_ => '▐',
|
||||
},
|
||||
_ => match right {
|
||||
0 => '▌',
|
||||
_ => '█',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the correct unicode symbol for 2x2 "pixels"
|
||||
const fn get_symbol_half_size(
|
||||
top_left: u8,
|
||||
top_right: u8,
|
||||
bottom_left: u8,
|
||||
bottom_right: u8,
|
||||
) -> char {
|
||||
const QUADRANT_SYMBOLS: [char; 16] = [
|
||||
' ', '▘', '▝', '▀', '▖', '▌', '▞', '▛', '▗', '▚', '▐', '▜', '▄', '▙', '▟', '█',
|
||||
];
|
||||
|
||||
let top_left = if top_left > 0 { 1 } else { 0 };
|
||||
let top_right = if top_right > 0 { 1 << 1 } else { 0 };
|
||||
let bottom_left = if bottom_left > 0 { 1 << 2 } else { 0 };
|
||||
let bottom_right = if bottom_right > 0 { 1 << 3 } else { 0 };
|
||||
|
||||
QUADRANT_SYMBOLS[top_left + top_right + bottom_left + bottom_right]
|
||||
}
|
||||
|
||||
/// Render a single 8x8 glyph into a cell by setting the corresponding cells in the buffer.
|
||||
fn render_glyph(glyph: [u8; 8], area: Rect, buf: &mut Buffer, pixel_size: PixelSize) {
|
||||
let (width, height) = cells_per_glyph(pixel_size);
|
||||
|
||||
let glyph_vertical_index = (0..glyph.len()).step_by(8 / height as usize);
|
||||
let glyph_horizontal_bit_selector = (0..8).step_by(8 / width as usize);
|
||||
|
||||
for (row, y) in glyph_vertical_index.zip(area.top()..area.bottom()) {
|
||||
for (col, x) in glyph_horizontal_bit_selector
|
||||
.clone()
|
||||
.zip(area.left()..area.right())
|
||||
{
|
||||
let cell = buf.get_mut(x, y);
|
||||
let symbol_character = match pixel_size {
|
||||
PixelSize::Full => match glyph[row] & (1 << col) {
|
||||
0 => ' ',
|
||||
_ => '█',
|
||||
},
|
||||
PixelSize::HalfHeight => {
|
||||
let top = glyph[row] & (1 << col);
|
||||
let bottom = glyph[row + 1] & (1 << col);
|
||||
get_symbol_half_height(top, bottom)
|
||||
}
|
||||
PixelSize::HalfWidth => {
|
||||
let left = glyph[row] & (1 << col);
|
||||
let right = glyph[row] & (1 << (col + 1));
|
||||
get_symbol_half_width(left, right)
|
||||
}
|
||||
PixelSize::Quadrant => {
|
||||
let top_left = glyph[row] & (1 << col);
|
||||
let top_right = glyph[row] & (1 << (col + 1));
|
||||
let bottom_left = glyph[row + 1] & (1 << col);
|
||||
let bottom_right = glyph[row + 1] & (1 << (col + 1));
|
||||
get_symbol_half_size(top_left, top_right, bottom_left, bottom_right)
|
||||
}
|
||||
};
|
||||
cell.set_char(symbol_character);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||
|
||||
#[test]
|
||||
fn build() -> Result<()> {
|
||||
let lines = vec![Line::from(vec!["Hello".red(), "World".blue()])];
|
||||
let style = Style::new().green();
|
||||
let pixel_size = PixelSize::default();
|
||||
assert_eq!(
|
||||
BigTextBuilder::default()
|
||||
.lines(lines.clone())
|
||||
.style(style)
|
||||
.build()?,
|
||||
BigText {
|
||||
lines,
|
||||
style,
|
||||
pixel_size
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_single_line() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.lines(vec![Line::from("SingleLine")])
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 80, 8));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines([
|
||||
" ████ ██ ███ ████ ██ ",
|
||||
"██ ██ ██ ██ ",
|
||||
"███ ███ █████ ███ ██ ██ ████ ██ ███ █████ ████ ",
|
||||
" ███ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ",
|
||||
" ███ ██ ██ ██ ██ ██ ██ ██████ ██ █ ██ ██ ██ ██████ ",
|
||||
"██ ██ ██ ██ ██ █████ ██ ██ ██ ██ ██ ██ ██ ██ ",
|
||||
" ████ ████ ██ ██ ██ ████ ████ ███████ ████ ██ ██ ████ ",
|
||||
" █████ ",
|
||||
]);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_truncated() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.lines(vec![Line::from("Truncated")])
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 70, 6));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines([
|
||||
"██████ █ ███",
|
||||
"█ ██ █ ██ ██",
|
||||
" ██ ██ ███ ██ ██ █████ ████ ████ █████ ████ ██",
|
||||
" ██ ███ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ █████",
|
||||
" ██ ██ ██ ██ ██ ██ ██ ██ █████ ██ ██████ ██ ██",
|
||||
" ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ █ ██ ██ ██",
|
||||
]);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_multiple_lines() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.lines(vec![Line::from("Multi"), Line::from("Lines")])
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 40, 16));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines([
|
||||
"██ ██ ███ █ ██ ",
|
||||
"███ ███ ██ ██ ",
|
||||
"███████ ██ ██ ██ █████ ███ ",
|
||||
"███████ ██ ██ ██ ██ ██ ",
|
||||
"██ █ ██ ██ ██ ██ ██ ██ ",
|
||||
"██ ██ ██ ██ ██ ██ █ ██ ",
|
||||
"██ ██ ███ ██ ████ ██ ████ ",
|
||||
" ",
|
||||
"████ ██ ",
|
||||
" ██ ",
|
||||
" ██ ███ █████ ████ █████ ",
|
||||
" ██ ██ ██ ██ ██ ██ ██ ",
|
||||
" ██ █ ██ ██ ██ ██████ ████ ",
|
||||
" ██ ██ ██ ██ ██ ██ ██ ",
|
||||
"███████ ████ ██ ██ ████ █████ ",
|
||||
" ",
|
||||
]);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_widget_style() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.lines(vec![Line::from("Styled")])
|
||||
.style(Style::new().bold())
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 48, 8));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines([
|
||||
" ████ █ ███ ███ ".bold(),
|
||||
"██ ██ ██ ██ ██ ".bold(),
|
||||
"███ █████ ██ ██ ██ ████ ██ ".bold(),
|
||||
" ███ ██ ██ ██ ██ ██ ██ █████ ".bold(),
|
||||
" ███ ██ ██ ██ ██ ██████ ██ ██ ".bold(),
|
||||
"██ ██ ██ █ █████ ██ ██ ██ ██ ".bold(),
|
||||
" ████ ██ ██ ████ ████ ███ ██ ".bold(),
|
||||
" █████ ".bold(),
|
||||
]);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_line_style() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.lines(vec![
|
||||
Line::from("Red".red()),
|
||||
Line::from("Green".green()),
|
||||
Line::from("Blue".blue()),
|
||||
])
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 40, 24));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let mut expected = Buffer::with_lines([
|
||||
"██████ ███ ",
|
||||
" ██ ██ ██ ",
|
||||
" ██ ██ ████ ██ ",
|
||||
" █████ ██ ██ █████ ",
|
||||
" ██ ██ ██████ ██ ██ ",
|
||||
" ██ ██ ██ ██ ██ ",
|
||||
"███ ██ ████ ███ ██ ",
|
||||
" ",
|
||||
" ████ ",
|
||||
" ██ ██ ",
|
||||
"██ ██ ███ ████ ████ █████ ",
|
||||
"██ ███ ██ ██ ██ ██ ██ ██ ██ ",
|
||||
"██ ███ ██ ██ ██████ ██████ ██ ██ ",
|
||||
" ██ ██ ██ ██ ██ ██ ██ ",
|
||||
" █████ ████ ████ ████ ██ ██ ",
|
||||
" ",
|
||||
"██████ ███ ",
|
||||
" ██ ██ ██ ",
|
||||
" ██ ██ ██ ██ ██ ████ ",
|
||||
" █████ ██ ██ ██ ██ ██ ",
|
||||
" ██ ██ ██ ██ ██ ██████ ",
|
||||
" ██ ██ ██ ██ ██ ██ ",
|
||||
"██████ ████ ███ ██ ████ ",
|
||||
" ",
|
||||
]);
|
||||
expected.set_style(Rect::new(0, 0, 24, 8), Style::new().red());
|
||||
expected.set_style(Rect::new(0, 8, 40, 8), Style::new().green());
|
||||
expected.set_style(Rect::new(0, 16, 32, 8), Style::new().blue());
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_half_height_single_line() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.pixel_size(PixelSize::HalfHeight)
|
||||
.lines(vec![Line::from("SingleLine")])
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 80, 4));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines([
|
||||
"▄█▀▀█▄ ▀▀ ▀██ ▀██▀ ▀▀ ",
|
||||
"▀██▄ ▀██ ██▀▀█▄ ▄█▀▀▄█▀ ██ ▄█▀▀█▄ ██ ▀██ ██▀▀█▄ ▄█▀▀█▄ ",
|
||||
"▄▄ ▀██ ██ ██ ██ ▀█▄▄██ ██ ██▀▀▀▀ ██ ▄█ ██ ██ ██ ██▀▀▀▀ ",
|
||||
" ▀▀▀▀ ▀▀▀▀ ▀▀ ▀▀ ▄▄▄▄█▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀▀▀▀ ▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀ ",
|
||||
]);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_half_height_truncated() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.pixel_size(PixelSize::HalfHeight)
|
||||
.lines(vec![Line::from("Truncated")])
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 70, 3));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines([
|
||||
"█▀██▀█ ▄█ ▀██",
|
||||
" ██ ▀█▄█▀█▄ ██ ██ ██▀▀█▄ ▄█▀▀█▄ ▀▀▀█▄ ▀██▀▀ ▄█▀▀█▄ ▄▄▄██",
|
||||
" ██ ██ ▀▀ ██ ██ ██ ██ ██ ▄▄ ▄█▀▀██ ██ ▄ ██▀▀▀▀ ██ ██",
|
||||
]);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_half_height_multiple_lines() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.pixel_size(PixelSize::HalfHeight)
|
||||
.lines(vec![Line::from("Multi"), Line::from("Lines")])
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 40, 8));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines([
|
||||
"██▄ ▄██ ▀██ ▄█ ▀▀ ",
|
||||
"███████ ██ ██ ██ ▀██▀▀ ▀██ ",
|
||||
"██ ▀ ██ ██ ██ ██ ██ ▄ ██ ",
|
||||
"▀▀ ▀▀ ▀▀▀ ▀▀ ▀▀▀▀ ▀▀ ▀▀▀▀ ",
|
||||
"▀██▀ ▀▀ ",
|
||||
" ██ ▀██ ██▀▀█▄ ▄█▀▀█▄ ▄█▀▀▀▀ ",
|
||||
" ██ ▄█ ██ ██ ██ ██▀▀▀▀ ▀▀▀█▄ ",
|
||||
"▀▀▀▀▀▀▀ ▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀ ▀▀▀▀▀ ",
|
||||
]);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_half_height_widget_style() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.pixel_size(PixelSize::HalfHeight)
|
||||
.lines(vec![Line::from("Styled")])
|
||||
.style(Style::new().bold())
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 48, 4));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines([
|
||||
"▄█▀▀█▄ ▄█ ▀██ ▀██ ".bold(),
|
||||
"▀██▄ ▀██▀▀ ██ ██ ██ ▄█▀▀█▄ ▄▄▄██ ".bold(),
|
||||
"▄▄ ▀██ ██ ▄ ▀█▄▄██ ██ ██▀▀▀▀ ██ ██ ".bold(),
|
||||
" ▀▀▀▀ ▀▀ ▄▄▄▄█▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀ ▀▀ ".bold(),
|
||||
]);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_half_height_line_style() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.pixel_size(PixelSize::HalfHeight)
|
||||
.lines(vec![
|
||||
Line::from("Red".red()),
|
||||
Line::from("Green".green()),
|
||||
Line::from("Blue".blue()),
|
||||
])
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 40, 12));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let mut expected = Buffer::with_lines([
|
||||
"▀██▀▀█▄ ▀██ ",
|
||||
" ██▄▄█▀ ▄█▀▀█▄ ▄▄▄██ ",
|
||||
" ██ ▀█▄ ██▀▀▀▀ ██ ██ ",
|
||||
"▀▀▀ ▀▀ ▀▀▀▀ ▀▀▀ ▀▀ ",
|
||||
" ▄█▀▀█▄ ",
|
||||
"██ ▀█▄█▀█▄ ▄█▀▀█▄ ▄█▀▀█▄ ██▀▀█▄ ",
|
||||
"▀█▄ ▀██ ██ ▀▀ ██▀▀▀▀ ██▀▀▀▀ ██ ██ ",
|
||||
" ▀▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀ ▀▀ ",
|
||||
"▀██▀▀█▄ ▀██ ",
|
||||
" ██▄▄█▀ ██ ██ ██ ▄█▀▀█▄ ",
|
||||
" ██ ██ ██ ██ ██ ██▀▀▀▀ ",
|
||||
"▀▀▀▀▀▀ ▀▀▀▀ ▀▀▀ ▀▀ ▀▀▀▀ ",
|
||||
]);
|
||||
expected.set_style(Rect::new(0, 0, 24, 4), Style::new().red());
|
||||
expected.set_style(Rect::new(0, 4, 40, 4), Style::new().green());
|
||||
expected.set_style(Rect::new(0, 8, 32, 4), Style::new().blue());
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_half_width_single_line() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.pixel_size(PixelSize::HalfWidth)
|
||||
.lines(vec![Line::from("SingleLine")])
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 40, 8));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines([
|
||||
"▐█▌ █ ▐█ ██ █ ",
|
||||
"█ █ █ ▐▌ ",
|
||||
"█▌ ▐█ ██▌ ▐█▐▌ █ ▐█▌ ▐▌ ▐█ ██▌ ▐█▌ ",
|
||||
"▐█ █ █ █ █ █ █ █ █ ▐▌ █ █ █ █ █ ",
|
||||
" ▐█ █ █ █ █ █ █ ███ ▐▌ ▌ █ █ █ ███ ",
|
||||
"█ █ █ █ █ ▐██ █ █ ▐▌▐▌ █ █ █ █ ",
|
||||
"▐█▌ ▐█▌ █ █ █ ▐█▌ ▐█▌ ███▌▐█▌ █ █ ▐█▌ ",
|
||||
" ██▌ ",
|
||||
]);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_half_width_truncated() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.pixel_size(PixelSize::HalfWidth)
|
||||
.lines(vec![Line::from("Truncated")])
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 35, 6));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines([
|
||||
"███ ▐ ▐█",
|
||||
"▌█▐ █ █",
|
||||
" █ █▐█ █ █ ██▌ ▐█▌ ▐█▌ ▐██ ▐█▌ █",
|
||||
" █ ▐█▐▌█ █ █ █ █ █ █ █ █ █ ▐██",
|
||||
" █ ▐▌▐▌█ █ █ █ █ ▐██ █ ███ █ █",
|
||||
" █ ▐▌ █ █ █ █ █ █ █ █ █▐ █ █ █",
|
||||
]);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_half_width_multiple_lines() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.pixel_size(PixelSize::HalfWidth)
|
||||
.lines(vec![Line::from("Multi"), Line::from("Lines")])
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 16));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines([
|
||||
"█ ▐▌ ▐█ ▐ █ ",
|
||||
"█▌█▌ █ █ ",
|
||||
"███▌█ █ █ ▐██ ▐█ ",
|
||||
"███▌█ █ █ █ █ ",
|
||||
"█▐▐▌█ █ █ █ █ ",
|
||||
"█ ▐▌█ █ █ █▐ █ ",
|
||||
"█ ▐▌▐█▐▌▐█▌ ▐▌ ▐█▌ ",
|
||||
" ",
|
||||
"██ █ ",
|
||||
"▐▌ ",
|
||||
"▐▌ ▐█ ██▌ ▐█▌ ▐██ ",
|
||||
"▐▌ █ █ █ █ █ █ ",
|
||||
"▐▌ ▌ █ █ █ ███ ▐█▌ ",
|
||||
"▐▌▐▌ █ █ █ █ █ ",
|
||||
"███▌▐█▌ █ █ ▐█▌ ██▌ ",
|
||||
" ",
|
||||
]);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_half_width_widget_style() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.pixel_size(PixelSize::HalfWidth)
|
||||
.lines(vec![Line::from("Styled")])
|
||||
.style(Style::new().bold())
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 24, 8));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines([
|
||||
"▐█▌ ▐ ▐█ ▐█ ".bold(),
|
||||
"█ █ █ █ █ ".bold(),
|
||||
"█▌ ▐██ █ █ █ ▐█▌ █ ".bold(),
|
||||
"▐█ █ █ █ █ █ █ ▐██ ".bold(),
|
||||
" ▐█ █ █ █ █ ███ █ █ ".bold(),
|
||||
"█ █ █▐ ▐██ █ █ █ █ ".bold(),
|
||||
"▐█▌ ▐▌ █ ▐█▌ ▐█▌ ▐█▐▌".bold(),
|
||||
" ██▌ ".bold(),
|
||||
]);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_half_width_line_style() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.pixel_size(PixelSize::HalfWidth)
|
||||
.lines(vec![
|
||||
Line::from("Red".red()),
|
||||
Line::from("Green".green()),
|
||||
Line::from("Blue".blue()),
|
||||
])
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 24));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let mut expected = Buffer::with_lines([
|
||||
"███ ▐█ ",
|
||||
"▐▌▐▌ █ ",
|
||||
"▐▌▐▌▐█▌ █ ",
|
||||
"▐██ █ █ ▐██ ",
|
||||
"▐▌█ ███ █ █ ",
|
||||
"▐▌▐▌█ █ █ ",
|
||||
"█▌▐▌▐█▌ ▐█▐▌ ",
|
||||
" ",
|
||||
" ██ ",
|
||||
"▐▌▐▌ ",
|
||||
"█ █▐█ ▐█▌ ▐█▌ ██▌ ",
|
||||
"█ ▐█▐▌█ █ █ █ █ █ ",
|
||||
"█ █▌▐▌▐▌███ ███ █ █ ",
|
||||
"▐▌▐▌▐▌ █ █ █ █ ",
|
||||
" ██▌██ ▐█▌ ▐█▌ █ █ ",
|
||||
" ",
|
||||
"███ ▐█ ",
|
||||
"▐▌▐▌ █ ",
|
||||
"▐▌▐▌ █ █ █ ▐█▌ ",
|
||||
"▐██ █ █ █ █ █ ",
|
||||
"▐▌▐▌ █ █ █ ███ ",
|
||||
"▐▌▐▌ █ █ █ █ ",
|
||||
"███ ▐█▌ ▐█▐▌▐█▌ ",
|
||||
" ",
|
||||
]);
|
||||
expected.set_style(Rect::new(0, 0, 12, 8), Style::new().red());
|
||||
expected.set_style(Rect::new(0, 8, 20, 8), Style::new().green());
|
||||
expected.set_style(Rect::new(0, 16, 16, 8), Style::new().blue());
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_half_size_symbols() -> Result<()> {
|
||||
assert_eq!(get_symbol_half_size(0, 0, 0, 0), ' ');
|
||||
assert_eq!(get_symbol_half_size(1, 0, 0, 0), '▘');
|
||||
assert_eq!(get_symbol_half_size(0, 1, 0, 0), '▝');
|
||||
assert_eq!(get_symbol_half_size(1, 1, 0, 0), '▀');
|
||||
assert_eq!(get_symbol_half_size(0, 0, 1, 0), '▖');
|
||||
assert_eq!(get_symbol_half_size(1, 0, 1, 0), '▌');
|
||||
assert_eq!(get_symbol_half_size(0, 1, 1, 0), '▞');
|
||||
assert_eq!(get_symbol_half_size(1, 1, 1, 0), '▛');
|
||||
assert_eq!(get_symbol_half_size(0, 0, 0, 1), '▗');
|
||||
assert_eq!(get_symbol_half_size(1, 0, 0, 1), '▚');
|
||||
assert_eq!(get_symbol_half_size(0, 1, 0, 1), '▐');
|
||||
assert_eq!(get_symbol_half_size(1, 1, 0, 1), '▜');
|
||||
assert_eq!(get_symbol_half_size(0, 0, 1, 1), '▄');
|
||||
assert_eq!(get_symbol_half_size(1, 0, 1, 1), '▙');
|
||||
assert_eq!(get_symbol_half_size(0, 1, 1, 1), '▟');
|
||||
assert_eq!(get_symbol_half_size(1, 1, 1, 1), '█');
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_half_size_single_line() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.pixel_size(PixelSize::Quadrant)
|
||||
.lines(vec![Line::from("SingleLine")])
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 40, 4));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines([
|
||||
"▟▀▙ ▀ ▝█ ▜▛ ▀ ",
|
||||
"▜▙ ▝█ █▀▙ ▟▀▟▘ █ ▟▀▙ ▐▌ ▝█ █▀▙ ▟▀▙ ",
|
||||
"▄▝█ █ █ █ ▜▄█ █ █▀▀ ▐▌▗▌ █ █ █ █▀▀ ",
|
||||
"▝▀▘ ▝▀▘ ▀ ▀ ▄▄▛ ▝▀▘ ▝▀▘ ▀▀▀▘▝▀▘ ▀ ▀ ▝▀▘ ",
|
||||
]);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_half_size_truncated() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.pixel_size(PixelSize::Quadrant)
|
||||
.lines(vec![Line::from("Truncated")])
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 35, 3));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines([
|
||||
"▛█▜ ▟ ▝█",
|
||||
" █ ▜▟▜▖█ █ █▀▙ ▟▀▙ ▝▀▙ ▝█▀ ▟▀▙ ▗▄█",
|
||||
" █ ▐▌▝▘█ █ █ █ █ ▄ ▟▀█ █▗ █▀▀ █ █",
|
||||
]);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_half_size_multiple_lines() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.pixel_size(PixelSize::Quadrant)
|
||||
.lines(vec![Line::from("Multi"), Line::from("Lines")])
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 8));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines([
|
||||
"█▖▟▌ ▝█ ▟ ▀ ",
|
||||
"███▌█ █ █ ▝█▀ ▝█ ",
|
||||
"█▝▐▌█ █ █ █▗ █ ",
|
||||
"▀ ▝▘▝▀▝▘▝▀▘ ▝▘ ▝▀▘ ",
|
||||
"▜▛ ▀ ",
|
||||
"▐▌ ▝█ █▀▙ ▟▀▙ ▟▀▀ ",
|
||||
"▐▌▗▌ █ █ █ █▀▀ ▝▀▙ ",
|
||||
"▀▀▀▘▝▀▘ ▀ ▀ ▝▀▘ ▀▀▘ ",
|
||||
]);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_half_size_widget_style() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.pixel_size(PixelSize::Quadrant)
|
||||
.lines(vec![Line::from("Styled")])
|
||||
.style(Style::new().bold())
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 24, 4));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines([
|
||||
"▟▀▙ ▟ ▝█ ▝█ ".bold(),
|
||||
"▜▙ ▝█▀ █ █ █ ▟▀▙ ▗▄█ ".bold(),
|
||||
"▄▝█ █▗ ▜▄█ █ █▀▀ █ █ ".bold(),
|
||||
"▝▀▘ ▝▘ ▄▄▛ ▝▀▘ ▝▀▘ ▝▀▝▘".bold(),
|
||||
]);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_half_size_line_style() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.pixel_size(PixelSize::Quadrant)
|
||||
.lines(vec![
|
||||
Line::from("Red".red()),
|
||||
Line::from("Green".green()),
|
||||
Line::from("Blue".blue()),
|
||||
])
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 12));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let mut expected = Buffer::with_lines([
|
||||
"▜▛▜▖ ▝█ ",
|
||||
"▐▙▟▘▟▀▙ ▗▄█ ",
|
||||
"▐▌▜▖█▀▀ █ █ ",
|
||||
"▀▘▝▘▝▀▘ ▝▀▝▘ ",
|
||||
"▗▛▜▖ ",
|
||||
"█ ▜▟▜▖▟▀▙ ▟▀▙ █▀▙ ",
|
||||
"▜▖▜▌▐▌▝▘█▀▀ █▀▀ █ █ ",
|
||||
" ▀▀▘▀▀ ▝▀▘ ▝▀▘ ▀ ▀ ",
|
||||
"▜▛▜▖▝█ ",
|
||||
"▐▙▟▘ █ █ █ ▟▀▙ ",
|
||||
"▐▌▐▌ █ █ █ █▀▀ ",
|
||||
"▀▀▀ ▝▀▘ ▝▀▝▘▝▀▘ ",
|
||||
]);
|
||||
expected.set_style(Rect::new(0, 0, 12, 4), Style::new().red());
|
||||
expected.set_style(Rect::new(0, 4, 20, 4), Style::new().green());
|
||||
expected.set_style(Rect::new(0, 8, 16, 4), Style::new().blue());
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -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,45 +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::enum_glob_use,
|
||||
clippy::missing_errors_doc,
|
||||
clippy::module_name_repetitions,
|
||||
clippy::must_use_candidate,
|
||||
clippy::wildcard_imports
|
||||
)]
|
||||
|
||||
mod app;
|
||||
mod big_text;
|
||||
mod colors;
|
||||
mod destroy;
|
||||
mod errors;
|
||||
mod tabs;
|
||||
mod term;
|
||||
mod theme;
|
||||
|
||||
pub use app::*;
|
||||
use color_eyre::Result;
|
||||
pub use colors::*;
|
||||
pub use term::*;
|
||||
pub use theme::*;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
errors::init_hooks()?;
|
||||
let terminal = &mut term::init()?;
|
||||
App::default().run(terminal)?;
|
||||
term::restore()?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
use std::{
|
||||
io::{self, stdout},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use color_eyre::{eyre::WrapErr, Result};
|
||||
use crossterm::{
|
||||
event::{self, Event},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
};
|
||||
use ratatui::prelude::*;
|
||||
|
||||
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,95 +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 anyhow::{Context, Result};
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::Paragraph};
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// A more robust application would probably want to handle errors and ensure that the terminal is
|
||||
/// restored to a sane state before exiting. This example does not do that. It also does not handle
|
||||
/// events or update the application state. It just draws a greeting and exits when the user
|
||||
/// presses 'q'.
|
||||
fn main() -> Result<()> {
|
||||
let mut terminal = setup_terminal().context("setup failed")?;
|
||||
run(&mut terminal).context("app loop failed")?;
|
||||
restore_terminal(&mut terminal).context("restore terminal failed")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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. A more robust application would probably
|
||||
/// want to handle errors and ensure that the terminal is restored to a sane state before exiting.
|
||||
fn setup_terminal() -> Result<Terminal<CrosstermBackend<Stdout>>> {
|
||||
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(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
|
||||
disable_raw_mode().context("failed to disable raw mode")?;
|
||||
execute!(terminal.backend_mut(), LeaveAlternateScreen)
|
||||
.context("unable to switch to main screen")?;
|
||||
terminal.show_cursor().context("unable to show cursor")
|
||||
}
|
||||
|
||||
/// 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(crate::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.size());
|
||||
}
|
||||
|
||||
/// 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.
|
||||
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)
|
||||
}
|
||||
362
examples/list.rs
362
examples/list.rs
@@ -1,362 +0,0 @@
|
||||
//! # [Ratatui] List 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::enum_glob_use, clippy::wildcard_imports)]
|
||||
|
||||
use std::{error::Error, io, io::stdout};
|
||||
|
||||
use color_eyre::config::HookBuilder;
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode, KeyEventKind},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
};
|
||||
use ratatui::{prelude::*, style::palette::tailwind, widgets::*};
|
||||
|
||||
const TODO_HEADER_BG: Color = tailwind::BLUE.c950;
|
||||
const NORMAL_ROW_COLOR: Color = tailwind::SLATE.c950;
|
||||
const ALT_ROW_COLOR: Color = tailwind::SLATE.c900;
|
||||
const SELECTED_STYLE_FG: Color = tailwind::BLUE.c300;
|
||||
const TEXT_COLOR: Color = tailwind::SLATE.c200;
|
||||
const COMPLETED_TEXT_COLOR: Color = tailwind::GREEN.c500;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
enum Status {
|
||||
Todo,
|
||||
Completed,
|
||||
}
|
||||
|
||||
struct TodoItem<'a> {
|
||||
todo: &'a str,
|
||||
info: &'a str,
|
||||
status: Status,
|
||||
}
|
||||
|
||||
struct StatefulList<'a> {
|
||||
state: ListState,
|
||||
items: Vec<TodoItem<'a>>,
|
||||
last_selected: Option<usize>,
|
||||
}
|
||||
|
||||
/// This struct holds the current state of the app. In particular, it has the `items` field which is
|
||||
/// a wrapper around `ListState`. Keeping track of the items state let us render the associated
|
||||
/// widget with its state and have access to features such as natural scrolling.
|
||||
///
|
||||
/// Check the event handling at the bottom to see how to change the state on incoming events.
|
||||
/// Check the drawing logic for items on how to specify the highlighting style for selected items.
|
||||
struct App<'a> {
|
||||
items: StatefulList<'a>,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
init_error_hooks()?;
|
||||
let terminal = init_terminal()?;
|
||||
|
||||
// create app and run it
|
||||
App::new().run(terminal)?;
|
||||
|
||||
restore_terminal()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn init_error_hooks() -> color_eyre::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 _ = restore_terminal();
|
||||
error(e)
|
||||
}))?;
|
||||
std::panic::set_hook(Box::new(move |info| {
|
||||
let _ = restore_terminal();
|
||||
panic(info);
|
||||
}));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn init_terminal() -> color_eyre::Result<Terminal<impl Backend>> {
|
||||
enable_raw_mode()?;
|
||||
stdout().execute(EnterAlternateScreen)?;
|
||||
let backend = CrosstermBackend::new(stdout());
|
||||
let terminal = Terminal::new(backend)?;
|
||||
Ok(terminal)
|
||||
}
|
||||
|
||||
fn restore_terminal() -> color_eyre::Result<()> {
|
||||
disable_raw_mode()?;
|
||||
stdout().execute(LeaveAlternateScreen)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl<'a> App<'a> {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
items: StatefulList::with_items([
|
||||
("Rewrite everything with Rust!", "I can't hold my inner voice. He tells me to rewrite the complete universe with Rust", Status::Todo),
|
||||
("Rewrite all of your tui apps with Ratatui", "Yes, you heard that right. Go and replace your tui with Ratatui.", Status::Completed),
|
||||
("Pet your cat", "Minnak loves to be pet by you! Don't forget to pet and give some treats!", Status::Todo),
|
||||
("Walk with your dog", "Max is bored, go walk with him!", Status::Todo),
|
||||
("Pay the bills", "Pay the train subscription!!!", Status::Completed),
|
||||
("Refactor list example", "If you see this info that means I completed this task!", Status::Completed),
|
||||
]),
|
||||
}
|
||||
}
|
||||
|
||||
/// Changes the status of the selected list item
|
||||
fn change_status(&mut self) {
|
||||
if let Some(i) = self.items.state.selected() {
|
||||
self.items.items[i].status = match self.items.items[i].status {
|
||||
Status::Completed => Status::Todo,
|
||||
Status::Todo => Status::Completed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn go_top(&mut self) {
|
||||
self.items.state.select(Some(0));
|
||||
}
|
||||
|
||||
fn go_bottom(&mut self) {
|
||||
self.items.state.select(Some(self.items.items.len() - 1));
|
||||
}
|
||||
}
|
||||
|
||||
impl App<'_> {
|
||||
fn run(&mut self, mut terminal: Terminal<impl Backend>) -> io::Result<()> {
|
||||
loop {
|
||||
self.draw(&mut terminal)?;
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
use KeyCode::*;
|
||||
match key.code {
|
||||
Char('q') | Esc => return Ok(()),
|
||||
Char('h') | Left => self.items.unselect(),
|
||||
Char('j') | Down => self.items.next(),
|
||||
Char('k') | Up => self.items.previous(),
|
||||
Char('l') | Right | Enter => self.change_status(),
|
||||
Char('g') => self.go_top(),
|
||||
Char('G') => self.go_bottom(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(&mut self, terminal: &mut Terminal<impl Backend>) -> io::Result<()> {
|
||||
terminal.draw(|f| f.render_widget(self, f.size()))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for &mut App<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
// Create a space for header, todo list and the footer.
|
||||
let vertical = Layout::vertical([
|
||||
Constraint::Length(2),
|
||||
Constraint::Min(0),
|
||||
Constraint::Length(2),
|
||||
]);
|
||||
let [header_area, rest_area, footer_area] = vertical.areas(area);
|
||||
|
||||
// Create two chunks with equal vertical screen space. One for the list and the other for
|
||||
// the info block.
|
||||
let vertical = Layout::vertical([Constraint::Percentage(50), Constraint::Percentage(50)]);
|
||||
let [upper_item_list_area, lower_item_list_area] = vertical.areas(rest_area);
|
||||
|
||||
render_title(header_area, buf);
|
||||
self.render_todo(upper_item_list_area, buf);
|
||||
self.render_info(lower_item_list_area, buf);
|
||||
render_footer(footer_area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl App<'_> {
|
||||
fn render_todo(&mut self, area: Rect, buf: &mut Buffer) {
|
||||
// We create two blocks, one is for the header (outer) and the other is for list (inner).
|
||||
let outer_block = Block::new()
|
||||
.borders(Borders::NONE)
|
||||
.title_alignment(Alignment::Center)
|
||||
.title("TODO List")
|
||||
.fg(TEXT_COLOR)
|
||||
.bg(TODO_HEADER_BG);
|
||||
let inner_block = Block::new()
|
||||
.borders(Borders::NONE)
|
||||
.fg(TEXT_COLOR)
|
||||
.bg(NORMAL_ROW_COLOR);
|
||||
|
||||
// We get the inner area from outer_block. We'll use this area later to render the table.
|
||||
let outer_area = area;
|
||||
let inner_area = outer_block.inner(outer_area);
|
||||
|
||||
// We can render the header in outer_area.
|
||||
outer_block.render(outer_area, buf);
|
||||
|
||||
// Iterate through all elements in the `items` and stylize them.
|
||||
let items: Vec<ListItem> = self
|
||||
.items
|
||||
.items
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, todo_item)| todo_item.to_list_item(i))
|
||||
.collect();
|
||||
|
||||
// Create a List from all list items and highlight the currently selected one
|
||||
let items = List::new(items)
|
||||
.block(inner_block)
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.add_modifier(Modifier::REVERSED)
|
||||
.fg(SELECTED_STYLE_FG),
|
||||
)
|
||||
.highlight_symbol(">")
|
||||
.highlight_spacing(HighlightSpacing::Always);
|
||||
|
||||
// We can now render the item list
|
||||
// (look careful we are using StatefulWidget's render.)
|
||||
// ratatui::widgets::StatefulWidget::render as stateful_render
|
||||
StatefulWidget::render(items, inner_area, buf, &mut self.items.state);
|
||||
}
|
||||
|
||||
fn render_info(&self, area: Rect, buf: &mut Buffer) {
|
||||
// We get the info depending on the item's state.
|
||||
let info = if let Some(i) = self.items.state.selected() {
|
||||
match self.items.items[i].status {
|
||||
Status::Completed => "✓ DONE: ".to_string() + self.items.items[i].info,
|
||||
Status::Todo => "TODO: ".to_string() + self.items.items[i].info,
|
||||
}
|
||||
} else {
|
||||
"Nothing to see here...".to_string()
|
||||
};
|
||||
|
||||
// We show the list item's info under the list in this paragraph
|
||||
let outer_info_block = Block::new()
|
||||
.borders(Borders::NONE)
|
||||
.title_alignment(Alignment::Center)
|
||||
.title("TODO Info")
|
||||
.fg(TEXT_COLOR)
|
||||
.bg(TODO_HEADER_BG);
|
||||
let inner_info_block = Block::new()
|
||||
.borders(Borders::NONE)
|
||||
.padding(Padding::horizontal(1))
|
||||
.bg(NORMAL_ROW_COLOR);
|
||||
|
||||
// This is a similar process to what we did for list. outer_info_area will be used for
|
||||
// header inner_info_area will be used for the list info.
|
||||
let outer_info_area = area;
|
||||
let inner_info_area = outer_info_block.inner(outer_info_area);
|
||||
|
||||
// We can render the header. Inner info will be rendered later
|
||||
outer_info_block.render(outer_info_area, buf);
|
||||
|
||||
let info_paragraph = Paragraph::new(info)
|
||||
.block(inner_info_block)
|
||||
.fg(TEXT_COLOR)
|
||||
.wrap(Wrap { trim: false });
|
||||
|
||||
// We can now render the item info
|
||||
info_paragraph.render(inner_info_area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_title(area: Rect, buf: &mut Buffer) {
|
||||
Paragraph::new("Ratatui List Example")
|
||||
.bold()
|
||||
.centered()
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
fn render_footer(area: Rect, buf: &mut Buffer) {
|
||||
Paragraph::new("\nUse ↓↑ to move, ← to unselect, → to change status, g/G to go top/bottom.")
|
||||
.centered()
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
impl StatefulList<'_> {
|
||||
fn with_items<'a>(items: [(&'a str, &'a str, Status); 6]) -> StatefulList<'a> {
|
||||
StatefulList {
|
||||
state: ListState::default(),
|
||||
items: items.iter().map(TodoItem::from).collect(),
|
||||
last_selected: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn next(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i >= self.items.len() - 1 {
|
||||
0
|
||||
} else {
|
||||
i + 1
|
||||
}
|
||||
}
|
||||
None => self.last_selected.unwrap_or(0),
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
|
||||
fn previous(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i == 0 {
|
||||
self.items.len() - 1
|
||||
} else {
|
||||
i - 1
|
||||
}
|
||||
}
|
||||
None => self.last_selected.unwrap_or(0),
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
|
||||
fn unselect(&mut self) {
|
||||
let offset = self.state.offset();
|
||||
self.last_selected = self.state.selected();
|
||||
self.state.select(None);
|
||||
*self.state.offset_mut() = offset;
|
||||
}
|
||||
}
|
||||
|
||||
impl TodoItem<'_> {
|
||||
fn to_list_item(&self, index: usize) -> ListItem {
|
||||
let bg_color = match index % 2 {
|
||||
0 => NORMAL_ROW_COLOR,
|
||||
_ => ALT_ROW_COLOR,
|
||||
};
|
||||
let line = match self.status {
|
||||
Status::Todo => Line::styled(format!(" ☐ {}", self.todo), TEXT_COLOR),
|
||||
Status::Completed => Line::styled(
|
||||
format!(" ✓ {}", self.todo),
|
||||
(COMPLETED_TEXT_COLOR, bg_color),
|
||||
),
|
||||
};
|
||||
|
||||
ListItem::new(line).bg(bg_color)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&(&'a str, &'a str, Status)> for TodoItem<'a> {
|
||||
fn from((todo, info, status): &(&'a str, &'a str, Status)) -> Self {
|
||||
Self {
|
||||
todo,
|
||||
info,
|
||||
status: *status,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,44 +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 crossterm::{
|
||||
event::{self, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{backend::CrosstermBackend, 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.size()))?;
|
||||
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,150 +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 crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{Block, Paragraph},
|
||||
};
|
||||
|
||||
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.size());
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
//! # [Ratatui] Paragraph example
|
||||
//!
|
||||
//! The latest version of this example is available in the [examples] folder in the repository.
|
||||
//!
|
||||
//! Please note that the examples are designed to be run against the `main` branch of the Github
|
||||
//! repository. This means that you may not be able to compile with the latest release version on
|
||||
//! crates.io, or the one that you have installed locally.
|
||||
//!
|
||||
//! See the [examples readme] for more information on finding examples that match the version of the
|
||||
//! library you are using.
|
||||
//!
|
||||
//! [Ratatui]: https://github.com/ratatui-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 crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{Block, Paragraph, Wrap},
|
||||
};
|
||||
|
||||
struct App {
|
||||
scroll: u16,
|
||||
}
|
||||
|
||||
impl App {
|
||||
const fn new() -> Self {
|
||||
Self { scroll: 0 }
|
||||
}
|
||||
|
||||
fn on_tick(&mut self) {
|
||||
self.scroll += 1;
|
||||
self.scroll %= 10;
|
||||
}
|
||||
}
|
||||
|
||||
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 size = f.size();
|
||||
|
||||
// Words made "loooong" to demonstrate line breaking.
|
||||
let s = "Veeeeeeeeeeeeeeeery loooooooooooooooooong striiiiiiiiiiiiiiiiiiiiiiiiiing. ";
|
||||
let mut long_line = s.repeat(usize::from(size.width) / s.len() + 4);
|
||||
long_line.push('\n');
|
||||
|
||||
let block = Block::new().black();
|
||||
f.render_widget(block, size);
|
||||
|
||||
let layout = Layout::vertical([Constraint::Ratio(1, 4); 4]).split(size);
|
||||
|
||||
let text = vec![
|
||||
Line::from("This is a line "),
|
||||
Line::from("This is a line ".red()),
|
||||
Line::from("This is a line".on_blue()),
|
||||
Line::from("This is a longer line".crossed_out()),
|
||||
Line::from(long_line.on_green()),
|
||||
Line::from("This is a line".green().italic()),
|
||||
Line::from(vec![
|
||||
"Masked text: ".into(),
|
||||
Span::styled(
|
||||
Masked::new("password", '*'),
|
||||
Style::default().fg(Color::Red),
|
||||
),
|
||||
]),
|
||||
];
|
||||
|
||||
let create_block = |title| {
|
||||
Block::bordered()
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.title(Span::styled(
|
||||
title,
|
||||
Style::default().add_modifier(Modifier::BOLD),
|
||||
))
|
||||
};
|
||||
|
||||
let paragraph = Paragraph::new(text.clone())
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.block(create_block("Default alignment (Left), no wrap"));
|
||||
f.render_widget(paragraph, layout[0]);
|
||||
|
||||
let paragraph = Paragraph::new(text.clone())
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.block(create_block("Default alignment (Left), with wrap"))
|
||||
.wrap(Wrap { trim: true });
|
||||
f.render_widget(paragraph, layout[1]);
|
||||
|
||||
let paragraph = Paragraph::new(text.clone())
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.block(create_block("Right alignment, with wrap"))
|
||||
.right_aligned()
|
||||
.wrap(Wrap { trim: true });
|
||||
f.render_widget(paragraph, layout[2]);
|
||||
|
||||
let paragraph = Paragraph::new(text)
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.block(create_block("Center alignment, with wrap, with scroll"))
|
||||
.centered()
|
||||
.wrap(Wrap { trim: true })
|
||||
.scroll((app.scroll, 0));
|
||||
f.render_widget(paragraph, layout[3]);
|
||||
}
|
||||
@@ -1,127 +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 crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{Block, Clear, Paragraph, Wrap},
|
||||
};
|
||||
|
||||
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.size();
|
||||
|
||||
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,231 +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)]
|
||||
#![allow(clippy::wildcard_imports)]
|
||||
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{prelude::*, symbols::scrollbar, widgets::*};
|
||||
|
||||
#[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 size = f.size();
|
||||
|
||||
// Words made "loooong" to demonstrate line breaking.
|
||||
let s = "Veeeeeeeeeeeeeeeery loooooooooooooooooong striiiiiiiiiiiiiiiiiiiiiiiiiing. ";
|
||||
let mut long_line = s.repeat(usize::from(size.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(size);
|
||||
|
||||
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,180 +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 crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use rand::{
|
||||
distributions::{Distribution, Uniform},
|
||||
rngs::ThreadRng,
|
||||
};
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{Block, Borders, Sparkline},
|
||||
};
|
||||
|
||||
#[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.size());
|
||||
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]);
|
||||
}
|
||||
50
ratatui-core/Cargo.toml
Normal file
50
ratatui-core/Cargo.toml
Normal file
@@ -0,0 +1,50 @@
|
||||
[package]
|
||||
name = "ratatui-core"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[features]
|
||||
underline-color = []
|
||||
unstable-widget-ref = []
|
||||
|
||||
[dependencies]
|
||||
bitflags.workspace = true
|
||||
cassowary.workspace = true
|
||||
compact_str.workspace = true
|
||||
document-features.workspace = true
|
||||
instability.workspace = true
|
||||
itertools.workspace = true
|
||||
lru.workspace = true
|
||||
paste.workspace = true
|
||||
palette = { workspace = true, optional = true }
|
||||
serde = { workspace = true, optional = true, features = ["derive"] }
|
||||
strum = { workspace = true, features = ["derive"] }
|
||||
termwiz = { workspace = true, optional = true }
|
||||
unicode-segmentation.workspace = true
|
||||
unicode-truncate.workspace = true
|
||||
unicode-width.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
argh = "0.1.12"
|
||||
color-eyre = "0.6.2"
|
||||
criterion = { version = "0.5.1", features = ["html_reports"] }
|
||||
crossterm = { version = "0.28.1", features = ["event-stream"] }
|
||||
fakeit = "1.1"
|
||||
font8x8 = "0.3.1"
|
||||
futures = "0.3.30"
|
||||
indoc = "2"
|
||||
octocrab = "0.40.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"] }
|
||||
@@ -96,30 +96,18 @@
|
||||
//! [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
|
||||
//! [Examples]: https://github.com/ratatui/ratatui/tree/main/examples/README.md
|
||||
//! [Backend Comparison]:
|
||||
//! https://ratatui.rs/concepts/backends/comparison/
|
||||
//! [Ratatui Website]: https://ratatui-org.github.io/ratatui-book
|
||||
//! [Ratatui Website]: https://ratatui.rs
|
||||
use std::io;
|
||||
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
use crate::{buffer::Cell, layout::Size, prelude::Rect};
|
||||
|
||||
#[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;
|
||||
use crate::{
|
||||
buffer::Cell,
|
||||
layout::{Position, Size},
|
||||
};
|
||||
|
||||
mod test;
|
||||
pub use self::test::TestBackend;
|
||||
@@ -192,25 +180,25 @@ pub trait Backend {
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
///
|
||||
/// [`show_cursor`]: Backend::show_cursor
|
||||
/// [`show_cursor`]: Self::show_cursor
|
||||
fn hide_cursor(&mut self) -> io::Result<()>;
|
||||
|
||||
/// Show the cursor on the terminal screen.
|
||||
///
|
||||
/// See [`hide_cursor`] for an example.
|
||||
///
|
||||
/// [`hide_cursor`]: Backend::hide_cursor
|
||||
/// [`hide_cursor`]: Self::hide_cursor
|
||||
fn show_cursor(&mut self) -> io::Result<()>;
|
||||
|
||||
/// Get the current cursor position on the terminal screen.
|
||||
///
|
||||
/// The returned tuple contains the x and y coordinates of the cursor. The origin
|
||||
/// (0, 0) is at the top left corner of the screen.
|
||||
/// The returned tuple contains the x and y coordinates of the cursor.
|
||||
/// The origin (0, 0) is at the top left corner of the screen.
|
||||
///
|
||||
/// See [`set_cursor`] for an example.
|
||||
/// See [`set_cursor_position`] for an example.
|
||||
///
|
||||
/// [`set_cursor`]: Backend::set_cursor
|
||||
fn get_cursor(&mut self) -> io::Result<(u16, u16)>;
|
||||
/// [`set_cursor_position`]: Self::set_cursor_position
|
||||
fn get_cursor_position(&mut self) -> io::Result<Position>;
|
||||
|
||||
/// Set the cursor position on the terminal screen to the given x and y coordinates.
|
||||
///
|
||||
@@ -220,14 +208,31 @@ pub trait Backend {
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::backend::{Backend, TestBackend};
|
||||
/// # use ratatui::layout::Position;
|
||||
/// # let mut backend = TestBackend::new(80, 25);
|
||||
/// backend.set_cursor(10, 20)?;
|
||||
/// assert_eq!(backend.get_cursor()?, (10, 20));
|
||||
/// backend.set_cursor_position(Position { x: 10, y: 20 })?;
|
||||
/// assert_eq!(backend.get_cursor_position()?, Position { x: 10, y: 20 });
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()>;
|
||||
|
||||
/// Get the current cursor position on the terminal screen.
|
||||
///
|
||||
/// [`get_cursor`]: Backend::get_cursor
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()>;
|
||||
/// The returned tuple contains the x and y coordinates of the cursor. The origin
|
||||
/// (0, 0) is at the top left corner of the screen.
|
||||
#[deprecated = "the method get_cursor_position indicates more clearly what about the cursor to get"]
|
||||
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
|
||||
let Position { x, y } = self.get_cursor_position()?;
|
||||
Ok((x, y))
|
||||
}
|
||||
|
||||
/// Set the cursor position on the terminal screen to the given x and y coordinates.
|
||||
///
|
||||
/// The origin (0, 0) is at the top left corner of the screen.
|
||||
#[deprecated = "the method set_cursor_position indicates more clearly what about the cursor to set"]
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
||||
self.set_cursor_position(Position { x, y })
|
||||
}
|
||||
|
||||
/// Clears the whole terminal screen
|
||||
///
|
||||
@@ -261,7 +266,7 @@ pub trait Backend {
|
||||
/// This method will return an error if the terminal screen could not be cleared. It will also
|
||||
/// return an error if the `clear_type` is not supported by the backend.
|
||||
///
|
||||
/// [`clear`]: Backend::clear
|
||||
/// [`clear`]: Self::clear
|
||||
fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
|
||||
match clear_type {
|
||||
ClearType::All => self.clear(),
|
||||
@@ -275,19 +280,19 @@ pub trait Backend {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the size of the terminal screen in columns/rows as a [`Rect`].
|
||||
/// Get the size of the terminal screen in columns/rows as a [`Size`].
|
||||
///
|
||||
/// The returned [`Rect`] contains the width and height of the terminal screen.
|
||||
/// The returned [`Size`] contains the width and height of the terminal screen.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, backend::TestBackend};
|
||||
/// let backend = TestBackend::new(80, 25);
|
||||
/// assert_eq!(backend.size()?, Rect::new(0, 0, 80, 25));
|
||||
/// assert_eq!(backend.size()?, Size::new(80, 25));
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
fn size(&self) -> io::Result<Rect>;
|
||||
fn size(&self) -> io::Result<Size>;
|
||||
|
||||
/// Get the size of the terminal screen in columns/rows and pixels as a [`WindowSize`].
|
||||
///
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
use std::{
|
||||
fmt::{self, Write},
|
||||
io,
|
||||
io, iter,
|
||||
};
|
||||
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
@@ -11,7 +11,7 @@ use unicode_width::UnicodeWidthStr;
|
||||
use crate::{
|
||||
backend::{Backend, ClearType, WindowSize},
|
||||
buffer::{Buffer, Cell},
|
||||
layout::{Rect, Size},
|
||||
layout::{Position, Rect, Size},
|
||||
};
|
||||
|
||||
/// A [`Backend`] implementation used for integration testing that renders to an memory buffer.
|
||||
@@ -34,9 +34,8 @@ use crate::{
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct TestBackend {
|
||||
width: u16,
|
||||
buffer: Buffer,
|
||||
height: u16,
|
||||
scrollback: Buffer,
|
||||
cursor: bool,
|
||||
pos: (u16, u16),
|
||||
}
|
||||
@@ -74,9 +73,8 @@ impl TestBackend {
|
||||
/// Creates a new `TestBackend` with the specified width and height.
|
||||
pub fn new(width: u16, height: u16) -> Self {
|
||||
Self {
|
||||
width,
|
||||
height,
|
||||
buffer: Buffer::empty(Rect::new(0, 0, width, height)),
|
||||
scrollback: Buffer::empty(Rect::new(0, 0, width, 0)),
|
||||
cursor: false,
|
||||
pos: (0, 0),
|
||||
}
|
||||
@@ -87,11 +85,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));
|
||||
self.width = width;
|
||||
self.height = 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.
|
||||
@@ -99,6 +115,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)]
|
||||
@@ -108,11 +125,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]
|
||||
@@ -123,6 +171,37 @@ 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]
|
||||
pub fn assert_cursor_position<P: Into<Position>>(&mut self, position: P) {
|
||||
let actual = self.get_cursor_position().unwrap();
|
||||
assert_eq!(actual, position.into());
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for TestBackend {
|
||||
@@ -139,8 +218,7 @@ impl Backend for TestBackend {
|
||||
I: Iterator<Item = (u16, u16, &'a Cell)>,
|
||||
{
|
||||
for (x, y, c) in content {
|
||||
let cell = self.buffer.get_mut(x, y);
|
||||
*cell = c.clone();
|
||||
self.buffer[(x, y)] = c.clone();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -155,12 +233,12 @@ impl Backend for TestBackend {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
|
||||
Ok(self.pos)
|
||||
fn get_cursor_position(&mut self) -> io::Result<Position> {
|
||||
Ok(self.pos.into())
|
||||
}
|
||||
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
||||
self.pos = (x, y);
|
||||
fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()> {
|
||||
self.pos = position.into().into();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -170,26 +248,29 @@ impl Backend for TestBackend {
|
||||
}
|
||||
|
||||
fn clear_region(&mut self, clear_type: super::ClearType) -> io::Result<()> {
|
||||
match clear_type {
|
||||
ClearType::All => self.clear()?,
|
||||
let region = match clear_type {
|
||||
ClearType::All => return self.clear(),
|
||||
ClearType::AfterCursor => {
|
||||
let index = self.buffer.index_of(self.pos.0, self.pos.1) + 1;
|
||||
self.buffer.content[index..].fill(Cell::default());
|
||||
&mut self.buffer.content[index..]
|
||||
}
|
||||
ClearType::BeforeCursor => {
|
||||
let index = self.buffer.index_of(self.pos.0, self.pos.1);
|
||||
self.buffer.content[..index].fill(Cell::default());
|
||||
&mut self.buffer.content[..index]
|
||||
}
|
||||
ClearType::CurrentLine => {
|
||||
let line_start_index = self.buffer.index_of(0, self.pos.1);
|
||||
let line_end_index = self.buffer.index_of(self.width - 1, self.pos.1);
|
||||
self.buffer.content[line_start_index..=line_end_index].fill(Cell::default());
|
||||
let line_end_index = self.buffer.index_of(self.buffer.area.width - 1, self.pos.1);
|
||||
&mut self.buffer.content[line_start_index..=line_end_index]
|
||||
}
|
||||
ClearType::UntilNewLine => {
|
||||
let index = self.buffer.index_of(self.pos.0, self.pos.1);
|
||||
let line_end_index = self.buffer.index_of(self.width - 1, self.pos.1);
|
||||
self.buffer.content[index..=line_end_index].fill(Cell::default());
|
||||
let line_end_index = self.buffer.index_of(self.buffer.area.width - 1, self.pos.1);
|
||||
&mut self.buffer.content[index..=line_end_index]
|
||||
}
|
||||
};
|
||||
for cell in region {
|
||||
cell.reset();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -206,46 +287,55 @@ 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<()> {
|
||||
let (cur_x, cur_y) = self.get_cursor()?;
|
||||
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;
|
||||
|
||||
// the next column ensuring that we don't go past the last column
|
||||
let new_cursor_x = cur_x.saturating_add(1).min(self.width.saturating_sub(1));
|
||||
let new_cursor_x = cur_x.saturating_add(1).min(width.saturating_sub(1));
|
||||
|
||||
let max_y = self.height.saturating_sub(1);
|
||||
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 == self.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(0, rotate_by)?;
|
||||
self.clear_region(ClearType::BeforeCursor)?;
|
||||
self.buffer
|
||||
.content
|
||||
.rotate_left((self.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);
|
||||
self.set_cursor(new_cursor_x, new_cursor_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(())
|
||||
}
|
||||
|
||||
fn size(&self) -> io::Result<Rect> {
|
||||
Ok(Rect::new(0, 0, self.width, self.height))
|
||||
fn size(&self) -> io::Result<Size> {
|
||||
Ok(self.buffer.area.as_size())
|
||||
}
|
||||
|
||||
fn window_size(&mut self) -> io::Result<WindowSize> {
|
||||
// Some arbitrary window pixel size, probably doesn't need much testing.
|
||||
static WINDOW_PIXEL_SIZE: Size = Size {
|
||||
const WINDOW_PIXEL_SIZE: Size = Size {
|
||||
width: 640,
|
||||
height: 480,
|
||||
};
|
||||
Ok(WindowSize {
|
||||
columns_rows: (self.width, self.height).into(),
|
||||
columns_rows: self.buffer.area.as_size(),
|
||||
pixels: WINDOW_PIXEL_SIZE,
|
||||
})
|
||||
}
|
||||
@@ -255,8 +345,25 @@ impl Backend for TestBackend {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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]
|
||||
@@ -264,9 +371,8 @@ mod tests {
|
||||
assert_eq!(
|
||||
TestBackend::new(10, 2),
|
||||
TestBackend {
|
||||
width: 10,
|
||||
height: 2,
|
||||
buffer: Buffer::with_lines([" "; 2]),
|
||||
scrollback: Buffer::empty(Rect::new(0, 0, 10, 0)),
|
||||
cursor: false,
|
||||
pos: (0, 0),
|
||||
}
|
||||
@@ -317,6 +423,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);
|
||||
@@ -326,8 +439,7 @@ mod tests {
|
||||
#[test]
|
||||
fn draw() {
|
||||
let mut backend = TestBackend::new(10, 2);
|
||||
let mut cell = Cell::default();
|
||||
cell.set_symbol("a");
|
||||
let cell = Cell::new("a");
|
||||
backend.draw([(0, 0, &cell)].into_iter()).unwrap();
|
||||
backend.draw([(0, 1, &cell)].into_iter()).unwrap();
|
||||
backend.assert_buffer_lines(["a "; 2]);
|
||||
@@ -348,23 +460,30 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_cursor() {
|
||||
fn get_cursor_position() {
|
||||
let mut backend = TestBackend::new(10, 2);
|
||||
assert_eq!(backend.get_cursor().unwrap(), (0, 0));
|
||||
assert_eq!(backend.get_cursor_position().unwrap(), Position::ORIGIN);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_cursor() {
|
||||
fn assert_cursor_position() {
|
||||
let mut backend = TestBackend::new(10, 2);
|
||||
backend.assert_cursor_position(Position::ORIGIN);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_cursor_position() {
|
||||
let mut backend = TestBackend::new(10, 10);
|
||||
backend.set_cursor(5, 5).unwrap();
|
||||
backend
|
||||
.set_cursor_position(Position { x: 5, y: 5 })
|
||||
.unwrap();
|
||||
assert_eq!(backend.pos, (5, 5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear() {
|
||||
let mut backend = TestBackend::new(4, 2);
|
||||
let mut cell = Cell::default();
|
||||
cell.set_symbol("a");
|
||||
let cell = Cell::new("a");
|
||||
backend.draw([(0, 0, &cell)].into_iter()).unwrap();
|
||||
backend.draw([(0, 1, &cell)].into_iter()).unwrap();
|
||||
backend.clear().unwrap();
|
||||
@@ -403,7 +522,9 @@ mod tests {
|
||||
"aaaaaaaaaa",
|
||||
]);
|
||||
|
||||
backend.set_cursor(3, 2).unwrap();
|
||||
backend
|
||||
.set_cursor_position(Position { x: 3, y: 2 })
|
||||
.unwrap();
|
||||
backend.clear_region(ClearType::AfterCursor).unwrap();
|
||||
backend.assert_buffer_lines([
|
||||
"aaaaaaaaaa",
|
||||
@@ -425,7 +546,9 @@ mod tests {
|
||||
"aaaaaaaaaa",
|
||||
]);
|
||||
|
||||
backend.set_cursor(5, 3).unwrap();
|
||||
backend
|
||||
.set_cursor_position(Position { x: 5, y: 3 })
|
||||
.unwrap();
|
||||
backend.clear_region(ClearType::BeforeCursor).unwrap();
|
||||
backend.assert_buffer_lines([
|
||||
" ",
|
||||
@@ -447,7 +570,9 @@ mod tests {
|
||||
"aaaaaaaaaa",
|
||||
]);
|
||||
|
||||
backend.set_cursor(3, 1).unwrap();
|
||||
backend
|
||||
.set_cursor_position(Position { x: 3, y: 1 })
|
||||
.unwrap();
|
||||
backend.clear_region(ClearType::CurrentLine).unwrap();
|
||||
backend.assert_buffer_lines([
|
||||
"aaaaaaaaaa",
|
||||
@@ -469,7 +594,9 @@ mod tests {
|
||||
"aaaaaaaaaa",
|
||||
]);
|
||||
|
||||
backend.set_cursor(3, 0).unwrap();
|
||||
backend
|
||||
.set_cursor_position(Position { x: 3, y: 0 })
|
||||
.unwrap();
|
||||
backend.clear_region(ClearType::UntilNewLine).unwrap();
|
||||
backend.assert_buffer_lines([
|
||||
"aaa ",
|
||||
@@ -491,22 +618,22 @@ mod tests {
|
||||
"eeeeeeeeee",
|
||||
]);
|
||||
|
||||
backend.set_cursor(0, 0).unwrap();
|
||||
backend.set_cursor_position(Position::ORIGIN).unwrap();
|
||||
|
||||
// If the cursor is not at the last line in the terminal the addition of a
|
||||
// newline simply moves the cursor down and to the right
|
||||
|
||||
backend.append_lines(1).unwrap();
|
||||
assert_eq!(backend.get_cursor().unwrap(), (1, 1));
|
||||
backend.assert_cursor_position(Position { x: 1, y: 1 });
|
||||
|
||||
backend.append_lines(1).unwrap();
|
||||
assert_eq!(backend.get_cursor().unwrap(), (2, 2));
|
||||
backend.assert_cursor_position(Position { x: 2, y: 2 });
|
||||
|
||||
backend.append_lines(1).unwrap();
|
||||
assert_eq!(backend.get_cursor().unwrap(), (3, 3));
|
||||
backend.assert_cursor_position(Position { x: 3, y: 3 });
|
||||
|
||||
backend.append_lines(1).unwrap();
|
||||
assert_eq!(backend.get_cursor().unwrap(), (4, 4));
|
||||
backend.assert_cursor_position(Position { x: 4, y: 4 });
|
||||
|
||||
// As such the buffer should remain unchanged
|
||||
backend.assert_buffer_lines([
|
||||
@@ -516,6 +643,7 @@ mod tests {
|
||||
"dddddddddd",
|
||||
"eeeeeeeeee",
|
||||
]);
|
||||
backend.assert_scrollback_empty();
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -531,21 +659,24 @@ mod tests {
|
||||
|
||||
// If the cursor is at the last line in the terminal the addition of a
|
||||
// newline will scroll the contents of the buffer
|
||||
backend.set_cursor(0, 4).unwrap();
|
||||
backend
|
||||
.set_cursor_position(Position { x: 0, y: 4 })
|
||||
.unwrap();
|
||||
|
||||
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
|
||||
assert_eq!(backend.get_cursor().unwrap(), (1, 4));
|
||||
backend.assert_cursor_position(Position { x: 1, y: 4 });
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -559,13 +690,13 @@ mod tests {
|
||||
"eeeeeeeeee",
|
||||
]);
|
||||
|
||||
backend.set_cursor(0, 0).unwrap();
|
||||
backend.set_cursor_position(Position::ORIGIN).unwrap();
|
||||
|
||||
// If the cursor is not at the last line in the terminal the addition of multiple
|
||||
// newlines simply moves the cursor n lines down and to the right by 1
|
||||
|
||||
backend.append_lines(4).unwrap();
|
||||
assert_eq!(backend.get_cursor().unwrap(), (1, 4));
|
||||
backend.assert_cursor_position(Position { x: 1, y: 4 });
|
||||
|
||||
// As such the buffer should remain unchanged
|
||||
backend.assert_buffer_lines([
|
||||
@@ -575,6 +706,7 @@ mod tests {
|
||||
"dddddddddd",
|
||||
"eeeeeeeeee",
|
||||
]);
|
||||
backend.assert_scrollback_empty();
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -588,10 +720,12 @@ mod tests {
|
||||
"eeeeeeeeee",
|
||||
]);
|
||||
|
||||
backend.set_cursor(0, 3).unwrap();
|
||||
backend
|
||||
.set_cursor_position(Position { x: 0, y: 3 })
|
||||
.unwrap();
|
||||
|
||||
backend.append_lines(3).unwrap();
|
||||
assert_eq!(backend.get_cursor().unwrap(), (1, 4));
|
||||
backend.assert_cursor_position(Position { x: 1, y: 4 });
|
||||
|
||||
backend.assert_buffer_lines([
|
||||
"cccccccccc",
|
||||
@@ -600,6 +734,7 @@ mod tests {
|
||||
" ",
|
||||
" ",
|
||||
]);
|
||||
backend.assert_scrollback_lines(["aaaaaaaaaa", "bbbbbbbbbb"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -613,10 +748,12 @@ mod tests {
|
||||
"eeeeeeeeee",
|
||||
]);
|
||||
|
||||
backend.set_cursor(0, 4).unwrap();
|
||||
backend
|
||||
.set_cursor_position(Position { x: 0, y: 4 })
|
||||
.unwrap();
|
||||
|
||||
backend.append_lines(5).unwrap();
|
||||
assert_eq!(backend.get_cursor().unwrap(), (1, 4));
|
||||
backend.assert_cursor_position(Position { x: 1, y: 4 });
|
||||
|
||||
backend.assert_buffer_lines([
|
||||
" ",
|
||||
@@ -625,6 +762,13 @@ mod tests {
|
||||
" ",
|
||||
" ",
|
||||
]);
|
||||
backend.assert_scrollback_lines([
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
"dddddddddd",
|
||||
"eeeeeeeeee",
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -638,10 +782,10 @@ mod tests {
|
||||
"eeeeeeeeee",
|
||||
]);
|
||||
|
||||
backend.set_cursor(0, 0).unwrap();
|
||||
backend.set_cursor_position(Position::ORIGIN).unwrap();
|
||||
|
||||
backend.append_lines(5).unwrap();
|
||||
assert_eq!(backend.get_cursor().unwrap(), (1, 4));
|
||||
backend.assert_cursor_position(Position { x: 1, y: 4 });
|
||||
|
||||
backend.assert_buffer_lines([
|
||||
"bbbbbbbbbb",
|
||||
@@ -650,12 +794,121 @@ 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::new(10, 5);
|
||||
backend.buffer = Buffer::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]
|
||||
fn size() {
|
||||
let backend = TestBackend::new(10, 2);
|
||||
assert_eq!(backend.size().unwrap(), Rect::new(0, 0, 10, 2));
|
||||
assert_eq!(backend.size().unwrap(), Size::new(10, 2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2,7 +2,6 @@
|
||||
//! A module for the [`Buffer`] and [`Cell`] types.
|
||||
|
||||
mod assert;
|
||||
#[allow(clippy::module_inception)]
|
||||
mod buffer;
|
||||
mod cell;
|
||||
|
||||
@@ -18,7 +18,7 @@ macro_rules! assert_buffer_eq {
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, (x, y, cell))| {
|
||||
let expected_cell = expected.get(x, y);
|
||||
let expected_cell = &expected[(x, y)];
|
||||
format!("{i}: at ({x}, {y})\n expected: {expected_cell:?}\n actual: {cell:?}")
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
@@ -1,9 +1,12 @@
|
||||
use std::fmt;
|
||||
use std::{
|
||||
fmt,
|
||||
ops::{Index, IndexMut},
|
||||
};
|
||||
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{buffer::Cell, prelude::*};
|
||||
use crate::{buffer::Cell, layout::Position, prelude::*};
|
||||
|
||||
/// A buffer that maps to the desired content of the terminal after the draw call
|
||||
///
|
||||
@@ -15,16 +18,34 @@ use crate::{buffer::Cell, prelude::*};
|
||||
/// # Examples:
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::{buffer::Cell, prelude::*};
|
||||
/// use ratatui::{
|
||||
/// buffer::{Buffer, Cell},
|
||||
/// layout::{Position, Rect},
|
||||
/// style::{Color, Style},
|
||||
/// };
|
||||
///
|
||||
/// # fn foo() -> Option<()> {
|
||||
/// let mut buf = Buffer::empty(Rect {
|
||||
/// x: 0,
|
||||
/// y: 0,
|
||||
/// width: 10,
|
||||
/// height: 5,
|
||||
/// });
|
||||
/// buf.get_mut(0, 2).set_symbol("x");
|
||||
/// assert_eq!(buf.get(0, 2).symbol(), "x");
|
||||
///
|
||||
/// // indexing using Position
|
||||
/// buf[Position { x: 0, y: 0 }].set_symbol("A");
|
||||
/// assert_eq!(buf[Position { x: 0, y: 0 }].symbol(), "A");
|
||||
///
|
||||
/// // indexing using (x, y) tuple (which is converted to Position)
|
||||
/// buf[(0, 1)].set_symbol("B");
|
||||
/// assert_eq!(buf[(0, 1)].symbol(), "x");
|
||||
///
|
||||
/// // getting an Option instead of panicking if the position is outside the buffer
|
||||
/// let cell = buf.cell_mut(Position { x: 0, y: 2 })?;
|
||||
/// cell.set_symbol("C");
|
||||
///
|
||||
/// let cell = buf.cell(Position { x: 0, y: 2 })?;
|
||||
/// assert_eq!(cell.symbol(), "C");
|
||||
///
|
||||
/// buf.set_string(
|
||||
/// 3,
|
||||
@@ -32,13 +53,12 @@ use crate::{buffer::Cell, prelude::*};
|
||||
/// "string",
|
||||
/// Style::default().fg(Color::Red).bg(Color::White),
|
||||
/// );
|
||||
/// let cell = buf.get(5, 0);
|
||||
/// let cell = &buf[(5, 0)]; // cannot move out of buf, so we borrow it
|
||||
/// assert_eq!(cell.symbol(), "r");
|
||||
/// assert_eq!(cell.fg, Color::Red);
|
||||
/// assert_eq!(cell.bg, Color::White);
|
||||
///
|
||||
/// buf.get_mut(5, 0).set_char('x');
|
||||
/// assert_eq!(buf.get(5, 0).symbol(), "x");
|
||||
/// # Some(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[derive(Default, Clone, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
@@ -54,14 +74,14 @@ impl Buffer {
|
||||
/// Returns a Buffer with all cells set to the default one
|
||||
#[must_use]
|
||||
pub fn empty(area: Rect) -> Self {
|
||||
Self::filled(area, &Cell::default())
|
||||
Self::filled(area, Cell::EMPTY)
|
||||
}
|
||||
|
||||
/// Returns a Buffer with all cells initialized with the attributes of the given Cell
|
||||
#[must_use]
|
||||
pub fn filled(area: Rect, cell: &Cell) -> Self {
|
||||
pub fn filled(area: Rect, cell: Cell) -> Self {
|
||||
let size = area.area() as usize;
|
||||
let content = vec![cell.clone(); size];
|
||||
let content = vec![cell; size];
|
||||
Self { area, content }
|
||||
}
|
||||
|
||||
@@ -92,20 +112,101 @@ impl Buffer {
|
||||
&self.area
|
||||
}
|
||||
|
||||
/// Returns a reference to Cell at the given coordinates
|
||||
/// Returns a reference to the [`Cell`] at the given coordinates
|
||||
///
|
||||
/// Callers should use [`Buffer[]`](Self::index) or [`Buffer::cell`] instead of this method.
|
||||
///
|
||||
/// Note: idiomatically methods named `get` usually return `Option<&T>`, but this method panics
|
||||
/// instead. This is kept for backwards compatibility. See [`cell`](Self::cell) for a safe
|
||||
/// alternative.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the index is out of bounds.
|
||||
#[track_caller]
|
||||
#[deprecated(note = "Use Buffer[] or Buffer::cell instead")]
|
||||
#[must_use]
|
||||
pub fn get(&self, x: u16, y: u16) -> &Cell {
|
||||
let i = self.index_of(x, y);
|
||||
&self.content[i]
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to Cell at the given coordinates
|
||||
/// Returns a mutable reference to the [`Cell`] at the given coordinates.
|
||||
///
|
||||
/// Callers should use [`Buffer[]`](Self::index_mut) or [`Buffer::cell_mut`] instead of this
|
||||
/// method.
|
||||
///
|
||||
/// Note: idiomatically methods named `get_mut` usually return `Option<&mut T>`, but this method
|
||||
/// panics instead. This is kept for backwards compatibility. See [`cell_mut`](Self::cell_mut)
|
||||
/// for a safe alternative.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the position is outside the `Buffer`'s area.
|
||||
#[track_caller]
|
||||
#[deprecated(note = "Use Buffer[] or Buffer::cell_mut instead")]
|
||||
#[must_use]
|
||||
pub fn get_mut(&mut self, x: u16, y: u16) -> &mut Cell {
|
||||
let i = self.index_of(x, y);
|
||||
&mut self.content[i]
|
||||
}
|
||||
|
||||
/// Returns a reference to the [`Cell`] at the given position or [`None`] if the position is
|
||||
/// outside the `Buffer`'s area.
|
||||
///
|
||||
/// This method accepts any value that can be converted to [`Position`] (e.g. `(x, y)` or
|
||||
/// `Position::new(x, y)`).
|
||||
///
|
||||
/// For a method that panics when the position is outside the buffer instead of returning
|
||||
/// `None`, use [`Buffer[]`](Self::index).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, buffer::Cell, layout::Position};
|
||||
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 10));
|
||||
///
|
||||
/// assert_eq!(buffer.cell(Position::new(0, 0)), Some(&Cell::default()));
|
||||
/// assert_eq!(buffer.cell(Position::new(10, 10)), None);
|
||||
/// assert_eq!(buffer.cell((0, 0)), Some(&Cell::default()));
|
||||
/// assert_eq!(buffer.cell((10, 10)), None);
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn cell<P: Into<Position>>(&self, position: P) -> Option<&Cell> {
|
||||
let position = position.into();
|
||||
let index = self.index_of_opt(position)?;
|
||||
self.content.get(index)
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the [`Cell`] at the given position or [`None`] if the
|
||||
/// position is outside the `Buffer`'s area.
|
||||
///
|
||||
/// This method accepts any value that can be converted to [`Position`] (e.g. `(x, y)` or
|
||||
/// `Position::new(x, y)`).
|
||||
///
|
||||
/// For a method that panics when the position is outside the buffer instead of returning
|
||||
/// `None`, use [`Buffer[]`](Self::index_mut).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, buffer::Cell, layout::Position};
|
||||
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 10));
|
||||
///
|
||||
/// if let Some(cell) = buffer.cell_mut(Position::new(0, 0)) {
|
||||
/// cell.set_symbol("A");
|
||||
/// }
|
||||
/// if let Some(cell) = buffer.cell_mut((0, 0)) {
|
||||
/// cell.set_style(Style::default().fg(Color::Red));
|
||||
/// }
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn cell_mut<P: Into<Position>>(&mut self, position: P) -> Option<&mut Cell> {
|
||||
let position = position.into();
|
||||
let index = self.index_of_opt(position)?;
|
||||
self.content.get_mut(index)
|
||||
}
|
||||
|
||||
/// Returns the index in the `Vec<Cell>` for the given global (x, y) coordinates.
|
||||
///
|
||||
/// Global coordinates are offset by the Buffer's area offset (`x`/`y`).
|
||||
@@ -114,8 +215,7 @@ impl Buffer {
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let rect = Rect::new(200, 100, 10, 10);
|
||||
/// let buffer = Buffer::empty(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);
|
||||
/// ```
|
||||
@@ -126,23 +226,37 @@ impl Buffer {
|
||||
///
|
||||
/// ```should_panic
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let rect = Rect::new(200, 100, 10, 10);
|
||||
/// let buffer = Buffer::empty(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).
|
||||
/// buffer.index_of(0, 0); // Panics
|
||||
/// ```
|
||||
#[track_caller]
|
||||
#[must_use]
|
||||
pub fn index_of(&self, x: u16, y: u16) -> usize {
|
||||
debug_assert!(
|
||||
x >= self.area.left()
|
||||
&& x < self.area.right()
|
||||
&& y >= self.area.top()
|
||||
&& y < self.area.bottom(),
|
||||
"Trying to access position outside the buffer: x={x}, y={y}, area={:?}",
|
||||
self.area
|
||||
);
|
||||
((y - self.area.y) * self.area.width + (x - self.area.x)) as usize
|
||||
self.index_of_opt(Position { x, y }).unwrap_or_else(|| {
|
||||
panic!(
|
||||
"index outside of buffer: the area is {area:?} but index is ({x}, {y})",
|
||||
area = self.area,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the index in the `Vec<Cell>` for the given global (x, y) coordinates.
|
||||
///
|
||||
/// Returns `None` if the given coordinates are outside of the Buffer's area.
|
||||
///
|
||||
/// 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;
|
||||
if !area.contains(position) {
|
||||
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)
|
||||
}
|
||||
|
||||
/// Returns the (global) coordinates of a cell given its index
|
||||
@@ -170,6 +284,7 @@ impl Buffer {
|
||||
/// // 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) {
|
||||
debug_assert!(
|
||||
i < self.content.len(),
|
||||
@@ -192,7 +307,7 @@ impl Buffer {
|
||||
}
|
||||
|
||||
/// Print at most the first n characters of a string if enough space is available
|
||||
/// until the end of the line.
|
||||
/// until the end of the line. Skips zero-width graphemes and control characters.
|
||||
///
|
||||
/// Use [`Buffer::set_string`] when the maximum amount of characters can be printed.
|
||||
pub fn set_stringn<T, S>(
|
||||
@@ -210,6 +325,7 @@ impl Buffer {
|
||||
let max_width = max_width.try_into().unwrap_or(u16::MAX);
|
||||
let mut remaining_width = self.area.right().saturating_sub(x).min(max_width);
|
||||
let graphemes = UnicodeSegmentation::graphemes(string.as_ref(), true)
|
||||
.filter(|symbol| !symbol.contains(|char: char| char.is_control()))
|
||||
.map(|symbol| (symbol, symbol.width() as u16))
|
||||
.filter(|(_symbol, width)| *width > 0)
|
||||
.map_while(|(symbol, width)| {
|
||||
@@ -218,12 +334,12 @@ impl Buffer {
|
||||
});
|
||||
let style = style.into();
|
||||
for (symbol, width) in graphemes {
|
||||
self.get_mut(x, y).set_symbol(symbol).set_style(style);
|
||||
self[(x, y)].set_symbol(symbol).set_style(style);
|
||||
let next_symbol = x + width;
|
||||
x += 1;
|
||||
// Reset following cells if multi-width (they would be hidden by the grapheme),
|
||||
while x < next_symbol {
|
||||
self.get_mut(x, y).reset();
|
||||
self[(x, y)].reset();
|
||||
x += 1;
|
||||
}
|
||||
}
|
||||
@@ -266,7 +382,7 @@ impl Buffer {
|
||||
let area = self.area.intersection(area);
|
||||
for y in area.top()..area.bottom() {
|
||||
for x in area.left()..area.right() {
|
||||
self.get_mut(x, y).set_style(style);
|
||||
self[(x, y)].set_style(style);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -278,22 +394,22 @@ impl Buffer {
|
||||
if self.content.len() > length {
|
||||
self.content.truncate(length);
|
||||
} else {
|
||||
self.content.resize(length, Cell::default());
|
||||
self.content.resize(length, Cell::EMPTY);
|
||||
}
|
||||
self.area = area;
|
||||
}
|
||||
|
||||
/// Reset all cells in the buffer
|
||||
pub fn reset(&mut self) {
|
||||
for c in &mut self.content {
|
||||
c.reset();
|
||||
for cell in &mut self.content {
|
||||
cell.reset();
|
||||
}
|
||||
}
|
||||
|
||||
/// Merge an other buffer into this one
|
||||
pub fn merge(&mut self, other: &Self) {
|
||||
let area = self.area.union(other.area);
|
||||
self.content.resize(area.area() as usize, Cell::default());
|
||||
self.content.resize(area.area() as usize, Cell::EMPTY);
|
||||
|
||||
// Move original content to the appropriate space
|
||||
let size = self.area.area() as usize;
|
||||
@@ -303,7 +419,7 @@ impl Buffer {
|
||||
let k = ((y - area.y) * area.width + x - area.x) as usize;
|
||||
if i != k {
|
||||
self.content[k] = self.content[i].clone();
|
||||
self.content[i] = Cell::default();
|
||||
self.content[i].reset();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -372,6 +488,60 @@ impl Buffer {
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: Into<Position>> Index<P> for Buffer {
|
||||
type Output = Cell;
|
||||
|
||||
/// Returns a reference to the [`Cell`] at the given position.
|
||||
///
|
||||
/// This method accepts any value that can be converted to [`Position`] (e.g. `(x, y)` or
|
||||
/// `Position::new(x, y)`).
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// May panic if the given position is outside the buffer's area. For a method that returns
|
||||
/// `None` instead of panicking, use [`Buffer::cell`](Self::cell).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::{prelude::*, buffer::Cell, layout::Position};
|
||||
/// let buf = Buffer::empty(Rect::new(0, 0, 10, 10));
|
||||
/// let cell = &buf[(0, 0)];
|
||||
/// let cell = &buf[Position::new(0, 0)];
|
||||
/// ```
|
||||
fn index(&self, position: P) -> &Self::Output {
|
||||
let position = position.into();
|
||||
let index = self.index_of(position.x, position.y);
|
||||
&self.content[index]
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: Into<Position>> IndexMut<P> for Buffer {
|
||||
/// Returns a mutable reference to the [`Cell`] at the given position.
|
||||
///
|
||||
/// This method accepts any value that can be converted to [`Position`] (e.g. `(x, y)` or
|
||||
/// `Position::new(x, y)`).
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// May panic if the given position is outside the buffer's area. For a method that returns
|
||||
/// `None` instead of panicking, use [`Buffer::cell_mut`](Self::cell_mut).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::{prelude::*, buffer::Cell, layout::Position};
|
||||
/// 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");
|
||||
/// ```
|
||||
fn index_mut(&mut self, position: P) -> &mut Self::Output {
|
||||
let position = position.into();
|
||||
let index = self.index_of(position.x, position.y);
|
||||
&mut self.content[index]
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Buffer {
|
||||
/// Writes a debug representation of the buffer to the given formatter.
|
||||
///
|
||||
@@ -453,12 +623,6 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
fn cell(s: &str) -> Cell {
|
||||
let mut cell = Cell::default();
|
||||
cell.set_symbol(s);
|
||||
cell
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn debug_empty_buffer() {
|
||||
let buffer = Buffer::empty(Rect::ZERO);
|
||||
@@ -559,17 +723,90 @@ mod tests {
|
||||
let buf = Buffer::empty(rect);
|
||||
|
||||
// There are a total of 100 cells; zero-indexed means that 100 would be the 101st cell.
|
||||
buf.pos_of(100);
|
||||
let _ = buf.pos_of(100);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::left(9, 10)]
|
||||
#[case::top(10, 9)]
|
||||
#[case::right(20, 10)]
|
||||
#[case::bottom(10, 20)]
|
||||
#[should_panic(
|
||||
expected = "index outside of buffer: the area is Rect { x: 10, y: 10, width: 10, height: 10 } but index is"
|
||||
)]
|
||||
fn index_of_panics_on_out_of_bounds(#[case] x: u16, #[case] y: u16) {
|
||||
let _ = Buffer::empty(Rect::new(10, 10, 10, 10)).index_of(x, y);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "outside the buffer")]
|
||||
fn index_of_panics_on_out_of_bounds() {
|
||||
let rect = Rect::new(0, 0, 10, 10);
|
||||
let buf = Buffer::empty(rect);
|
||||
fn test_cell() {
|
||||
let buf = Buffer::with_lines(["Hello", "World"]);
|
||||
|
||||
// width is 10; zero-indexed means that 10 would be the 11th cell.
|
||||
buf.index_of(10, 0);
|
||||
let mut expected = Cell::default();
|
||||
expected.set_symbol("H");
|
||||
|
||||
assert_eq!(buf.cell((0, 0)), Some(&expected));
|
||||
assert_eq!(buf.cell((10, 10)), None);
|
||||
assert_eq!(buf.cell(Position::new(0, 0)), Some(&expected));
|
||||
assert_eq!(buf.cell(Position::new(10, 10)), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cell_mut() {
|
||||
let mut buf = Buffer::with_lines(["Hello", "World"]);
|
||||
|
||||
let mut expected = Cell::default();
|
||||
expected.set_symbol("H");
|
||||
|
||||
assert_eq!(buf.cell_mut((0, 0)), Some(&mut expected));
|
||||
assert_eq!(buf.cell_mut((10, 10)), None);
|
||||
assert_eq!(buf.cell_mut(Position::new(0, 0)), Some(&mut expected));
|
||||
assert_eq!(buf.cell_mut(Position::new(10, 10)), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn index() {
|
||||
let buf = Buffer::with_lines(["Hello", "World"]);
|
||||
|
||||
let mut expected = Cell::default();
|
||||
expected.set_symbol("H");
|
||||
|
||||
assert_eq!(buf[(0, 0)], expected);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::left(9, 10)]
|
||||
#[case::top(10, 9)]
|
||||
#[case::right(20, 10)]
|
||||
#[case::bottom(10, 20)]
|
||||
#[should_panic(
|
||||
expected = "index outside of buffer: the area is Rect { x: 10, y: 10, width: 10, height: 10 } but index is"
|
||||
)]
|
||||
fn index_out_of_bounds_panics(#[case] x: u16, #[case] y: u16) {
|
||||
let rect = Rect::new(10, 10, 10, 10);
|
||||
let buf = Buffer::empty(rect);
|
||||
let _ = buf[(x, y)];
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn index_mut() {
|
||||
let mut buf = Buffer::with_lines(["Cat", "Dog"]);
|
||||
buf[(0, 0)].set_symbol("B");
|
||||
buf[Position::new(0, 1)].set_symbol("L");
|
||||
assert_eq!(buf, Buffer::with_lines(["Bat", "Log"]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::left(9, 10)]
|
||||
#[case::top(10, 9)]
|
||||
#[case::right(20, 10)]
|
||||
#[case::bottom(10, 20)]
|
||||
#[should_panic(
|
||||
expected = "index outside of buffer: the area is Rect { x: 10, y: 10, width: 10, height: 10 } but index is"
|
||||
)]
|
||||
fn index_mut_out_of_bounds_panics(#[case] x: u16, #[case] y: u16) {
|
||||
let mut buf = Buffer::empty(Rect::new(10, 10, 10, 10));
|
||||
buf[(x, y)].set_symbol("A");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -615,16 +852,18 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn set_string_zero_width() {
|
||||
assert_eq!("\u{200B}".width(), 0);
|
||||
|
||||
let area = Rect::new(0, 0, 1, 1);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
|
||||
// Leading grapheme with zero width
|
||||
let s = "\u{1}a";
|
||||
let s = "\u{200B}a";
|
||||
buffer.set_stringn(0, 0, s, 1, Style::default());
|
||||
assert_eq!(buffer, Buffer::with_lines(["a"]));
|
||||
|
||||
// Trailing grapheme with zero with
|
||||
let s = "a\u{1}";
|
||||
let s = "a\u{200B}";
|
||||
buffer.set_stringn(0, 0, s, 1, Style::default());
|
||||
assert_eq!(buffer, Buffer::with_lines(["a"]));
|
||||
}
|
||||
@@ -749,14 +988,14 @@ mod tests {
|
||||
let prev = Buffer::empty(area);
|
||||
let next = Buffer::empty(area);
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(diff, vec![]);
|
||||
assert_eq!(diff, []);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_empty_filled() {
|
||||
let area = Rect::new(0, 0, 40, 40);
|
||||
let prev = Buffer::empty(area);
|
||||
let next = Buffer::filled(area, Cell::default().set_symbol("a"));
|
||||
let next = Buffer::filled(area, Cell::new("a"));
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(diff.len(), 40 * 40);
|
||||
}
|
||||
@@ -764,10 +1003,10 @@ mod tests {
|
||||
#[test]
|
||||
fn diff_filled_filled() {
|
||||
let area = Rect::new(0, 0, 40, 40);
|
||||
let prev = Buffer::filled(area, Cell::default().set_symbol("a"));
|
||||
let next = Buffer::filled(area, Cell::default().set_symbol("a"));
|
||||
let prev = Buffer::filled(area, Cell::new("a"));
|
||||
let next = Buffer::filled(area, Cell::new("a"));
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(diff, vec![]);
|
||||
assert_eq!(diff, []);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -789,22 +1028,23 @@ mod tests {
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(
|
||||
diff,
|
||||
vec![
|
||||
(2, 1, &cell("I")),
|
||||
(3, 1, &cell("T")),
|
||||
(4, 1, &cell("L")),
|
||||
(5, 1, &cell("E")),
|
||||
[
|
||||
(2, 1, &Cell::new("I")),
|
||||
(3, 1, &Cell::new("T")),
|
||||
(4, 1, &Cell::new("L")),
|
||||
(5, 1, &Cell::new("E")),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[rustfmt::skip]
|
||||
fn diff_multi_width() {
|
||||
#[rustfmt::skip]
|
||||
let prev = Buffer::with_lines([
|
||||
"┌Title─┐ ",
|
||||
"└──────┘ ",
|
||||
]);
|
||||
#[rustfmt::skip]
|
||||
let next = Buffer::with_lines([
|
||||
"┌称号──┐ ",
|
||||
"└──────┘ ",
|
||||
@@ -812,12 +1052,12 @@ mod tests {
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(
|
||||
diff,
|
||||
vec![
|
||||
(1, 0, &cell("称")),
|
||||
[
|
||||
(1, 0, &Cell::new("称")),
|
||||
// Skipped "i"
|
||||
(3, 0, &cell("号")),
|
||||
(3, 0, &Cell::new("号")),
|
||||
// Skipped "l"
|
||||
(5, 0, &cell("─")),
|
||||
(5, 0, &Cell::new("─")),
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -830,7 +1070,11 @@ mod tests {
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(
|
||||
diff,
|
||||
vec![(1, 0, &cell("─")), (2, 0, &cell("称")), (4, 0, &cell("号")),]
|
||||
[
|
||||
(1, 0, &Cell::new("─")),
|
||||
(2, 0, &Cell::new("称")),
|
||||
(4, 0, &Cell::new("号")),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -843,59 +1087,25 @@ mod tests {
|
||||
}
|
||||
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(diff, vec![(0, 0, &cell("4"))],);
|
||||
assert_eq!(diff, [(0, 0, &Cell::new("4"))],);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge() {
|
||||
let mut one = Buffer::filled(
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("1"),
|
||||
);
|
||||
let two = Buffer::filled(
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 2,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("2"),
|
||||
);
|
||||
#[rstest]
|
||||
#[case(Rect::new(0, 0, 2, 2), Rect::new(0, 2, 2, 2), ["11", "11", "22", "22"])]
|
||||
#[case(Rect::new(2, 2, 2, 2), Rect::new(0, 0, 2, 2), ["22 ", "22 ", " 11", " 11"])]
|
||||
fn merge<'line, Lines>(#[case] one: Rect, #[case] two: Rect, #[case] expected: Lines)
|
||||
where
|
||||
Lines: IntoIterator,
|
||||
Lines::Item: Into<Line<'line>>,
|
||||
{
|
||||
let mut one = Buffer::filled(one, Cell::new("1"));
|
||||
let two = Buffer::filled(two, Cell::new("2"));
|
||||
one.merge(&two);
|
||||
assert_eq!(one, Buffer::with_lines(["11", "11", "22", "22"]));
|
||||
assert_eq!(one, Buffer::with_lines(expected));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge2() {
|
||||
let mut one = Buffer::filled(
|
||||
Rect {
|
||||
x: 2,
|
||||
y: 2,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("1"),
|
||||
);
|
||||
let two = Buffer::filled(
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("2"),
|
||||
);
|
||||
one.merge(&two);
|
||||
assert_eq!(one, Buffer::with_lines(["22 ", "22 ", " 11", " 11"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge3() {
|
||||
fn merge_with_offset() {
|
||||
let mut one = Buffer::filled(
|
||||
Rect {
|
||||
x: 3,
|
||||
@@ -903,7 +1113,7 @@ mod tests {
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("1"),
|
||||
Cell::new("1"),
|
||||
);
|
||||
let two = Buffer::filled(
|
||||
Rect {
|
||||
@@ -912,67 +1122,48 @@ mod tests {
|
||||
width: 3,
|
||||
height: 4,
|
||||
},
|
||||
Cell::default().set_symbol("2"),
|
||||
Cell::new("2"),
|
||||
);
|
||||
one.merge(&two);
|
||||
let mut merged = Buffer::with_lines(["222 ", "222 ", "2221", "2221"]);
|
||||
merged.area = Rect {
|
||||
let mut expected = Buffer::with_lines(["222 ", "222 ", "2221", "2221"]);
|
||||
expected.area = Rect {
|
||||
x: 1,
|
||||
y: 1,
|
||||
width: 4,
|
||||
height: 4,
|
||||
};
|
||||
assert_eq!(one, merged);
|
||||
assert_eq!(one, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_skip() {
|
||||
let mut one = Buffer::filled(
|
||||
Rect {
|
||||
#[rstest]
|
||||
#[case(false, true, [false, false, true, true, true, true])]
|
||||
#[case(true, false, [true, true, false, false, false, false])]
|
||||
fn merge_skip(#[case] skip_one: bool, #[case] skip_two: bool, #[case] expected: [bool; 6]) {
|
||||
let mut one = {
|
||||
let area = Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("1"),
|
||||
);
|
||||
let two = Buffer::filled(
|
||||
Rect {
|
||||
};
|
||||
let mut cell = Cell::new("1");
|
||||
cell.skip = skip_one;
|
||||
Buffer::filled(area, cell)
|
||||
};
|
||||
let two = {
|
||||
let area = Rect {
|
||||
x: 0,
|
||||
y: 1,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("2").set_skip(true),
|
||||
);
|
||||
};
|
||||
let mut cell = Cell::new("2");
|
||||
cell.skip = skip_two;
|
||||
Buffer::filled(area, cell)
|
||||
};
|
||||
one.merge(&two);
|
||||
let skipped: Vec<bool> = one.content().iter().map(|c| c.skip).collect();
|
||||
assert_eq!(skipped, vec![false, false, true, true, true, true]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_skip2() {
|
||||
let mut one = Buffer::filled(
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("1").set_skip(true),
|
||||
);
|
||||
let two = Buffer::filled(
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 1,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("2"),
|
||||
);
|
||||
one.merge(&two);
|
||||
let skipped: Vec<bool> = one.content().iter().map(|c| c.skip).collect();
|
||||
assert_eq!(skipped, vec![true, true, false, false, false, false]);
|
||||
let skipped = one.content().iter().map(|c| c.skip).collect::<Vec<_>>();
|
||||
assert_eq!(skipped, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -983,4 +1174,74 @@ mod tests {
|
||||
buf.set_string(0, 1, "bar", Style::new().blue());
|
||||
assert_eq!(buf, Buffer::with_lines(["foo".red(), "bar".blue()]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn control_sequence_rendered_full() {
|
||||
let text = "I \x1b[0;36mwas\x1b[0m here!";
|
||||
|
||||
let mut buffer = Buffer::filled(Rect::new(0, 0, 25, 3), Cell::new("x"));
|
||||
buffer.set_string(1, 1, text, Style::new());
|
||||
|
||||
let expected = Buffer::with_lines([
|
||||
"xxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||
"xI [0;36mwas[0m here!xxxx",
|
||||
"xxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||
]);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn control_sequence_rendered_partially() {
|
||||
let text = "I \x1b[0;36mwas\x1b[0m here!";
|
||||
|
||||
let mut buffer = Buffer::filled(Rect::new(0, 0, 11, 3), Cell::new("x"));
|
||||
buffer.set_string(1, 1, text, Style::new());
|
||||
|
||||
#[rustfmt::skip]
|
||||
let expected = Buffer::with_lines([
|
||||
"xxxxxxxxxxx",
|
||||
"xI [0;36mwa",
|
||||
"xxxxxxxxxxx",
|
||||
]);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
/// Emojis normally contain various characters which should stay part of the Emoji.
|
||||
/// This should work fine by utilizing unicode_segmentation but a testcase is probably helpful
|
||||
/// due to the nature of never perfect Unicode implementations and all of its quirks.
|
||||
#[rstest]
|
||||
// Shrug without gender or skintone. Has a width of 2 like all emojis have.
|
||||
#[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")]
|
||||
// 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")]
|
||||
fn renders_emoji(#[case] input: &str, #[case] expected: &str) {
|
||||
use unicode_width::UnicodeWidthChar;
|
||||
|
||||
dbg!(input);
|
||||
dbg!(input.len());
|
||||
dbg!(input
|
||||
.graphemes(true)
|
||||
.map(|symbol| (symbol, symbol.escape_unicode().to_string(), symbol.width()))
|
||||
.collect::<Vec<_>>());
|
||||
dbg!(input
|
||||
.chars()
|
||||
.map(|char| (
|
||||
char,
|
||||
char.escape_unicode().to_string(),
|
||||
char.width(),
|
||||
char.is_control()
|
||||
))
|
||||
.collect::<Vec<_>>());
|
||||
|
||||
let mut buffer = Buffer::filled(Rect::new(0, 0, 7, 1), Cell::new("x"));
|
||||
buffer.set_string(0, 0, input, Style::new());
|
||||
|
||||
let expected = Buffer::with_lines([expected]);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
}
|
||||
297
ratatui-core/src/buffer/cell.rs
Normal file
297
ratatui-core/src/buffer/cell.rs
Normal file
@@ -0,0 +1,297 @@
|
||||
use compact_str::CompactString;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
/// A buffer cell
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Cell {
|
||||
/// The string to be drawn in the cell.
|
||||
///
|
||||
/// This accepts unicode grapheme clusters which might take up more than one 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/ratatui/pull/601> for more information.
|
||||
symbol: CompactString,
|
||||
|
||||
/// The foreground color of the cell.
|
||||
pub fg: Color,
|
||||
|
||||
/// The background color of the cell.
|
||||
pub bg: Color,
|
||||
|
||||
/// The underline color of the cell.
|
||||
#[cfg(feature = "underline-color")]
|
||||
pub underline_color: Color,
|
||||
|
||||
/// The modifier of the cell.
|
||||
pub modifier: Modifier,
|
||||
|
||||
/// Whether the cell should be skipped when copying (diffing) the buffer to the screen.
|
||||
pub skip: bool,
|
||||
}
|
||||
|
||||
impl Cell {
|
||||
/// An empty `Cell`
|
||||
pub const EMPTY: Self = Self::new(" ");
|
||||
|
||||
/// Creates a new `Cell` with the given symbol.
|
||||
///
|
||||
/// This works at compile time and puts the symbol onto the stack. Fails to build when the
|
||||
/// symbol doesnt fit onto the stack and requires to be placed on the heap. Use
|
||||
/// `Self::default().set_symbol()` in that case. See [`CompactString::const_new`] for more
|
||||
/// details on this.
|
||||
pub const fn new(symbol: &'static str) -> Self {
|
||||
Self {
|
||||
symbol: CompactString::const_new(symbol),
|
||||
fg: Color::Reset,
|
||||
bg: Color::Reset,
|
||||
#[cfg(feature = "underline-color")]
|
||||
underline_color: Color::Reset,
|
||||
modifier: Modifier::empty(),
|
||||
skip: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the symbol of the cell.
|
||||
#[must_use]
|
||||
pub fn symbol(&self) -> &str {
|
||||
self.symbol.as_str()
|
||||
}
|
||||
|
||||
/// Sets the symbol of the cell.
|
||||
pub fn set_symbol(&mut self, symbol: &str) -> &mut Self {
|
||||
self.symbol = CompactString::new(symbol);
|
||||
self
|
||||
}
|
||||
|
||||
/// Appends a symbol to the cell.
|
||||
///
|
||||
/// This is particularly useful for adding zero-width characters to the cell.
|
||||
pub(crate) fn append_symbol(&mut self, symbol: &str) -> &mut Self {
|
||||
self.symbol.push_str(symbol);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the symbol of the cell to a single character.
|
||||
pub fn set_char(&mut self, ch: char) -> &mut Self {
|
||||
let mut buf = [0; 4];
|
||||
self.symbol = CompactString::new(ch.encode_utf8(&mut buf));
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the foreground color of the cell.
|
||||
pub fn set_fg(&mut self, color: Color) -> &mut Self {
|
||||
self.fg = color;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the background color of the cell.
|
||||
pub fn set_bg(&mut self, color: Color) -> &mut Self {
|
||||
self.bg = color;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the cell.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
pub fn set_style<S: Into<Style>>(&mut self, style: S) -> &mut Self {
|
||||
let style = style.into();
|
||||
if let Some(c) = style.fg {
|
||||
self.fg = c;
|
||||
}
|
||||
if let Some(c) = style.bg {
|
||||
self.bg = c;
|
||||
}
|
||||
#[cfg(feature = "underline-color")]
|
||||
if let Some(c) = style.underline_color {
|
||||
self.underline_color = c;
|
||||
}
|
||||
self.modifier.insert(style.add_modifier);
|
||||
self.modifier.remove(style.sub_modifier);
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the style of the cell.
|
||||
#[must_use]
|
||||
pub const fn style(&self) -> Style {
|
||||
Style {
|
||||
fg: Some(self.fg),
|
||||
bg: Some(self.bg),
|
||||
#[cfg(feature = "underline-color")]
|
||||
underline_color: Some(self.underline_color),
|
||||
add_modifier: self.modifier,
|
||||
sub_modifier: Modifier::empty(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the cell to be skipped when copying (diffing) the buffer to the screen.
|
||||
///
|
||||
/// This is helpful when it is necessary to prevent the buffer from overwriting a cell that is
|
||||
/// covered by an image from some terminal graphics protocol (Sixel / iTerm / Kitty ...).
|
||||
pub fn set_skip(&mut self, skip: bool) -> &mut Self {
|
||||
self.skip = skip;
|
||||
self
|
||||
}
|
||||
|
||||
/// Resets the cell to the empty state.
|
||||
pub fn reset(&mut self) {
|
||||
self.symbol = CompactString::const_new(" ");
|
||||
self.fg = Color::Reset;
|
||||
self.bg = Color::Reset;
|
||||
#[cfg(feature = "underline-color")]
|
||||
{
|
||||
self.underline_color = Color::Reset;
|
||||
}
|
||||
self.modifier = Modifier::empty();
|
||||
self.skip = false;
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Cell {
|
||||
fn default() -> Self {
|
||||
Self::EMPTY
|
||||
}
|
||||
}
|
||||
|
||||
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::*;
|
||||
|
||||
#[test]
|
||||
fn new() {
|
||||
let cell = Cell::new("あ");
|
||||
assert_eq!(
|
||||
cell,
|
||||
Cell {
|
||||
symbol: CompactString::const_new("あ"),
|
||||
fg: Color::Reset,
|
||||
bg: Color::Reset,
|
||||
#[cfg(feature = "underline-color")]
|
||||
underline_color: Color::Reset,
|
||||
modifier: Modifier::empty(),
|
||||
skip: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
let cell = Cell::EMPTY;
|
||||
assert_eq!(cell.symbol(), " ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_symbol() {
|
||||
let mut cell = Cell::EMPTY;
|
||||
cell.set_symbol("あ"); // Multi-byte character
|
||||
assert_eq!(cell.symbol(), "あ");
|
||||
cell.set_symbol("👨👩👧👦"); // Multiple code units combined with ZWJ
|
||||
assert_eq!(cell.symbol(), "👨👩👧👦");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_symbol() {
|
||||
let mut cell = Cell::EMPTY;
|
||||
cell.set_symbol("あ"); // Multi-byte character
|
||||
cell.append_symbol("\u{200B}"); // zero-width space
|
||||
assert_eq!(cell.symbol(), "あ\u{200B}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_char() {
|
||||
let mut cell = Cell::EMPTY;
|
||||
cell.set_char('あ'); // Multi-byte character
|
||||
assert_eq!(cell.symbol(), "あ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_fg() {
|
||||
let mut cell = Cell::EMPTY;
|
||||
cell.set_fg(Color::Red);
|
||||
assert_eq!(cell.fg, Color::Red);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_bg() {
|
||||
let mut cell = Cell::EMPTY;
|
||||
cell.set_bg(Color::Red);
|
||||
assert_eq!(cell.bg, Color::Red);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_style() {
|
||||
let mut cell = Cell::EMPTY;
|
||||
cell.set_style(Style::new().fg(Color::Red).bg(Color::Blue));
|
||||
assert_eq!(cell.fg, Color::Red);
|
||||
assert_eq!(cell.bg, Color::Blue);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_skip() {
|
||||
let mut cell = Cell::EMPTY;
|
||||
cell.set_skip(true);
|
||||
assert!(cell.skip);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset() {
|
||||
let mut cell = Cell::EMPTY;
|
||||
cell.set_symbol("あ");
|
||||
cell.set_fg(Color::Red);
|
||||
cell.set_bg(Color::Blue);
|
||||
cell.set_skip(true);
|
||||
cell.reset();
|
||||
assert_eq!(cell.symbol(), " ");
|
||||
assert_eq!(cell.fg, Color::Reset);
|
||||
assert_eq!(cell.bg, Color::Reset);
|
||||
assert!(!cell.skip);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn style() {
|
||||
let cell = Cell::EMPTY;
|
||||
assert_eq!(
|
||||
cell.style(),
|
||||
Style {
|
||||
fg: Some(Color::Reset),
|
||||
bg: Some(Color::Reset),
|
||||
#[cfg(feature = "underline-color")]
|
||||
underline_color: Some(Color::Reset),
|
||||
add_modifier: Modifier::empty(),
|
||||
sub_modifier: Modifier::empty(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default() {
|
||||
let cell = Cell::default();
|
||||
assert_eq!(cell.symbol(), " ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cell_eq() {
|
||||
let cell1 = Cell::new("あ");
|
||||
let cell2 = Cell::new("あ");
|
||||
assert_eq!(cell1, cell2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cell_ne() {
|
||||
let cell1 = Cell::new("あ");
|
||||
let cell2 = Cell::new("い");
|
||||
assert_ne!(cell1, cell2);
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,8 @@
|
||||
|
||||
mod alignment;
|
||||
mod constraint;
|
||||
mod corner;
|
||||
mod direction;
|
||||
mod flex;
|
||||
#[allow(clippy::module_inception)]
|
||||
mod layout;
|
||||
mod margin;
|
||||
mod position;
|
||||
@@ -14,7 +12,6 @@ mod size;
|
||||
|
||||
pub use alignment::Alignment;
|
||||
pub use constraint::Constraint;
|
||||
pub use corner::Corner;
|
||||
pub use direction::Direction;
|
||||
pub use flex::Flex;
|
||||
pub use layout::Layout;
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::fmt;
|
||||
|
||||
use itertools::Itertools;
|
||||
use strum::EnumIs;
|
||||
|
||||
/// A constraint that defines the size of a layout element.
|
||||
@@ -117,8 +116,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
|
||||
///
|
||||
@@ -229,7 +232,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
|
||||
@@ -246,10 +249,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
|
||||
@@ -266,7 +266,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
|
||||
@@ -283,7 +283,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
|
||||
@@ -300,7 +300,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
|
||||
@@ -310,17 +310,14 @@ impl Constraint {
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// # let area = Rect::default();
|
||||
/// let constraints = Constraint::from_mins([1, 2, 3]);
|
||||
/// let constraints = Constraint::from_fills([1, 2, 3]);
|
||||
/// let layout = Layout::default().constraints(constraints).split(area);
|
||||
/// ```
|
||||
pub fn from_fills<T>(proportional_factors: T) -> Vec<Self>
|
||||
where
|
||||
T: IntoIterator<Item = u16>,
|
||||
{
|
||||
proportional_factors
|
||||
.into_iter()
|
||||
.map(Self::Fill)
|
||||
.collect_vec()
|
||||
proportional_factors.into_iter().map(Self::Fill).collect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{cell::RefCell, collections::HashMap, iter, num::NonZeroUsize, rc::Rc, sync::OnceLock};
|
||||
use std::{cell::RefCell, collections::HashMap, iter, num::NonZeroUsize, rc::Rc};
|
||||
|
||||
use cassowary::{
|
||||
strength::REQUIRED,
|
||||
@@ -37,7 +37,9 @@ type Cache = LruCache<(Rect, Layout), (Segments, Spacers)>;
|
||||
const FLOAT_PRECISION_MULTIPLIER: f64 = 100.0;
|
||||
|
||||
thread_local! {
|
||||
static LAYOUT_CACHE: OnceLock<RefCell<Cache>> = const { OnceLock::new() };
|
||||
static LAYOUT_CACHE: RefCell<Cache> = RefCell::new(Cache::new(
|
||||
NonZeroUsize::new(Layout::DEFAULT_CACHE_SIZE).unwrap(),
|
||||
));
|
||||
}
|
||||
|
||||
/// A layout is a set of constraints that can be applied to a given area to split it into smaller
|
||||
@@ -105,7 +107,7 @@ thread_local! {
|
||||
/// example](https://camo.githubusercontent.com/77d22f3313b782a81e5e033ef82814bb48d786d2598699c27f8e757ccee62021/68747470733a2f2f7668732e636861726d2e73682f7668732d315a4e6f4e4c4e6c4c746b4a58706767396e435635652e676966)
|
||||
///
|
||||
/// [`cassowary-rs`]: https://crates.io/crates/cassowary
|
||||
/// [Examples]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
/// [Examples]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Layout {
|
||||
direction: Direction,
|
||||
@@ -209,22 +211,9 @@ impl Layout {
|
||||
/// that subsequent calls with the same parameters are faster. The cache is a `LruCache`, and
|
||||
/// grows until `cache_size` is reached.
|
||||
///
|
||||
/// Returns true if the cell's value was set by this call.
|
||||
/// Returns false if the cell's value was not set by this call, this means that another thread
|
||||
/// has set this value or that the cache size is already initialized.
|
||||
///
|
||||
/// Note that a custom cache size will be set only if this function:
|
||||
/// * is called before [`Layout::split()`] otherwise, the cache size is
|
||||
/// [`Self::DEFAULT_CACHE_SIZE`].
|
||||
/// * is called for the first time, subsequent calls do not modify the cache size.
|
||||
pub fn init_cache(cache_size: usize) -> bool {
|
||||
LAYOUT_CACHE
|
||||
.with(|c| {
|
||||
c.set(RefCell::new(LruCache::new(
|
||||
NonZeroUsize::new(cache_size).unwrap(),
|
||||
)))
|
||||
})
|
||||
.is_ok()
|
||||
/// By default, the cache size is [`Self::DEFAULT_CACHE_SIZE`].
|
||||
pub fn init_cache(cache_size: NonZeroUsize) {
|
||||
LAYOUT_CACHE.with_borrow_mut(|c| c.resize(cache_size));
|
||||
}
|
||||
|
||||
/// Set the direction of the layout.
|
||||
@@ -439,7 +428,7 @@ impl Layout {
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// # fn render(frame: &mut Frame) {
|
||||
/// let area = frame.size();
|
||||
/// let area = frame.area();
|
||||
/// let layout = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
|
||||
/// let [top, main] = layout.areas(area);
|
||||
///
|
||||
@@ -471,7 +460,7 @@ impl Layout {
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// # fn render(frame: &mut Frame) {
|
||||
/// let area = frame.size();
|
||||
/// let area = frame.area();
|
||||
/// let layout = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
|
||||
/// let [top, main] = layout.areas(area);
|
||||
/// let [before, inbetween, after] = layout.spacers(area);
|
||||
@@ -571,17 +560,10 @@ impl Layout {
|
||||
/// );
|
||||
/// ```
|
||||
pub fn split_with_spacers(&self, area: Rect) -> (Segments, Spacers) {
|
||||
LAYOUT_CACHE.with(|c| {
|
||||
c.get_or_init(|| {
|
||||
RefCell::new(LruCache::new(
|
||||
NonZeroUsize::new(Self::DEFAULT_CACHE_SIZE).unwrap(),
|
||||
))
|
||||
})
|
||||
.borrow_mut()
|
||||
.get_or_insert((area, self.clone()), || {
|
||||
self.try_split(area).expect("failed to split")
|
||||
})
|
||||
.clone()
|
||||
LAYOUT_CACHE.with_borrow_mut(|c| {
|
||||
let key = (area, self.clone());
|
||||
c.get_or_insert(key, || self.try_split(area).expect("failed to split"))
|
||||
.clone()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -608,7 +590,7 @@ impl Layout {
|
||||
// This is equivalent to storing the solver in `Layout` and calling `solver.reset()` here.
|
||||
let mut solver = Solver::new();
|
||||
|
||||
let inner_area = area.inner(&self.margin);
|
||||
let inner_area = area.inner(self.margin);
|
||||
let (area_start, area_end) = match self.direction {
|
||||
Direction::Horizontal => (
|
||||
f64::from(inner_area.x) * FLOAT_PRECISION_MULTIPLIER,
|
||||
@@ -1099,37 +1081,14 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_cache_size() {
|
||||
assert!(Layout::init_cache(10));
|
||||
assert!(!Layout::init_cache(15));
|
||||
LAYOUT_CACHE.with(|c| {
|
||||
assert_eq!(c.get().unwrap().borrow().cap().get(), 10);
|
||||
fn cache_size() {
|
||||
LAYOUT_CACHE.with_borrow(|c| {
|
||||
assert_eq!(c.cap().get(), Layout::DEFAULT_CACHE_SIZE);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_cache_size() {
|
||||
let target = Rect {
|
||||
x: 2,
|
||||
y: 2,
|
||||
width: 10,
|
||||
height: 10,
|
||||
};
|
||||
|
||||
Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Percentage(10),
|
||||
Constraint::Max(5),
|
||||
Constraint::Min(1),
|
||||
])
|
||||
.split(target);
|
||||
assert!(!Layout::init_cache(15));
|
||||
LAYOUT_CACHE.with(|c| {
|
||||
assert_eq!(
|
||||
c.get().unwrap().borrow().cap().get(),
|
||||
Layout::DEFAULT_CACHE_SIZE
|
||||
);
|
||||
Layout::init_cache(NonZeroUsize::new(10).unwrap());
|
||||
LAYOUT_CACHE.with_borrow(|c| {
|
||||
assert_eq!(c.cap().get(), 10);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1336,7 +1295,7 @@ mod tests {
|
||||
use crate::{
|
||||
layout::flex::Flex,
|
||||
prelude::{Constraint::*, *},
|
||||
widgets::Paragraph,
|
||||
// widgets::Paragraph, // TODO
|
||||
};
|
||||
|
||||
/// Test that the given constraints applied to the given area result in the expected layout.
|
||||
@@ -1356,9 +1315,9 @@ mod tests {
|
||||
.flex(flex)
|
||||
.split(area);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
for (i, c) in ('a'..='z').take(constraints.len()).enumerate() {
|
||||
for (c, &area) in ('a'..='z').take(constraints.len()).zip(layout.iter()) {
|
||||
let s = c.to_string().repeat(area.width as usize);
|
||||
Paragraph::new(s).render(layout[i], &mut buffer);
|
||||
// Paragraph::new(s).render(area, &mut buffer); // TODO
|
||||
}
|
||||
assert_eq!(buffer, Buffer::with_lines([expected]));
|
||||
}
|
||||
@@ -1881,14 +1840,11 @@ mod tests {
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(10),
|
||||
Constraint::Max(5),
|
||||
Constraint::Min(1),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.constraints([
|
||||
Constraint::Percentage(10),
|
||||
Constraint::Max(5),
|
||||
Constraint::Min(1),
|
||||
])
|
||||
.split(target);
|
||||
|
||||
assert_eq!(target.height, chunks.iter().map(|r| r.height).sum::<u16>());
|
||||
@@ -1932,7 +1888,7 @@ mod tests {
|
||||
);
|
||||
|
||||
// minimal bug from
|
||||
// https://github.com/ratatui-org/ratatui/pull/404#issuecomment-1681850644
|
||||
// https://github.com/ratatui/ratatui/pull/404#issuecomment-1681850644
|
||||
// TODO: check if this bug is now resolved?
|
||||
let layout = Layout::default()
|
||||
.constraints([Min(1), Length(0), Min(1)])
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Margin {
|
||||
pub horizontal: u16,
|
||||
pub vertical: u16,
|
||||
@@ -1,4 +1,6 @@
|
||||
#![warn(missing_docs)]
|
||||
use std::fmt;
|
||||
|
||||
use crate::layout::Rect;
|
||||
|
||||
/// Position in the terminal
|
||||
@@ -22,6 +24,7 @@ use crate::layout::Rect;
|
||||
/// let (x, y) = position.into();
|
||||
/// ```
|
||||
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Position {
|
||||
/// The x coordinate of the position
|
||||
///
|
||||
@@ -37,6 +40,9 @@ pub struct Position {
|
||||
}
|
||||
|
||||
impl Position {
|
||||
/// Position at the origin, the top left edge at 0,0
|
||||
pub const ORIGIN: Self = Self { x: 0, y: 0 };
|
||||
|
||||
/// Create a new position
|
||||
pub const fn new(x: u16, y: u16) -> Self {
|
||||
Self { x, y }
|
||||
@@ -61,6 +67,12 @@ impl From<Rect> for Position {
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Position {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "({}, {})", self.x, self.y)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -94,4 +106,10 @@ mod tests {
|
||||
assert_eq!(position.x, 1);
|
||||
assert_eq!(position.y, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_string() {
|
||||
let position = Position::new(1, 2);
|
||||
assert_eq!(position.to_string(), "(1, 2)");
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,8 @@ pub struct Rect {
|
||||
/// Positive numbers move to the right/bottom and negative to the left/top.
|
||||
///
|
||||
/// See [`Rect::offset`]
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Offset {
|
||||
/// How much to move on the X axis
|
||||
pub x: i32,
|
||||
@@ -58,7 +59,7 @@ impl Rect {
|
||||
/// 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_value();
|
||||
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);
|
||||
@@ -119,9 +120,8 @@ impl Rect {
|
||||
/// Returns a new `Rect` inside the current one, with the given margin on each side.
|
||||
///
|
||||
/// If the margin is larger than the `Rect`, the returned `Rect` will have no area.
|
||||
#[allow(clippy::trivially_copy_pass_by_ref)] // See PR #1008
|
||||
#[must_use = "method returns the modified value"]
|
||||
pub const fn inner(self, margin: &Margin) -> Self {
|
||||
pub const fn inner(self, margin: Margin) -> Self {
|
||||
let doubled_margin_horizontal = margin.horizontal.saturating_mul(2);
|
||||
let doubled_margin_vertical = margin.vertical.saturating_mul(2);
|
||||
|
||||
@@ -236,7 +236,7 @@ impl Rect {
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// # fn render(frame: &mut Frame) {
|
||||
/// let area = frame.size();
|
||||
/// let area = frame.area();
|
||||
/// let rect = Rect::new(0, 0, 100, 100).clamp(area);
|
||||
/// # }
|
||||
/// ```
|
||||
@@ -291,7 +291,7 @@ impl Rect {
|
||||
/// # use ratatui::prelude::*;
|
||||
/// fn render(area: Rect, buf: &mut Buffer) {
|
||||
/// for position in area.positions() {
|
||||
/// buf.get_mut(position.x, position.y).set_symbol("x");
|
||||
/// buf[(position.x, position.y)].set_symbol("x");
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
@@ -406,7 +406,7 @@ mod tests {
|
||||
#[test]
|
||||
fn inner() {
|
||||
assert_eq!(
|
||||
Rect::new(1, 2, 3, 4).inner(&Margin::new(1, 2)),
|
||||
Rect::new(1, 2, 3, 4).inner(Margin::new(1, 2)),
|
||||
Rect::new(2, 4, 1, 0)
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
use self::layout::Position;
|
||||
use crate::prelude::*;
|
||||
|
||||
/// An iterator over rows within a `Rect`.
|
||||
@@ -1,4 +1,6 @@
|
||||
#![warn(missing_docs)]
|
||||
use std::fmt;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
/// A simple size struct
|
||||
@@ -6,6 +8,7 @@ use crate::prelude::*;
|
||||
/// The width and height are stored as `u16` values and represent the number of columns and rows
|
||||
/// respectively.
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Size {
|
||||
/// The width in columns
|
||||
pub width: u16,
|
||||
@@ -14,6 +17,9 @@ pub struct Size {
|
||||
}
|
||||
|
||||
impl Size {
|
||||
/// A zero sized Size
|
||||
pub const ZERO: Self = Self::new(0, 0);
|
||||
|
||||
/// Create a new `Size` struct
|
||||
pub const fn new(width: u16, height: u16) -> Self {
|
||||
Self { width, height }
|
||||
@@ -32,6 +38,12 @@ impl From<Rect> for Size {
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Size {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}x{}", self.width, self.height)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -56,4 +68,9 @@ mod tests {
|
||||
assert_eq!(size.width, 10);
|
||||
assert_eq!(size.height, 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display() {
|
||||
assert_eq!(Size::new(10, 20).to_string(), "10x20");
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
//! 
|
||||
//! 
|
||||
//!
|
||||
//! <div align="center">
|
||||
//!
|
||||
//! [![Crate Badge]][Crate] [![Docs Badge]][API Docs] [![CI Badge]][CI Workflow] [![License
|
||||
//! Badge]](./LICENSE) [![Sponsors Badge]][GitHub Sponsors]<br>
|
||||
//! [![Codecov Badge]][Codecov] [![Deps.rs Badge]][Deps.rs] [![Discord Badge]][Discord Server]
|
||||
//! [![Matrix Badge]][Matrix]<br>
|
||||
//! [![Crate Badge]][Crate] [![Docs Badge]][API Docs] [![CI Badge]][CI Workflow] [![Deps.rs
|
||||
//! Badge]][Deps.rs]<br> [![Codecov Badge]][Codecov] [![License Badge]](./LICENSE) [![Sponsors
|
||||
//! Badge]][GitHub Sponsors]<br> [![Discord Badge]][Discord Server] [![Matrix Badge]][Matrix]
|
||||
//! [![Forum Badge]][Forum]<br>
|
||||
//!
|
||||
//! [Ratatui Website] · [API Docs] · [Examples] · [Changelog] · [Breaking Changes]<br>
|
||||
//! [Contributing] · [Report a bug] · [Request a Feature] · [Create a Pull Request]
|
||||
@@ -20,10 +20,10 @@
|
||||
//!
|
||||
//! ## Installation
|
||||
//!
|
||||
//! Add `ratatui` and `crossterm` as dependencies to your cargo.toml:
|
||||
//! Add `ratatui` as a dependency to your cargo.toml:
|
||||
//!
|
||||
//! ```shell
|
||||
//! cargo add ratatui crossterm
|
||||
//! cargo add ratatui
|
||||
//! ```
|
||||
//!
|
||||
//! Ratatui uses [Crossterm] by default as it works on most platforms. See the [Installation]
|
||||
@@ -44,6 +44,7 @@
|
||||
//! ## Other documentation
|
||||
//!
|
||||
//! - [Ratatui Website] - explains the library's concepts and provides step-by-step tutorials
|
||||
//! - [Ratatui Forum][Forum] - a place to ask questions and discuss the library
|
||||
//! - [API Docs] - the full API documentation for the library on docs.rs.
|
||||
//! - [Examples] - a collection of examples that demonstrate how to use the library.
|
||||
//! - [Contributing] - Please read this if you are interested in contributing to the project.
|
||||
@@ -101,47 +102,28 @@
|
||||
//! ### Example
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use std::io::{self, stdout};
|
||||
//!
|
||||
//! use crossterm::{
|
||||
//! event::{self, Event, KeyCode},
|
||||
//! terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
//! ExecutableCommand,
|
||||
//! use ratatui::{
|
||||
//! crossterm::event::{self, Event, KeyCode, KeyEventKind},
|
||||
//! widgets::{Block, Paragraph},
|
||||
//! };
|
||||
//! use ratatui::{prelude::*, widgets::*};
|
||||
//!
|
||||
//! 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))? {
|
||||
//! fn main() -> std::io::Result<()> {
|
||||
//! let mut terminal = ratatui::init();
|
||||
//! loop {
|
||||
//! terminal.draw(|frame| {
|
||||
//! frame.render_widget(
|
||||
//! Paragraph::new("Hello World!").block(Block::bordered().title("Greeting")),
|
||||
//! frame.area(),
|
||||
//! );
|
||||
//! })?;
|
||||
//! if let Event::Key(key) = event::read()? {
|
||||
//! if key.kind == event::KeyEventKind::Press && key.code == KeyCode::Char('q') {
|
||||
//! return Ok(true);
|
||||
//! if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
|
||||
//! break;
|
||||
//! }
|
||||
//! }
|
||||
//! }
|
||||
//! Ok(false)
|
||||
//! }
|
||||
//!
|
||||
//! fn ui(frame: &mut Frame) {
|
||||
//! frame.render_widget(
|
||||
//! Paragraph::new("Hello World!").block(Block::bordered().title("Greeting")),
|
||||
//! frame.size(),
|
||||
//! );
|
||||
//! ratatui::restore();
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
@@ -157,34 +139,27 @@
|
||||
//! section of the [Ratatui Website] for more info.
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use ratatui::{prelude::*, widgets::*};
|
||||
//! use ratatui::{
|
||||
//! layout::{Constraint, Layout},
|
||||
//! widgets::Block,
|
||||
//! Frame,
|
||||
//! };
|
||||
//!
|
||||
//! fn ui(frame: &mut Frame) {
|
||||
//! let main_layout = Layout::new(
|
||||
//! Direction::Vertical,
|
||||
//! [
|
||||
//! Constraint::Length(1),
|
||||
//! Constraint::Min(0),
|
||||
//! Constraint::Length(1),
|
||||
//! ],
|
||||
//! )
|
||||
//! .split(frame.size());
|
||||
//! frame.render_widget(
|
||||
//! Block::new().borders(Borders::TOP).title("Title Bar"),
|
||||
//! main_layout[0],
|
||||
//! );
|
||||
//! frame.render_widget(
|
||||
//! Block::new().borders(Borders::TOP).title("Status Bar"),
|
||||
//! main_layout[2],
|
||||
//! );
|
||||
//! fn draw(frame: &mut Frame) {
|
||||
//! let [title_area, main_area, status_area] = Layout::vertical([
|
||||
//! Constraint::Length(1),
|
||||
//! Constraint::Min(0),
|
||||
//! Constraint::Length(1),
|
||||
//! ])
|
||||
//! .areas(frame.area());
|
||||
//! let [left_area, right_area] =
|
||||
//! Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
//! .areas(main_area);
|
||||
//!
|
||||
//! let inner_layout = Layout::new(
|
||||
//! Direction::Horizontal,
|
||||
//! [Constraint::Percentage(50), Constraint::Percentage(50)],
|
||||
//! )
|
||||
//! .split(main_layout[1]);
|
||||
//! frame.render_widget(Block::bordered().title("Left"), inner_layout[0]);
|
||||
//! frame.render_widget(Block::bordered().title("Right"), inner_layout[1]);
|
||||
//! frame.render_widget(Block::bordered().title("Title Bar"), title_area);
|
||||
//! frame.render_widget(Block::bordered().title("Status Bar"), status_area);
|
||||
//! frame.render_widget(Block::bordered().title("Left"), left_area);
|
||||
//! frame.render_widget(Block::bordered().title("Right"), right_area);
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
@@ -205,48 +180,41 @@
|
||||
//! [Ratatui Website] for more info.
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use ratatui::{prelude::*, widgets::*};
|
||||
//! use ratatui::{
|
||||
//! layout::{Constraint, Layout},
|
||||
//! style::{Color, Modifier, Style, Stylize},
|
||||
//! text::{Line, Span},
|
||||
//! widgets::{Block, Paragraph},
|
||||
//! Frame,
|
||||
//! };
|
||||
//!
|
||||
//! fn ui(frame: &mut Frame) {
|
||||
//! let areas = Layout::new(
|
||||
//! Direction::Vertical,
|
||||
//! [
|
||||
//! Constraint::Length(1),
|
||||
//! Constraint::Length(1),
|
||||
//! Constraint::Length(1),
|
||||
//! Constraint::Length(1),
|
||||
//! Constraint::Min(0),
|
||||
//! ],
|
||||
//! )
|
||||
//! .split(frame.size());
|
||||
//! fn draw(frame: &mut Frame) {
|
||||
//! let areas = Layout::vertical([Constraint::Length(1); 4]).split(frame.area());
|
||||
//!
|
||||
//! let span1 = Span::raw("Hello ");
|
||||
//! let span2 = Span::styled(
|
||||
//! "World",
|
||||
//! Style::new()
|
||||
//! .fg(Color::Green)
|
||||
//! .bg(Color::White)
|
||||
//! .add_modifier(Modifier::BOLD),
|
||||
//! );
|
||||
//! let span3 = "!".red().on_light_yellow().italic();
|
||||
//! let line = Line::from(vec![
|
||||
//! Span::raw("Hello "),
|
||||
//! Span::styled(
|
||||
//! "World",
|
||||
//! Style::new()
|
||||
//! .fg(Color::Green)
|
||||
//! .bg(Color::White)
|
||||
//! .add_modifier(Modifier::BOLD),
|
||||
//! ),
|
||||
//! "!".red().on_light_yellow().italic(),
|
||||
//! ]);
|
||||
//! frame.render_widget(line, areas[0]);
|
||||
//!
|
||||
//! let line = Line::from(vec![span1, span2, span3]);
|
||||
//! let text: Text = Text::from(vec![line]);
|
||||
//! // using the short-hand syntax and implicit conversions
|
||||
//! let paragraph = Paragraph::new("Hello World!".red().on_white().bold());
|
||||
//! frame.render_widget(paragraph, areas[1]);
|
||||
//!
|
||||
//! frame.render_widget(Paragraph::new(text), areas[0]);
|
||||
//! // or using the short-hand syntax and implicit conversions
|
||||
//! frame.render_widget(
|
||||
//! Paragraph::new("Hello World!".red().on_white().bold()),
|
||||
//! areas[1],
|
||||
//! );
|
||||
//! // style the whole widget instead of just the text
|
||||
//! let paragraph = Paragraph::new("Hello World!").style(Style::new().red().on_white());
|
||||
//! frame.render_widget(paragraph, areas[2]);
|
||||
//!
|
||||
//! // to style the whole widget instead of just the text
|
||||
//! frame.render_widget(
|
||||
//! Paragraph::new("Hello World!").style(Style::new().red().on_white()),
|
||||
//! areas[2],
|
||||
//! );
|
||||
//! // or using the short-hand syntax
|
||||
//! frame.render_widget(Paragraph::new("Hello World!").blue().on_yellow(), areas[3]);
|
||||
//! // use the simpler short-hand syntax
|
||||
//! let paragraph = Paragraph::new("Hello World!").blue().on_yellow();
|
||||
//! frame.render_widget(paragraph, areas[3]);
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
@@ -255,22 +223,6 @@
|
||||
//! ![docsrs-styling]
|
||||
#![cfg_attr(feature = "document-features", doc = "\n## Features")]
|
||||
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
|
||||
#![cfg_attr(
|
||||
feature = "document-features",
|
||||
doc = "[`CrossTermBackend`]: backend::CrosstermBackend"
|
||||
)]
|
||||
#![cfg_attr(
|
||||
feature = "document-features",
|
||||
doc = "[`TermionBackend`]: backend::TermionBackend"
|
||||
)]
|
||||
#![cfg_attr(
|
||||
feature = "document-features",
|
||||
doc = "[`TermwizBackend`]: backend::TermwizBackend"
|
||||
)]
|
||||
#![cfg_attr(
|
||||
feature = "document-features",
|
||||
doc = "[`calendar`]: widgets::calendar::Monthly"
|
||||
)]
|
||||
//!
|
||||
//! [Ratatui Website]: https://ratatui.rs/
|
||||
//! [Installation]: https://ratatui.rs/installation/
|
||||
@@ -282,21 +234,21 @@
|
||||
//! [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/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
|
||||
//! [docsrs-hello]: https://github.com/ratatui/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-hello.png?raw=true
|
||||
//! [docsrs-layout]: https://github.com/ratatui/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-layout.png?raw=true
|
||||
//! [docsrs-styling]: https://github.com/ratatui/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-styling.png?raw=true
|
||||
//! [`Frame`]: terminal::Frame
|
||||
//! [`render_widget`]: terminal::Frame::render_widget
|
||||
//! [`Widget`]: widgets::Widget
|
||||
@@ -315,43 +267,52 @@
|
||||
//! [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
|
||||
//! [Crate Badge]: https://img.shields.io/crates/v/ratatui?logo=rust&style=flat-square
|
||||
//! [License Badge]: https://img.shields.io/crates/l/ratatui?style=flat-square
|
||||
//! [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
|
||||
//! [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
|
||||
//! [Discord Badge]:
|
||||
//! https://img.shields.io/discord/1070692720437383208?label=discord&logo=discord&style=flat-square
|
||||
//! [GitHub Sponsors]: https://github.com/sponsors/ratatui
|
||||
//! [Crate Badge]: https://img.shields.io/crates/v/ratatui?logo=rust&style=flat-square&logoColor=E05D44&color=E05D44
|
||||
//! [License Badge]: https://img.shields.io/crates/l/ratatui?style=flat-square&color=1370D3
|
||||
//! [CI Badge]: https://img.shields.io/github/actions/workflow/status/ratatui/ratatui/ci.yml?style=flat-square&logo=github
|
||||
//! [CI Workflow]: https://github.com/ratatui/ratatui/actions/workflows/ci.yml
|
||||
//! [Codecov Badge]: https://img.shields.io/codecov/c/github/ratatui/ratatui?logo=codecov&style=flat-square&token=BAQ8SOKEST&color=C43AC3&logoColor=C43AC3
|
||||
//! [Codecov]: https://app.codecov.io/gh/ratatui/ratatui
|
||||
//! [Deps.rs Badge]: https://deps.rs/repo/github/ratatui/ratatui/status.svg?style=flat-square
|
||||
//! [Deps.rs]: https://deps.rs/repo/github/ratatui/ratatui
|
||||
//! [Discord Badge]: https://img.shields.io/discord/1070692720437383208?label=discord&logo=discord&style=flat-square&color=1370D3&logoColor=1370D3
|
||||
//! [Discord Server]: https://discord.gg/pMCEU9hNEj
|
||||
//! [Docs Badge]: https://img.shields.io/docsrs/ratatui?logo=rust&style=flat-square
|
||||
//! [Matrix Badge]:
|
||||
//! https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix
|
||||
//! [Docs Badge]: https://img.shields.io/docsrs/ratatui?logo=rust&style=flat-square&logoColor=E05D44
|
||||
//! [Matrix Badge]: https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix&color=C43AC3
|
||||
//! [Matrix]: https://matrix.to/#/#ratatui:matrix.org
|
||||
//! [Sponsors Badge]: https://img.shields.io/github/sponsors/ratatui-org?logo=github&style=flat-square
|
||||
//! [Forum Badge]: https://img.shields.io/discourse/likes?server=https%3A%2F%2Fforum.ratatui.rs&style=flat-square&logo=discourse&label=forum&color=C43AC3
|
||||
//! [Forum]: https://forum.ratatui.rs
|
||||
//! [Sponsors Badge]: https://img.shields.io/github/sponsors/ratatui?logo=github&style=flat-square&color=1370D3
|
||||
|
||||
// show the feature flags in the generated documentation
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![doc(
|
||||
html_logo_url = "https://raw.githubusercontent.com/ratatui-org/ratatui/main/assets/logo.png",
|
||||
html_favicon_url = "https://raw.githubusercontent.com/ratatui-org/ratatui/main/assets/favicon.ico"
|
||||
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"
|
||||
)]
|
||||
|
||||
/// re-export the `crossterm` crate so that users don't have to add it as a dependency
|
||||
#[cfg(feature = "crossterm")]
|
||||
pub use crossterm;
|
||||
#[cfg(feature = "crossterm")]
|
||||
pub use terminal::{
|
||||
init, init_with_options, restore, try_init, try_init_with_options, try_restore, DefaultTerminal,
|
||||
};
|
||||
pub use terminal::{CompletedFrame, Frame, Terminal, TerminalOptions, Viewport};
|
||||
/// re-export the `termion` crate so that users don't have to add it as a dependency
|
||||
#[cfg(all(not(windows), feature = "termion"))]
|
||||
pub use termion;
|
||||
/// re-export the `termwiz` crate so that users don't have to add it as a dependency
|
||||
#[cfg(feature = "termwiz")]
|
||||
pub use termwiz;
|
||||
|
||||
pub mod backend;
|
||||
pub mod buffer;
|
||||
pub mod layout;
|
||||
pub mod prelude;
|
||||
pub mod style;
|
||||
pub mod symbols;
|
||||
pub mod terminal;
|
||||
mod terminal;
|
||||
pub mod text;
|
||||
pub mod widgets;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use self::terminal::{CompletedFrame, Frame, Terminal, TerminalOptions, Viewport};
|
||||
|
||||
pub mod prelude;
|
||||
41
ratatui-core/src/prelude.rs
Normal file
41
ratatui-core/src/prelude.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
//! A prelude for conveniently writing applications using this library.
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use ratatui::prelude::*;
|
||||
//! ```
|
||||
//!
|
||||
//! Aside from the main types that are used in the library, this prelude also re-exports several
|
||||
//! modules to make it easy to qualify types that would otherwise collide. E.g.:
|
||||
//!
|
||||
//! ```rust
|
||||
//! use ratatui::{prelude::*, widgets::*};
|
||||
//!
|
||||
//! #[derive(Debug, Default, PartialEq, Eq)]
|
||||
//! struct Line;
|
||||
//!
|
||||
//! assert_eq!(Line::default(), Line);
|
||||
//! assert_eq!(text::Line::default(), ratatui::text::Line::from(vec![]));
|
||||
//! ```
|
||||
|
||||
// TODO: re-export the following modules:
|
||||
// #[cfg(feature = "crossterm")]
|
||||
// pub use crate::backend::CrosstermBackend;
|
||||
// #[cfg(all(not(windows), feature = "termion"))]
|
||||
// pub use crate::backend::TermionBackend;
|
||||
// #[cfg(feature = "termwiz")]
|
||||
// pub use crate::backend::TermwizBackend;
|
||||
pub(crate) use crate::widgets::{StatefulWidgetRef, WidgetRef};
|
||||
pub use crate::{
|
||||
backend::{self, Backend},
|
||||
buffer::{self, Buffer},
|
||||
layout::{self, Alignment, Constraint, Direction, Layout, Margin, Position, Rect, Size},
|
||||
style::{self, Color, Modifier, Style, Stylize},
|
||||
symbols::{self},
|
||||
text::{self, Line, Masked, Span, Text},
|
||||
widgets::{
|
||||
// block::BlockExt, // TODO
|
||||
StatefulWidget,
|
||||
Widget,
|
||||
},
|
||||
Frame, Terminal,
|
||||
};
|
||||
@@ -71,13 +71,15 @@
|
||||
use std::fmt;
|
||||
|
||||
use bitflags::bitflags;
|
||||
pub use color::{Color, ParseColorError};
|
||||
use stylize::ColorDebugKind;
|
||||
pub use stylize::{Styled, Stylize};
|
||||
|
||||
mod color;
|
||||
mod stylize;
|
||||
|
||||
pub use color::{Color, ParseColorError};
|
||||
pub use stylize::{Styled, Stylize};
|
||||
pub mod palette;
|
||||
#[cfg(feature = "palette")]
|
||||
mod palette_conversion;
|
||||
mod stylize;
|
||||
|
||||
bitflags! {
|
||||
/// Modifier changes the way a piece of text is displayed.
|
||||
@@ -179,7 +181,7 @@ impl fmt::Debug for Modifier {
|
||||
/// ];
|
||||
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
|
||||
/// for style in &styles {
|
||||
/// buffer.get_mut(0, 0).set_style(*style);
|
||||
/// buffer[(0, 0)].set_style(*style);
|
||||
/// }
|
||||
/// assert_eq!(
|
||||
/// Style {
|
||||
@@ -190,7 +192,7 @@ impl fmt::Debug for Modifier {
|
||||
/// add_modifier: Modifier::BOLD | Modifier::UNDERLINED,
|
||||
/// sub_modifier: Modifier::empty(),
|
||||
/// },
|
||||
/// buffer.get(0, 0).style(),
|
||||
/// buffer[(0, 0)].style(),
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
@@ -208,7 +210,7 @@ impl fmt::Debug for Modifier {
|
||||
/// ];
|
||||
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
|
||||
/// for style in &styles {
|
||||
/// buffer.get_mut(0, 0).set_style(*style);
|
||||
/// buffer[(0, 0)].set_style(*style);
|
||||
/// }
|
||||
/// assert_eq!(
|
||||
/// Style {
|
||||
@@ -219,10 +221,10 @@ impl fmt::Debug for Modifier {
|
||||
/// add_modifier: Modifier::empty(),
|
||||
/// sub_modifier: Modifier::empty(),
|
||||
/// },
|
||||
/// buffer.get(0, 0).style(),
|
||||
/// buffer[(0, 0)].style(),
|
||||
/// );
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
#[derive(Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Style {
|
||||
pub fg: Option<Color>,
|
||||
@@ -233,9 +235,52 @@ pub struct Style {
|
||||
pub sub_modifier: Modifier,
|
||||
}
|
||||
|
||||
impl Default for Style {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
/// 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()")?;
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -554,6 +599,16 @@ 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()")]
|
||||
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 = [
|
||||
@@ -600,9 +655,9 @@ mod tests {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
|
||||
|
||||
for m in mods {
|
||||
buffer.get_mut(0, 0).set_style(Style::reset());
|
||||
buffer.get_mut(0, 0).set_style(Style::new().add_modifier(m));
|
||||
let style = buffer.get(0, 0).style();
|
||||
buffer[(0, 0)].set_style(Style::reset());
|
||||
buffer[(0, 0)].set_style(Style::new().add_modifier(m));
|
||||
let style = buffer[(0, 0)].style();
|
||||
assert!(style.add_modifier.contains(m));
|
||||
assert!(!style.sub_modifier.contains(m));
|
||||
}
|
||||
@@ -652,151 +707,80 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
#[rstest]
|
||||
#[case(Style::new().black(), Color::Black)]
|
||||
#[case(Style::new().red(), Color::Red)]
|
||||
#[case(Style::new().green(), Color::Green)]
|
||||
#[case(Style::new().yellow(), Color::Yellow)]
|
||||
#[case(Style::new().blue(), Color::Blue)]
|
||||
#[case(Style::new().magenta(), Color::Magenta)]
|
||||
#[case(Style::new().cyan(), Color::Cyan)]
|
||||
#[case(Style::new().white(), Color::White)]
|
||||
#[case(Style::new().gray(), Color::Gray)]
|
||||
#[case(Style::new().dark_gray(), Color::DarkGray)]
|
||||
#[case(Style::new().light_red(), Color::LightRed)]
|
||||
#[case(Style::new().light_green(), Color::LightGreen)]
|
||||
#[case(Style::new().light_yellow(), Color::LightYellow)]
|
||||
#[case(Style::new().light_blue(), Color::LightBlue)]
|
||||
#[case(Style::new().light_magenta(), Color::LightMagenta)]
|
||||
#[case(Style::new().light_cyan(), Color::LightCyan)]
|
||||
#[case(Style::new().white(), Color::White)]
|
||||
fn fg_can_be_stylized(#[case] stylized: Style, #[case] expected: Color) {
|
||||
assert_eq!(stylized, Style::new().fg(expected));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(Style::new().on_black(), Color::Black)]
|
||||
#[case(Style::new().on_red(), Color::Red)]
|
||||
#[case(Style::new().on_green(), Color::Green)]
|
||||
#[case(Style::new().on_yellow(), Color::Yellow)]
|
||||
#[case(Style::new().on_blue(), Color::Blue)]
|
||||
#[case(Style::new().on_magenta(), Color::Magenta)]
|
||||
#[case(Style::new().on_cyan(), Color::Cyan)]
|
||||
#[case(Style::new().on_white(), Color::White)]
|
||||
#[case(Style::new().on_gray(), Color::Gray)]
|
||||
#[case(Style::new().on_dark_gray(), Color::DarkGray)]
|
||||
#[case(Style::new().on_light_red(), Color::LightRed)]
|
||||
#[case(Style::new().on_light_green(), Color::LightGreen)]
|
||||
#[case(Style::new().on_light_yellow(), Color::LightYellow)]
|
||||
#[case(Style::new().on_light_blue(), Color::LightBlue)]
|
||||
#[case(Style::new().on_light_magenta(), Color::LightMagenta)]
|
||||
#[case(Style::new().on_light_cyan(), Color::LightCyan)]
|
||||
#[case(Style::new().on_white(), Color::White)]
|
||||
fn bg_can_be_stylized(#[case] stylized: Style, #[case] expected: Color) {
|
||||
assert_eq!(stylized, Style::new().bg(expected));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(Style::new().bold(), Modifier::BOLD)]
|
||||
#[case(Style::new().dim(), Modifier::DIM)]
|
||||
#[case(Style::new().italic(), Modifier::ITALIC)]
|
||||
#[case(Style::new().underlined(), Modifier::UNDERLINED)]
|
||||
#[case(Style::new().slow_blink(), Modifier::SLOW_BLINK)]
|
||||
#[case(Style::new().rapid_blink(), Modifier::RAPID_BLINK)]
|
||||
#[case(Style::new().reversed(), Modifier::REVERSED)]
|
||||
#[case(Style::new().hidden(), Modifier::HIDDEN)]
|
||||
#[case(Style::new().crossed_out(), Modifier::CROSSED_OUT)]
|
||||
fn add_modifier_can_be_stylized(#[case] stylized: Style, #[case] expected: Modifier) {
|
||||
assert_eq!(stylized, Style::new().add_modifier(expected));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(Style::new().not_bold(), Modifier::BOLD)]
|
||||
#[case(Style::new().not_dim(), Modifier::DIM)]
|
||||
#[case(Style::new().not_italic(), Modifier::ITALIC)]
|
||||
#[case(Style::new().not_underlined(), Modifier::UNDERLINED)]
|
||||
#[case(Style::new().not_slow_blink(), Modifier::SLOW_BLINK)]
|
||||
#[case(Style::new().not_rapid_blink(), Modifier::RAPID_BLINK)]
|
||||
#[case(Style::new().not_reversed(), Modifier::REVERSED)]
|
||||
#[case(Style::new().not_hidden(), Modifier::HIDDEN)]
|
||||
#[case(Style::new().not_crossed_out(), Modifier::CROSSED_OUT)]
|
||||
fn remove_modifier_can_be_stylized(#[case] stylized: Style, #[case] expected: Modifier) {
|
||||
assert_eq!(stylized, Style::new().remove_modifier(expected));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn style_can_be_stylized() {
|
||||
// foreground colors
|
||||
assert_eq!(Style::new().black(), Style::new().fg(Color::Black));
|
||||
assert_eq!(Style::new().red(), Style::new().fg(Color::Red));
|
||||
assert_eq!(Style::new().green(), Style::new().fg(Color::Green));
|
||||
assert_eq!(Style::new().yellow(), Style::new().fg(Color::Yellow));
|
||||
assert_eq!(Style::new().blue(), Style::new().fg(Color::Blue));
|
||||
assert_eq!(Style::new().magenta(), Style::new().fg(Color::Magenta));
|
||||
assert_eq!(Style::new().cyan(), Style::new().fg(Color::Cyan));
|
||||
assert_eq!(Style::new().white(), Style::new().fg(Color::White));
|
||||
assert_eq!(Style::new().gray(), Style::new().fg(Color::Gray));
|
||||
assert_eq!(Style::new().dark_gray(), Style::new().fg(Color::DarkGray));
|
||||
assert_eq!(Style::new().light_red(), Style::new().fg(Color::LightRed));
|
||||
assert_eq!(
|
||||
Style::new().light_green(),
|
||||
Style::new().fg(Color::LightGreen)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().light_yellow(),
|
||||
Style::new().fg(Color::LightYellow)
|
||||
);
|
||||
assert_eq!(Style::new().light_blue(), Style::new().fg(Color::LightBlue));
|
||||
assert_eq!(
|
||||
Style::new().light_magenta(),
|
||||
Style::new().fg(Color::LightMagenta)
|
||||
);
|
||||
assert_eq!(Style::new().light_cyan(), Style::new().fg(Color::LightCyan));
|
||||
assert_eq!(Style::new().white(), Style::new().fg(Color::White));
|
||||
|
||||
// Background colors
|
||||
assert_eq!(Style::new().on_black(), Style::new().bg(Color::Black));
|
||||
assert_eq!(Style::new().on_red(), Style::new().bg(Color::Red));
|
||||
assert_eq!(Style::new().on_green(), Style::new().bg(Color::Green));
|
||||
assert_eq!(Style::new().on_yellow(), Style::new().bg(Color::Yellow));
|
||||
assert_eq!(Style::new().on_blue(), Style::new().bg(Color::Blue));
|
||||
assert_eq!(Style::new().on_magenta(), Style::new().bg(Color::Magenta));
|
||||
assert_eq!(Style::new().on_cyan(), Style::new().bg(Color::Cyan));
|
||||
assert_eq!(Style::new().on_white(), Style::new().bg(Color::White));
|
||||
assert_eq!(Style::new().on_gray(), Style::new().bg(Color::Gray));
|
||||
assert_eq!(
|
||||
Style::new().on_dark_gray(),
|
||||
Style::new().bg(Color::DarkGray)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().on_light_red(),
|
||||
Style::new().bg(Color::LightRed)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().on_light_green(),
|
||||
Style::new().bg(Color::LightGreen)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().on_light_yellow(),
|
||||
Style::new().bg(Color::LightYellow)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().on_light_blue(),
|
||||
Style::new().bg(Color::LightBlue)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().on_light_magenta(),
|
||||
Style::new().bg(Color::LightMagenta)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().on_light_cyan(),
|
||||
Style::new().bg(Color::LightCyan)
|
||||
);
|
||||
assert_eq!(Style::new().on_white(), Style::new().bg(Color::White));
|
||||
|
||||
// Add Modifiers
|
||||
assert_eq!(
|
||||
Style::new().bold(),
|
||||
Style::new().add_modifier(Modifier::BOLD)
|
||||
);
|
||||
assert_eq!(Style::new().dim(), Style::new().add_modifier(Modifier::DIM));
|
||||
assert_eq!(
|
||||
Style::new().italic(),
|
||||
Style::new().add_modifier(Modifier::ITALIC)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().underlined(),
|
||||
Style::new().add_modifier(Modifier::UNDERLINED)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().slow_blink(),
|
||||
Style::new().add_modifier(Modifier::SLOW_BLINK)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().rapid_blink(),
|
||||
Style::new().add_modifier(Modifier::RAPID_BLINK)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().reversed(),
|
||||
Style::new().add_modifier(Modifier::REVERSED)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().hidden(),
|
||||
Style::new().add_modifier(Modifier::HIDDEN)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().crossed_out(),
|
||||
Style::new().add_modifier(Modifier::CROSSED_OUT)
|
||||
);
|
||||
|
||||
// Remove Modifiers
|
||||
assert_eq!(
|
||||
Style::new().not_bold(),
|
||||
Style::new().remove_modifier(Modifier::BOLD)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().not_dim(),
|
||||
Style::new().remove_modifier(Modifier::DIM)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().not_italic(),
|
||||
Style::new().remove_modifier(Modifier::ITALIC)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().not_underlined(),
|
||||
Style::new().remove_modifier(Modifier::UNDERLINED)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().not_slow_blink(),
|
||||
Style::new().remove_modifier(Modifier::SLOW_BLINK)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().not_rapid_blink(),
|
||||
Style::new().remove_modifier(Modifier::RAPID_BLINK)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().not_reversed(),
|
||||
Style::new().remove_modifier(Modifier::REVERSED)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().not_hidden(),
|
||||
Style::new().remove_modifier(Modifier::HIDDEN)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().not_crossed_out(),
|
||||
Style::new().remove_modifier(Modifier::CROSSED_OUT)
|
||||
);
|
||||
|
||||
// reset
|
||||
fn reset_can_be_stylized() {
|
||||
assert_eq!(Style::new().reset(), Style::reset());
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -113,7 +115,7 @@ pub enum Color {
|
||||
/// 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>
|
||||
///
|
||||
@@ -313,16 +315,7 @@ impl FromStr for Color {
|
||||
_ => {
|
||||
if let Ok(index) = s.parse::<u8>() {
|
||||
Self::Indexed(index)
|
||||
} else if let (Ok(r), Ok(g), Ok(b)) = {
|
||||
if !s.starts_with('#') || s.len() != 7 {
|
||||
return Err(ParseColorError);
|
||||
}
|
||||
(
|
||||
u8::from_str_radix(&s[1..3], 16),
|
||||
u8::from_str_radix(&s[3..5], 16),
|
||||
u8::from_str_radix(&s[5..7], 16),
|
||||
)
|
||||
} {
|
||||
} else if let Some((r, g, b)) = parse_hex_color(s) {
|
||||
Self::Rgb(r, g, b)
|
||||
} else {
|
||||
return Err(ParseColorError);
|
||||
@@ -333,6 +326,16 @@ impl FromStr for Color {
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_hex_color(input: &str) -> Option<(u8, u8, u8)> {
|
||||
if !input.starts_with('#') || input.len() != 7 {
|
||||
return None;
|
||||
}
|
||||
let r = u8::from_str_radix(input.get(1..3)?, 16).ok()?;
|
||||
let g = u8::from_str_radix(input.get(3..5)?, 16).ok()?;
|
||||
let b = u8::from_str_radix(input.get(5..7)?, 16).ok()?;
|
||||
Some((r, g, b))
|
||||
}
|
||||
|
||||
impl fmt::Display for Color {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
@@ -360,6 +363,10 @@ 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
|
||||
@@ -587,6 +594,7 @@ mod tests {
|
||||
"abcdef0", // 7 chars is not a color
|
||||
" bcdefa", // doesn't start with a '#'
|
||||
"#abcdef00", // too many chars
|
||||
"#1🦀2", // len 7 but on char boundaries shouldnt panic
|
||||
"resett", // typo
|
||||
"lightblackk", // typo
|
||||
];
|
||||
83
ratatui-core/src/style/palette_conversion.rs
Normal file
83
ratatui-core/src/style/palette_conversion.rs
Normal file
@@ -0,0 +1,83 @@
|
||||
//! Conversions from colors in the `palette` crate to [`Color`].
|
||||
|
||||
use ::palette::{
|
||||
bool_mask::LazySelect,
|
||||
num::{Arithmetics, MulSub, PartialCmp, Powf, Real},
|
||||
LinSrgb,
|
||||
};
|
||||
use palette::{stimulus::IntoStimulus, Srgb};
|
||||
|
||||
use super::Color;
|
||||
|
||||
/// Convert an [`palette::Srgb`] color to a [`Color`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use palette::Srgb;
|
||||
/// use ratatui::style::Color;
|
||||
///
|
||||
/// let color = Color::from(Srgb::new(1.0f32, 0.0, 0.0));
|
||||
/// assert_eq!(color, Color::Rgb(255, 0, 0));
|
||||
/// ```
|
||||
impl<T: IntoStimulus<u8>> From<Srgb<T>> for Color {
|
||||
fn from(color: Srgb<T>) -> Self {
|
||||
let (red, green, blue) = color.into_format().into_components();
|
||||
Self::Rgb(red, green, blue)
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a [`palette::LinSrgb`] color to a [`Color`].
|
||||
///
|
||||
/// Note: this conversion only works for floating point linear sRGB colors. If you have a linear
|
||||
/// sRGB color in another format, you need to convert it to floating point first.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use palette::LinSrgb;
|
||||
/// use ratatui::style::Color;
|
||||
///
|
||||
/// let color = Color::from(LinSrgb::new(1.0f32, 0.0, 0.0));
|
||||
/// assert_eq!(color, Color::Rgb(255, 0, 0));
|
||||
/// ```
|
||||
impl<T: IntoStimulus<u8>> From<LinSrgb<T>> for Color
|
||||
where
|
||||
T: Real + Powf + MulSub + Arithmetics + PartialCmp + Clone,
|
||||
T::Mask: LazySelect<T>,
|
||||
{
|
||||
fn from(color: LinSrgb<T>) -> Self {
|
||||
let srgb_color = Srgb::<T>::from_linear(color);
|
||||
Self::from(srgb_color)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn from_srgb() {
|
||||
const RED: Color = Color::Rgb(255, 0, 0);
|
||||
assert_eq!(Color::from(Srgb::new(255u8, 0, 0)), RED);
|
||||
assert_eq!(Color::from(Srgb::new(65535u16, 0, 0)), RED);
|
||||
assert_eq!(Color::from(Srgb::new(1.0f32, 0.0, 0.0)), RED);
|
||||
|
||||
assert_eq!(
|
||||
Color::from(Srgb::new(0.5f32, 0.5, 0.5)),
|
||||
Color::Rgb(128, 128, 128)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_lin_srgb() {
|
||||
const RED: Color = Color::Rgb(255, 0, 0);
|
||||
assert_eq!(Color::from(LinSrgb::new(1.0f32, 0.0, 0.0)), RED);
|
||||
assert_eq!(Color::from(LinSrgb::new(1.0f64, 0.0, 0.0)), RED);
|
||||
|
||||
assert_eq!(
|
||||
Color::from(LinSrgb::new(0.5f32, 0.5, 0.5)),
|
||||
Color::Rgb(188, 188, 188)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -137,9 +208,9 @@ macro_rules! modifier {
|
||||
/// ```
|
||||
pub trait Stylize<'a, T>: Sized {
|
||||
#[must_use = "`bg` returns the modified style without modifying the original"]
|
||||
fn bg(self, color: Color) -> T;
|
||||
fn bg<C: Into<Color>>(self, color: C) -> T;
|
||||
#[must_use = "`fg` returns the modified style without modifying the original"]
|
||||
fn fg<S: Into<Color>>(self, color: S) -> T;
|
||||
fn fg<C: Into<Color>>(self, color: C) -> T;
|
||||
#[must_use = "`reset` returns the modified style without modifying the original"]
|
||||
fn reset(self) -> T;
|
||||
#[must_use = "`add_modifier` returns the modified style without modifying the original"]
|
||||
@@ -179,12 +250,12 @@ impl<'a, T, U> Stylize<'a, T> for U
|
||||
where
|
||||
U: Styled<Item = T>,
|
||||
{
|
||||
fn bg(self, color: Color) -> T {
|
||||
let style = self.style().bg(color);
|
||||
fn bg<C: Into<Color>>(self, color: C) -> T {
|
||||
let style = self.style().bg(color.into());
|
||||
self.set_style(style)
|
||||
}
|
||||
|
||||
fn fg<S: Into<Color>>(self, color: S) -> T {
|
||||
fn fg<C: Into<Color>>(self, color: C) -> T {
|
||||
let style = self.style().fg(color.into());
|
||||
self.set_style(style)
|
||||
}
|
||||
@@ -231,6 +302,7 @@ impl Styled for String {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use itertools::Itertools;
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -346,7 +418,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 +495,82 @@ mod tests {
|
||||
Span::styled("hello", all_modifier_black)
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(ColorDebugKind::Foreground, Color::Black, ".black()")]
|
||||
#[case(ColorDebugKind::Foreground, Color::Red, ".red()")]
|
||||
#[case(ColorDebugKind::Foreground, Color::Green, ".green()")]
|
||||
#[case(ColorDebugKind::Foreground, Color::Yellow, ".yellow()")]
|
||||
#[case(ColorDebugKind::Foreground, Color::Blue, ".blue()")]
|
||||
#[case(ColorDebugKind::Foreground, Color::Magenta, ".magenta()")]
|
||||
#[case(ColorDebugKind::Foreground, Color::Cyan, ".cyan()")]
|
||||
#[case(ColorDebugKind::Foreground, Color::Gray, ".gray()")]
|
||||
#[case(ColorDebugKind::Foreground, Color::DarkGray, ".dark_gray()")]
|
||||
#[case(ColorDebugKind::Foreground, Color::LightRed, ".light_red()")]
|
||||
#[case(ColorDebugKind::Foreground, Color::LightGreen, ".light_green()")]
|
||||
#[case(ColorDebugKind::Foreground, Color::LightYellow, ".light_yellow()")]
|
||||
#[case(ColorDebugKind::Foreground, Color::LightBlue, ".light_blue()")]
|
||||
#[case(ColorDebugKind::Foreground, Color::LightMagenta, ".light_magenta()")]
|
||||
#[case(ColorDebugKind::Foreground, Color::LightCyan, ".light_cyan()")]
|
||||
#[case(ColorDebugKind::Foreground, Color::White, ".white()")]
|
||||
#[case(
|
||||
ColorDebugKind::Foreground,
|
||||
Color::Indexed(10),
|
||||
".fg(Color::Indexed(10))"
|
||||
)]
|
||||
#[case(
|
||||
ColorDebugKind::Foreground,
|
||||
Color::Rgb(255, 0, 0),
|
||||
".fg(Color::Rgb(255, 0, 0))"
|
||||
)]
|
||||
#[case(ColorDebugKind::Background, Color::Black, ".on_black()")]
|
||||
#[case(ColorDebugKind::Background, Color::Red, ".on_red()")]
|
||||
#[case(ColorDebugKind::Background, Color::Green, ".on_green()")]
|
||||
#[case(ColorDebugKind::Background, Color::Yellow, ".on_yellow()")]
|
||||
#[case(ColorDebugKind::Background, Color::Blue, ".on_blue()")]
|
||||
#[case(ColorDebugKind::Background, Color::Magenta, ".on_magenta()")]
|
||||
#[case(ColorDebugKind::Background, Color::Cyan, ".on_cyan()")]
|
||||
#[case(ColorDebugKind::Background, Color::Gray, ".on_gray()")]
|
||||
#[case(ColorDebugKind::Background, Color::DarkGray, ".on_dark_gray()")]
|
||||
#[case(ColorDebugKind::Background, Color::LightRed, ".on_light_red()")]
|
||||
#[case(ColorDebugKind::Background, Color::LightGreen, ".on_light_green()")]
|
||||
#[case(ColorDebugKind::Background, Color::LightYellow, ".on_light_yellow()")]
|
||||
#[case(ColorDebugKind::Background, Color::LightBlue, ".on_light_blue()")]
|
||||
#[case(ColorDebugKind::Background, Color::LightMagenta, ".on_light_magenta()")]
|
||||
#[case(ColorDebugKind::Background, Color::LightCyan, ".on_light_cyan()")]
|
||||
#[case(ColorDebugKind::Background, Color::White, ".on_white()")]
|
||||
#[case(
|
||||
ColorDebugKind::Background,
|
||||
Color::Indexed(10),
|
||||
".bg(Color::Indexed(10))"
|
||||
)]
|
||||
#[case(
|
||||
ColorDebugKind::Background,
|
||||
Color::Rgb(255, 0, 0),
|
||||
".bg(Color::Rgb(255, 0, 0))"
|
||||
)]
|
||||
#[cfg(feature = "underline-color")]
|
||||
#[case(
|
||||
ColorDebugKind::Underline,
|
||||
Color::Black,
|
||||
".underline_color(Color::Black)"
|
||||
)]
|
||||
#[cfg(feature = "underline-color")]
|
||||
#[case(ColorDebugKind::Underline, Color::Red, ".underline_color(Color::Red)")]
|
||||
#[cfg(feature = "underline-color")]
|
||||
#[case(
|
||||
ColorDebugKind::Underline,
|
||||
Color::Green,
|
||||
".underline_color(Color::Green)"
|
||||
)]
|
||||
#[cfg(feature = "underline-color")]
|
||||
#[case(
|
||||
ColorDebugKind::Underline,
|
||||
Color::Yellow,
|
||||
".underline_color(Color::Yellow)"
|
||||
)]
|
||||
fn stylize_debug(#[case] kind: ColorDebugKind, #[case] color: Color, #[case] expected: &str) {
|
||||
let debug = color.stylize_debug(kind);
|
||||
assert_eq!(format!("{debug:?}"), expected);
|
||||
}
|
||||
}
|
||||
228
ratatui-core/src/symbols.rs
Normal file
228
ratatui-core/src/symbols.rs
Normal file
@@ -0,0 +1,228 @@
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
pub mod border;
|
||||
pub mod line;
|
||||
|
||||
pub mod block {
|
||||
pub const FULL: &str = "█";
|
||||
pub const SEVEN_EIGHTHS: &str = "▉";
|
||||
pub const THREE_QUARTERS: &str = "▊";
|
||||
pub const FIVE_EIGHTHS: &str = "▋";
|
||||
pub const HALF: &str = "▌";
|
||||
pub const THREE_EIGHTHS: &str = "▍";
|
||||
pub const ONE_QUARTER: &str = "▎";
|
||||
pub const ONE_EIGHTH: &str = "▏";
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Set {
|
||||
pub full: &'static str,
|
||||
pub seven_eighths: &'static str,
|
||||
pub three_quarters: &'static str,
|
||||
pub five_eighths: &'static str,
|
||||
pub half: &'static str,
|
||||
pub three_eighths: &'static str,
|
||||
pub one_quarter: &'static str,
|
||||
pub one_eighth: &'static str,
|
||||
pub empty: &'static str,
|
||||
}
|
||||
|
||||
impl Default for Set {
|
||||
fn default() -> Self {
|
||||
NINE_LEVELS
|
||||
}
|
||||
}
|
||||
|
||||
pub const THREE_LEVELS: Set = Set {
|
||||
full: FULL,
|
||||
seven_eighths: FULL,
|
||||
three_quarters: HALF,
|
||||
five_eighths: HALF,
|
||||
half: HALF,
|
||||
three_eighths: HALF,
|
||||
one_quarter: HALF,
|
||||
one_eighth: " ",
|
||||
empty: " ",
|
||||
};
|
||||
|
||||
pub const NINE_LEVELS: Set = Set {
|
||||
full: FULL,
|
||||
seven_eighths: SEVEN_EIGHTHS,
|
||||
three_quarters: THREE_QUARTERS,
|
||||
five_eighths: FIVE_EIGHTHS,
|
||||
half: HALF,
|
||||
three_eighths: THREE_EIGHTHS,
|
||||
one_quarter: ONE_QUARTER,
|
||||
one_eighth: ONE_EIGHTH,
|
||||
empty: " ",
|
||||
};
|
||||
}
|
||||
|
||||
pub mod half_block {
|
||||
pub const UPPER: char = '▀';
|
||||
pub const LOWER: char = '▄';
|
||||
pub const FULL: char = '█';
|
||||
}
|
||||
|
||||
pub mod bar {
|
||||
pub const FULL: &str = "█";
|
||||
pub const SEVEN_EIGHTHS: &str = "▇";
|
||||
pub const THREE_QUARTERS: &str = "▆";
|
||||
pub const FIVE_EIGHTHS: &str = "▅";
|
||||
pub const HALF: &str = "▄";
|
||||
pub const THREE_EIGHTHS: &str = "▃";
|
||||
pub const ONE_QUARTER: &str = "▂";
|
||||
pub const ONE_EIGHTH: &str = "▁";
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Set {
|
||||
pub full: &'static str,
|
||||
pub seven_eighths: &'static str,
|
||||
pub three_quarters: &'static str,
|
||||
pub five_eighths: &'static str,
|
||||
pub half: &'static str,
|
||||
pub three_eighths: &'static str,
|
||||
pub one_quarter: &'static str,
|
||||
pub one_eighth: &'static str,
|
||||
pub empty: &'static str,
|
||||
}
|
||||
|
||||
impl Default for Set {
|
||||
fn default() -> Self {
|
||||
NINE_LEVELS
|
||||
}
|
||||
}
|
||||
|
||||
pub const THREE_LEVELS: Set = Set {
|
||||
full: FULL,
|
||||
seven_eighths: FULL,
|
||||
three_quarters: HALF,
|
||||
five_eighths: HALF,
|
||||
half: HALF,
|
||||
three_eighths: HALF,
|
||||
one_quarter: HALF,
|
||||
one_eighth: " ",
|
||||
empty: " ",
|
||||
};
|
||||
|
||||
pub const NINE_LEVELS: Set = Set {
|
||||
full: FULL,
|
||||
seven_eighths: SEVEN_EIGHTHS,
|
||||
three_quarters: THREE_QUARTERS,
|
||||
five_eighths: FIVE_EIGHTHS,
|
||||
half: HALF,
|
||||
three_eighths: THREE_EIGHTHS,
|
||||
one_quarter: ONE_QUARTER,
|
||||
one_eighth: ONE_EIGHTH,
|
||||
empty: " ",
|
||||
};
|
||||
}
|
||||
|
||||
pub const DOT: &str = "•";
|
||||
|
||||
pub mod braille {
|
||||
pub const BLANK: u16 = 0x2800;
|
||||
pub const DOTS: [[u16; 2]; 4] = [
|
||||
[0x0001, 0x0008],
|
||||
[0x0002, 0x0010],
|
||||
[0x0004, 0x0020],
|
||||
[0x0040, 0x0080],
|
||||
];
|
||||
}
|
||||
|
||||
/// Marker to use when plotting data points
|
||||
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub enum Marker {
|
||||
/// One point per cell in shape of dot (`•`)
|
||||
#[default]
|
||||
Dot,
|
||||
/// One point per cell in shape of a block (`█`)
|
||||
Block,
|
||||
/// One point per cell in the shape of a bar (`▄`)
|
||||
Bar,
|
||||
/// Use the [Unicode Braille Patterns](https://en.wikipedia.org/wiki/Braille_Patterns) block to
|
||||
/// represent data points.
|
||||
///
|
||||
/// This is a 2x4 grid of dots, where each dot can be either on or off.
|
||||
///
|
||||
/// Note: Support for this marker is limited to terminals and fonts that support Unicode
|
||||
/// Braille Patterns. If your terminal does not support this, you will see unicode replacement
|
||||
/// characters (`<60>`) instead of Braille dots (`⠓`, `⣇`, `⣿`).
|
||||
Braille,
|
||||
/// Use the unicode block and half block characters (`█`, `▄`, and `▀`) to represent points in
|
||||
/// a grid that is double the resolution of the terminal. Because each terminal cell is
|
||||
/// generally about twice as tall as it is wide, this allows for a square grid of pixels.
|
||||
HalfBlock,
|
||||
}
|
||||
|
||||
pub mod scrollbar {
|
||||
use super::{block, line};
|
||||
|
||||
/// Scrollbar Set
|
||||
/// ```text
|
||||
/// <--▮------->
|
||||
/// ^ ^ ^ ^
|
||||
/// │ │ │ └ end
|
||||
/// │ │ └──── track
|
||||
/// │ └──────── thumb
|
||||
/// └─────────── begin
|
||||
/// ```
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Set {
|
||||
pub track: &'static str,
|
||||
pub thumb: &'static str,
|
||||
pub begin: &'static str,
|
||||
pub end: &'static str,
|
||||
}
|
||||
|
||||
pub const DOUBLE_VERTICAL: Set = Set {
|
||||
track: line::DOUBLE_VERTICAL,
|
||||
thumb: block::FULL,
|
||||
begin: "▲",
|
||||
end: "▼",
|
||||
};
|
||||
|
||||
pub const DOUBLE_HORIZONTAL: Set = Set {
|
||||
track: line::DOUBLE_HORIZONTAL,
|
||||
thumb: block::FULL,
|
||||
begin: "◄",
|
||||
end: "►",
|
||||
};
|
||||
|
||||
pub const VERTICAL: Set = Set {
|
||||
track: line::VERTICAL,
|
||||
thumb: block::FULL,
|
||||
begin: "↑",
|
||||
end: "↓",
|
||||
};
|
||||
|
||||
pub const HORIZONTAL: Set = Set {
|
||||
track: line::HORIZONTAL,
|
||||
thumb: block::FULL,
|
||||
begin: "←",
|
||||
end: "→",
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use strum::ParseError;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn marker_tostring() {
|
||||
assert_eq!(Marker::Dot.to_string(), "Dot");
|
||||
assert_eq!(Marker::Block.to_string(), "Block");
|
||||
assert_eq!(Marker::Bar.to_string(), "Bar");
|
||||
assert_eq!(Marker::Braille.to_string(), "Braille");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn marker_from_str() {
|
||||
assert_eq!("Dot".parse::<Marker>(), Ok(Marker::Dot));
|
||||
assert_eq!("Block".parse::<Marker>(), Ok(Marker::Block));
|
||||
assert_eq!("Bar".parse::<Marker>(), Ok(Marker::Bar));
|
||||
assert_eq!("Braille".parse::<Marker>(), Ok(Marker::Braille));
|
||||
assert_eq!("".parse::<Marker>(), Err(ParseError::VariantNotFound));
|
||||
}
|
||||
}
|
||||
509
ratatui-core/src/symbols/border.rs
Normal file
509
ratatui-core/src/symbols/border.rs
Normal file
@@ -0,0 +1,509 @@
|
||||
use super::{block, line};
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub struct Set {
|
||||
pub top_left: &'static str,
|
||||
pub top_right: &'static str,
|
||||
pub bottom_left: &'static str,
|
||||
pub bottom_right: &'static str,
|
||||
pub vertical_left: &'static str,
|
||||
pub vertical_right: &'static str,
|
||||
pub horizontal_top: &'static str,
|
||||
pub horizontal_bottom: &'static str,
|
||||
}
|
||||
|
||||
impl Default for Set {
|
||||
fn default() -> Self {
|
||||
PLAIN
|
||||
}
|
||||
}
|
||||
|
||||
/// Border Set with a single line width
|
||||
///
|
||||
/// ```text
|
||||
/// ┌─────┐
|
||||
/// │xxxxx│
|
||||
/// │xxxxx│
|
||||
/// └─────┘
|
||||
/// ```
|
||||
pub const PLAIN: Set = Set {
|
||||
top_left: line::NORMAL.top_left,
|
||||
top_right: line::NORMAL.top_right,
|
||||
bottom_left: line::NORMAL.bottom_left,
|
||||
bottom_right: line::NORMAL.bottom_right,
|
||||
vertical_left: line::NORMAL.vertical,
|
||||
vertical_right: line::NORMAL.vertical,
|
||||
horizontal_top: line::NORMAL.horizontal,
|
||||
horizontal_bottom: line::NORMAL.horizontal,
|
||||
};
|
||||
|
||||
/// Border Set with a single line width and rounded corners
|
||||
///
|
||||
/// ```text
|
||||
/// ╭─────╮
|
||||
/// │xxxxx│
|
||||
/// │xxxxx│
|
||||
/// ╰─────╯
|
||||
/// ```
|
||||
pub const ROUNDED: Set = Set {
|
||||
top_left: line::ROUNDED.top_left,
|
||||
top_right: line::ROUNDED.top_right,
|
||||
bottom_left: line::ROUNDED.bottom_left,
|
||||
bottom_right: line::ROUNDED.bottom_right,
|
||||
vertical_left: line::ROUNDED.vertical,
|
||||
vertical_right: line::ROUNDED.vertical,
|
||||
horizontal_top: line::ROUNDED.horizontal,
|
||||
horizontal_bottom: line::ROUNDED.horizontal,
|
||||
};
|
||||
|
||||
/// Border Set with a double line width
|
||||
///
|
||||
/// ```text
|
||||
/// ╔═════╗
|
||||
/// ║xxxxx║
|
||||
/// ║xxxxx║
|
||||
/// ╚═════╝
|
||||
/// ```
|
||||
pub const DOUBLE: Set = Set {
|
||||
top_left: line::DOUBLE.top_left,
|
||||
top_right: line::DOUBLE.top_right,
|
||||
bottom_left: line::DOUBLE.bottom_left,
|
||||
bottom_right: line::DOUBLE.bottom_right,
|
||||
vertical_left: line::DOUBLE.vertical,
|
||||
vertical_right: line::DOUBLE.vertical,
|
||||
horizontal_top: line::DOUBLE.horizontal,
|
||||
horizontal_bottom: line::DOUBLE.horizontal,
|
||||
};
|
||||
|
||||
/// Border Set with a thick line width
|
||||
///
|
||||
/// ```text
|
||||
/// ┏━━━━━┓
|
||||
/// ┃xxxxx┃
|
||||
/// ┃xxxxx┃
|
||||
/// ┗━━━━━┛
|
||||
/// ```
|
||||
pub const THICK: Set = Set {
|
||||
top_left: line::THICK.top_left,
|
||||
top_right: line::THICK.top_right,
|
||||
bottom_left: line::THICK.bottom_left,
|
||||
bottom_right: line::THICK.bottom_right,
|
||||
vertical_left: line::THICK.vertical,
|
||||
vertical_right: line::THICK.vertical,
|
||||
horizontal_top: line::THICK.horizontal,
|
||||
horizontal_bottom: line::THICK.horizontal,
|
||||
};
|
||||
|
||||
pub const QUADRANT_TOP_LEFT: &str = "▘";
|
||||
pub const QUADRANT_TOP_RIGHT: &str = "▝";
|
||||
pub const QUADRANT_BOTTOM_LEFT: &str = "▖";
|
||||
pub const QUADRANT_BOTTOM_RIGHT: &str = "▗";
|
||||
pub const QUADRANT_TOP_HALF: &str = "▀";
|
||||
pub const QUADRANT_BOTTOM_HALF: &str = "▄";
|
||||
pub const QUADRANT_LEFT_HALF: &str = "▌";
|
||||
pub const QUADRANT_RIGHT_HALF: &str = "▐";
|
||||
pub const QUADRANT_TOP_LEFT_BOTTOM_LEFT_BOTTOM_RIGHT: &str = "▙";
|
||||
pub const QUADRANT_TOP_LEFT_TOP_RIGHT_BOTTOM_LEFT: &str = "▛";
|
||||
pub const QUADRANT_TOP_LEFT_TOP_RIGHT_BOTTOM_RIGHT: &str = "▜";
|
||||
pub const QUADRANT_TOP_RIGHT_BOTTOM_LEFT_BOTTOM_RIGHT: &str = "▟";
|
||||
pub const QUADRANT_TOP_LEFT_BOTTOM_RIGHT: &str = "▚";
|
||||
pub const QUADRANT_TOP_RIGHT_BOTTOM_LEFT: &str = "▞";
|
||||
pub const QUADRANT_BLOCK: &str = "█";
|
||||
|
||||
/// Quadrant used for setting a border outside a block by one half cell "pixel".
|
||||
///
|
||||
/// ```text
|
||||
/// ▛▀▀▀▀▀▜
|
||||
/// ▌xxxxx▐
|
||||
/// ▌xxxxx▐
|
||||
/// ▙▄▄▄▄▄▟
|
||||
/// ```
|
||||
pub const QUADRANT_OUTSIDE: Set = Set {
|
||||
top_left: QUADRANT_TOP_LEFT_TOP_RIGHT_BOTTOM_LEFT,
|
||||
top_right: QUADRANT_TOP_LEFT_TOP_RIGHT_BOTTOM_RIGHT,
|
||||
bottom_left: QUADRANT_TOP_LEFT_BOTTOM_LEFT_BOTTOM_RIGHT,
|
||||
bottom_right: QUADRANT_TOP_RIGHT_BOTTOM_LEFT_BOTTOM_RIGHT,
|
||||
vertical_left: QUADRANT_LEFT_HALF,
|
||||
vertical_right: QUADRANT_RIGHT_HALF,
|
||||
horizontal_top: QUADRANT_TOP_HALF,
|
||||
horizontal_bottom: QUADRANT_BOTTOM_HALF,
|
||||
};
|
||||
|
||||
/// Quadrant used for setting a border inside a block by one half cell "pixel".
|
||||
///
|
||||
/// ```text
|
||||
/// ▗▄▄▄▄▄▖
|
||||
/// ▐xxxxx▌
|
||||
/// ▐xxxxx▌
|
||||
/// ▝▀▀▀▀▀▘
|
||||
/// ```
|
||||
pub const QUADRANT_INSIDE: Set = Set {
|
||||
top_right: QUADRANT_BOTTOM_LEFT,
|
||||
top_left: QUADRANT_BOTTOM_RIGHT,
|
||||
bottom_right: QUADRANT_TOP_LEFT,
|
||||
bottom_left: QUADRANT_TOP_RIGHT,
|
||||
vertical_left: QUADRANT_RIGHT_HALF,
|
||||
vertical_right: QUADRANT_LEFT_HALF,
|
||||
horizontal_top: QUADRANT_BOTTOM_HALF,
|
||||
horizontal_bottom: QUADRANT_TOP_HALF,
|
||||
};
|
||||
|
||||
pub const ONE_EIGHTH_TOP_EIGHT: &str = "▔";
|
||||
pub const ONE_EIGHTH_BOTTOM_EIGHT: &str = "▁";
|
||||
pub const ONE_EIGHTH_LEFT_EIGHT: &str = "▏";
|
||||
pub const ONE_EIGHTH_RIGHT_EIGHT: &str = "▕";
|
||||
|
||||
/// Wide border set based on McGugan box technique
|
||||
///
|
||||
/// ```text
|
||||
/// ▁▁▁▁▁▁▁
|
||||
/// ▏xxxxx▕
|
||||
/// ▏xxxxx▕
|
||||
/// ▔▔▔▔▔▔▔
|
||||
/// ```
|
||||
#[allow(clippy::doc_markdown)]
|
||||
pub const ONE_EIGHTH_WIDE: Set = Set {
|
||||
top_right: ONE_EIGHTH_BOTTOM_EIGHT,
|
||||
top_left: ONE_EIGHTH_BOTTOM_EIGHT,
|
||||
bottom_right: ONE_EIGHTH_TOP_EIGHT,
|
||||
bottom_left: ONE_EIGHTH_TOP_EIGHT,
|
||||
vertical_left: ONE_EIGHTH_LEFT_EIGHT,
|
||||
vertical_right: ONE_EIGHTH_RIGHT_EIGHT,
|
||||
horizontal_top: ONE_EIGHTH_BOTTOM_EIGHT,
|
||||
horizontal_bottom: ONE_EIGHTH_TOP_EIGHT,
|
||||
};
|
||||
|
||||
/// Tall border set based on McGugan box technique
|
||||
///
|
||||
/// ```text
|
||||
/// ▕▔▔▏
|
||||
/// ▕xx▏
|
||||
/// ▕xx▏
|
||||
/// ▕▁▁▏
|
||||
/// ```
|
||||
#[allow(clippy::doc_markdown)]
|
||||
pub const ONE_EIGHTH_TALL: Set = Set {
|
||||
top_right: ONE_EIGHTH_LEFT_EIGHT,
|
||||
top_left: ONE_EIGHTH_RIGHT_EIGHT,
|
||||
bottom_right: ONE_EIGHTH_LEFT_EIGHT,
|
||||
bottom_left: ONE_EIGHTH_RIGHT_EIGHT,
|
||||
vertical_left: ONE_EIGHTH_RIGHT_EIGHT,
|
||||
vertical_right: ONE_EIGHTH_LEFT_EIGHT,
|
||||
horizontal_top: ONE_EIGHTH_TOP_EIGHT,
|
||||
horizontal_bottom: ONE_EIGHTH_BOTTOM_EIGHT,
|
||||
};
|
||||
|
||||
/// Wide proportional (visually equal width and height) border with using set of quadrants.
|
||||
///
|
||||
/// The border is created by using half blocks for top and bottom, and full
|
||||
/// blocks for right and left sides to make horizontal and vertical borders seem equal.
|
||||
///
|
||||
/// ```text
|
||||
/// ▄▄▄▄
|
||||
/// █xx█
|
||||
/// █xx█
|
||||
/// ▀▀▀▀
|
||||
/// ```
|
||||
pub const PROPORTIONAL_WIDE: Set = Set {
|
||||
top_right: QUADRANT_BOTTOM_HALF,
|
||||
top_left: QUADRANT_BOTTOM_HALF,
|
||||
bottom_right: QUADRANT_TOP_HALF,
|
||||
bottom_left: QUADRANT_TOP_HALF,
|
||||
vertical_left: QUADRANT_BLOCK,
|
||||
vertical_right: QUADRANT_BLOCK,
|
||||
horizontal_top: QUADRANT_BOTTOM_HALF,
|
||||
horizontal_bottom: QUADRANT_TOP_HALF,
|
||||
};
|
||||
|
||||
/// Tall proportional (visually equal width and height) border with using set of quadrants.
|
||||
///
|
||||
/// The border is created by using full blocks for all sides, except for the top and bottom,
|
||||
/// which use half blocks to make horizontal and vertical borders seem equal.
|
||||
///
|
||||
/// ```text
|
||||
/// ▕█▀▀█
|
||||
/// ▕█xx█
|
||||
/// ▕█xx█
|
||||
/// ▕█▄▄█
|
||||
/// ```
|
||||
pub const PROPORTIONAL_TALL: Set = Set {
|
||||
top_right: QUADRANT_BLOCK,
|
||||
top_left: QUADRANT_BLOCK,
|
||||
bottom_right: QUADRANT_BLOCK,
|
||||
bottom_left: QUADRANT_BLOCK,
|
||||
vertical_left: QUADRANT_BLOCK,
|
||||
vertical_right: QUADRANT_BLOCK,
|
||||
horizontal_top: QUADRANT_TOP_HALF,
|
||||
horizontal_bottom: QUADRANT_BOTTOM_HALF,
|
||||
};
|
||||
|
||||
/// Solid border set
|
||||
///
|
||||
/// The border is created by using full blocks for all sides.
|
||||
///
|
||||
/// ```text
|
||||
/// ████
|
||||
/// █xx█
|
||||
/// █xx█
|
||||
/// ████
|
||||
/// ```
|
||||
pub const FULL: Set = Set {
|
||||
top_left: block::FULL,
|
||||
top_right: block::FULL,
|
||||
bottom_left: block::FULL,
|
||||
bottom_right: block::FULL,
|
||||
vertical_left: block::FULL,
|
||||
vertical_right: block::FULL,
|
||||
horizontal_top: block::FULL,
|
||||
horizontal_bottom: block::FULL,
|
||||
};
|
||||
|
||||
/// Empty border set
|
||||
///
|
||||
/// The border is created by using empty strings for all sides.
|
||||
///
|
||||
/// This is useful for ensuring that the border style is applied to a border on a block with a title
|
||||
/// without actually drawing a border.
|
||||
///
|
||||
/// ░ Example
|
||||
///
|
||||
/// `░` represents the content in the area not covered by the border to make it easier to see the
|
||||
/// blank symbols.
|
||||
///
|
||||
/// ```text
|
||||
/// ░░░░░░░░
|
||||
/// ░░ ░░
|
||||
/// ░░ ░░ ░░
|
||||
/// ░░ ░░ ░░
|
||||
/// ░░ ░░
|
||||
/// ░░░░░░░░
|
||||
/// ```
|
||||
pub const EMPTY: Set = Set {
|
||||
top_left: " ",
|
||||
top_right: " ",
|
||||
bottom_left: " ",
|
||||
bottom_right: " ",
|
||||
vertical_left: " ",
|
||||
vertical_right: " ",
|
||||
horizontal_top: " ",
|
||||
horizontal_bottom: " ",
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use indoc::{formatdoc, indoc};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn default() {
|
||||
assert_eq!(Set::default(), PLAIN);
|
||||
}
|
||||
|
||||
/// A helper function to render a border set to a string.
|
||||
///
|
||||
/// '░' (U+2591 Light Shade) is used as a placeholder for empty space to make it easier to see
|
||||
/// the size of the border symbols.
|
||||
fn render(set: Set) -> String {
|
||||
formatdoc!(
|
||||
"░░░░░░
|
||||
░{}{}{}{}░
|
||||
░{}░░{}░
|
||||
░{}░░{}░
|
||||
░{}{}{}{}░
|
||||
░░░░░░",
|
||||
set.top_left,
|
||||
set.horizontal_top,
|
||||
set.horizontal_top,
|
||||
set.top_right,
|
||||
set.vertical_left,
|
||||
set.vertical_right,
|
||||
set.vertical_left,
|
||||
set.vertical_right,
|
||||
set.bottom_left,
|
||||
set.horizontal_bottom,
|
||||
set.horizontal_bottom,
|
||||
set.bottom_right
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plain() {
|
||||
assert_eq!(
|
||||
render(PLAIN),
|
||||
indoc!(
|
||||
"░░░░░░
|
||||
░┌──┐░
|
||||
░│░░│░
|
||||
░│░░│░
|
||||
░└──┘░
|
||||
░░░░░░"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rounded() {
|
||||
assert_eq!(
|
||||
render(ROUNDED),
|
||||
indoc!(
|
||||
"░░░░░░
|
||||
░╭──╮░
|
||||
░│░░│░
|
||||
░│░░│░
|
||||
░╰──╯░
|
||||
░░░░░░"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn double() {
|
||||
assert_eq!(
|
||||
render(DOUBLE),
|
||||
indoc!(
|
||||
"░░░░░░
|
||||
░╔══╗░
|
||||
░║░░║░
|
||||
░║░░║░
|
||||
░╚══╝░
|
||||
░░░░░░"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thick() {
|
||||
assert_eq!(
|
||||
render(THICK),
|
||||
indoc!(
|
||||
"░░░░░░
|
||||
░┏━━┓░
|
||||
░┃░░┃░
|
||||
░┃░░┃░
|
||||
░┗━━┛░
|
||||
░░░░░░"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quadrant_outside() {
|
||||
assert_eq!(
|
||||
render(QUADRANT_OUTSIDE),
|
||||
indoc!(
|
||||
"░░░░░░
|
||||
░▛▀▀▜░
|
||||
░▌░░▐░
|
||||
░▌░░▐░
|
||||
░▙▄▄▟░
|
||||
░░░░░░"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quadrant_inside() {
|
||||
assert_eq!(
|
||||
render(QUADRANT_INSIDE),
|
||||
indoc!(
|
||||
"░░░░░░
|
||||
░▗▄▄▖░
|
||||
░▐░░▌░
|
||||
░▐░░▌░
|
||||
░▝▀▀▘░
|
||||
░░░░░░"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_eighth_wide() {
|
||||
assert_eq!(
|
||||
render(ONE_EIGHTH_WIDE),
|
||||
indoc!(
|
||||
"░░░░░░
|
||||
░▁▁▁▁░
|
||||
░▏░░▕░
|
||||
░▏░░▕░
|
||||
░▔▔▔▔░
|
||||
░░░░░░"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_eighth_tall() {
|
||||
assert_eq!(
|
||||
render(ONE_EIGHTH_TALL),
|
||||
indoc!(
|
||||
"░░░░░░
|
||||
░▕▔▔▏░
|
||||
░▕░░▏░
|
||||
░▕░░▏░
|
||||
░▕▁▁▏░
|
||||
░░░░░░"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proportional_wide() {
|
||||
assert_eq!(
|
||||
render(PROPORTIONAL_WIDE),
|
||||
indoc!(
|
||||
"░░░░░░
|
||||
░▄▄▄▄░
|
||||
░█░░█░
|
||||
░█░░█░
|
||||
░▀▀▀▀░
|
||||
░░░░░░"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proportional_tall() {
|
||||
assert_eq!(
|
||||
render(PROPORTIONAL_TALL),
|
||||
indoc!(
|
||||
"░░░░░░
|
||||
░█▀▀█░
|
||||
░█░░█░
|
||||
░█░░█░
|
||||
░█▄▄█░
|
||||
░░░░░░"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full() {
|
||||
assert_eq!(
|
||||
render(FULL),
|
||||
indoc!(
|
||||
"░░░░░░
|
||||
░████░
|
||||
░█░░█░
|
||||
░█░░█░
|
||||
░████░
|
||||
░░░░░░"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
assert_eq!(
|
||||
render(EMPTY),
|
||||
indoc!(
|
||||
"░░░░░░
|
||||
░ ░
|
||||
░ ░░ ░
|
||||
░ ░░ ░
|
||||
░ ░
|
||||
░░░░░░"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
208
ratatui-core/src/symbols/line.rs
Normal file
208
ratatui-core/src/symbols/line.rs
Normal file
@@ -0,0 +1,208 @@
|
||||
pub const VERTICAL: &str = "│";
|
||||
pub const DOUBLE_VERTICAL: &str = "║";
|
||||
pub const THICK_VERTICAL: &str = "┃";
|
||||
|
||||
pub const HORIZONTAL: &str = "─";
|
||||
pub const DOUBLE_HORIZONTAL: &str = "═";
|
||||
pub const THICK_HORIZONTAL: &str = "━";
|
||||
|
||||
pub const TOP_RIGHT: &str = "┐";
|
||||
pub const ROUNDED_TOP_RIGHT: &str = "╮";
|
||||
pub const DOUBLE_TOP_RIGHT: &str = "╗";
|
||||
pub const THICK_TOP_RIGHT: &str = "┓";
|
||||
|
||||
pub const TOP_LEFT: &str = "┌";
|
||||
pub const ROUNDED_TOP_LEFT: &str = "╭";
|
||||
pub const DOUBLE_TOP_LEFT: &str = "╔";
|
||||
pub const THICK_TOP_LEFT: &str = "┏";
|
||||
|
||||
pub const BOTTOM_RIGHT: &str = "┘";
|
||||
pub const ROUNDED_BOTTOM_RIGHT: &str = "╯";
|
||||
pub const DOUBLE_BOTTOM_RIGHT: &str = "╝";
|
||||
pub const THICK_BOTTOM_RIGHT: &str = "┛";
|
||||
|
||||
pub const BOTTOM_LEFT: &str = "└";
|
||||
pub const ROUNDED_BOTTOM_LEFT: &str = "╰";
|
||||
pub const DOUBLE_BOTTOM_LEFT: &str = "╚";
|
||||
pub const THICK_BOTTOM_LEFT: &str = "┗";
|
||||
|
||||
pub const VERTICAL_LEFT: &str = "┤";
|
||||
pub const DOUBLE_VERTICAL_LEFT: &str = "╣";
|
||||
pub const THICK_VERTICAL_LEFT: &str = "┫";
|
||||
|
||||
pub const VERTICAL_RIGHT: &str = "├";
|
||||
pub const DOUBLE_VERTICAL_RIGHT: &str = "╠";
|
||||
pub const THICK_VERTICAL_RIGHT: &str = "┣";
|
||||
|
||||
pub const HORIZONTAL_DOWN: &str = "┬";
|
||||
pub const DOUBLE_HORIZONTAL_DOWN: &str = "╦";
|
||||
pub const THICK_HORIZONTAL_DOWN: &str = "┳";
|
||||
|
||||
pub const HORIZONTAL_UP: &str = "┴";
|
||||
pub const DOUBLE_HORIZONTAL_UP: &str = "╩";
|
||||
pub const THICK_HORIZONTAL_UP: &str = "┻";
|
||||
|
||||
pub const CROSS: &str = "┼";
|
||||
pub const DOUBLE_CROSS: &str = "╬";
|
||||
pub const THICK_CROSS: &str = "╋";
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub struct Set {
|
||||
pub vertical: &'static str,
|
||||
pub horizontal: &'static str,
|
||||
pub top_right: &'static str,
|
||||
pub top_left: &'static str,
|
||||
pub bottom_right: &'static str,
|
||||
pub bottom_left: &'static str,
|
||||
pub vertical_left: &'static str,
|
||||
pub vertical_right: &'static str,
|
||||
pub horizontal_down: &'static str,
|
||||
pub horizontal_up: &'static str,
|
||||
pub cross: &'static str,
|
||||
}
|
||||
|
||||
impl Default for Set {
|
||||
fn default() -> Self {
|
||||
NORMAL
|
||||
}
|
||||
}
|
||||
|
||||
pub const NORMAL: Set = Set {
|
||||
vertical: VERTICAL,
|
||||
horizontal: HORIZONTAL,
|
||||
top_right: TOP_RIGHT,
|
||||
top_left: TOP_LEFT,
|
||||
bottom_right: BOTTOM_RIGHT,
|
||||
bottom_left: BOTTOM_LEFT,
|
||||
vertical_left: VERTICAL_LEFT,
|
||||
vertical_right: VERTICAL_RIGHT,
|
||||
horizontal_down: HORIZONTAL_DOWN,
|
||||
horizontal_up: HORIZONTAL_UP,
|
||||
cross: CROSS,
|
||||
};
|
||||
|
||||
pub const ROUNDED: Set = Set {
|
||||
top_right: ROUNDED_TOP_RIGHT,
|
||||
top_left: ROUNDED_TOP_LEFT,
|
||||
bottom_right: ROUNDED_BOTTOM_RIGHT,
|
||||
bottom_left: ROUNDED_BOTTOM_LEFT,
|
||||
..NORMAL
|
||||
};
|
||||
|
||||
pub const DOUBLE: Set = Set {
|
||||
vertical: DOUBLE_VERTICAL,
|
||||
horizontal: DOUBLE_HORIZONTAL,
|
||||
top_right: DOUBLE_TOP_RIGHT,
|
||||
top_left: DOUBLE_TOP_LEFT,
|
||||
bottom_right: DOUBLE_BOTTOM_RIGHT,
|
||||
bottom_left: DOUBLE_BOTTOM_LEFT,
|
||||
vertical_left: DOUBLE_VERTICAL_LEFT,
|
||||
vertical_right: DOUBLE_VERTICAL_RIGHT,
|
||||
horizontal_down: DOUBLE_HORIZONTAL_DOWN,
|
||||
horizontal_up: DOUBLE_HORIZONTAL_UP,
|
||||
cross: DOUBLE_CROSS,
|
||||
};
|
||||
|
||||
pub const THICK: Set = Set {
|
||||
vertical: THICK_VERTICAL,
|
||||
horizontal: THICK_HORIZONTAL,
|
||||
top_right: THICK_TOP_RIGHT,
|
||||
top_left: THICK_TOP_LEFT,
|
||||
bottom_right: THICK_BOTTOM_RIGHT,
|
||||
bottom_left: THICK_BOTTOM_LEFT,
|
||||
vertical_left: THICK_VERTICAL_LEFT,
|
||||
vertical_right: THICK_VERTICAL_RIGHT,
|
||||
horizontal_down: THICK_HORIZONTAL_DOWN,
|
||||
horizontal_up: THICK_HORIZONTAL_UP,
|
||||
cross: THICK_CROSS,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use indoc::{formatdoc, indoc};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn default() {
|
||||
assert_eq!(Set::default(), NORMAL);
|
||||
}
|
||||
|
||||
/// A helper function to render a set of symbols.
|
||||
fn render(set: Set) -> String {
|
||||
formatdoc!(
|
||||
"{}{}{}{}
|
||||
{}{}{}{}
|
||||
{}{}{}{}
|
||||
{}{}{}{}",
|
||||
set.top_left,
|
||||
set.horizontal,
|
||||
set.horizontal_down,
|
||||
set.top_right,
|
||||
set.vertical,
|
||||
" ",
|
||||
set.vertical,
|
||||
set.vertical,
|
||||
set.vertical_right,
|
||||
set.horizontal,
|
||||
set.cross,
|
||||
set.vertical_left,
|
||||
set.bottom_left,
|
||||
set.horizontal,
|
||||
set.horizontal_up,
|
||||
set.bottom_right
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normal() {
|
||||
assert_eq!(
|
||||
render(NORMAL),
|
||||
indoc!(
|
||||
"┌─┬┐
|
||||
│ ││
|
||||
├─┼┤
|
||||
└─┴┘"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rounded() {
|
||||
assert_eq!(
|
||||
render(ROUNDED),
|
||||
indoc!(
|
||||
"╭─┬╮
|
||||
│ ││
|
||||
├─┼┤
|
||||
╰─┴╯"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn double() {
|
||||
assert_eq!(
|
||||
render(DOUBLE),
|
||||
indoc!(
|
||||
"╔═╦╗
|
||||
║ ║║
|
||||
╠═╬╣
|
||||
╚═╩╝"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thick() {
|
||||
assert_eq!(
|
||||
render(THICK),
|
||||
indoc!(
|
||||
"┏━┳┓
|
||||
┃ ┃┃
|
||||
┣━╋┫
|
||||
┗━┻┛"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@
|
||||
//! let backend = CrosstermBackend::new(stdout());
|
||||
//! let mut terminal = Terminal::new(backend)?;
|
||||
//! terminal.draw(|frame| {
|
||||
//! let area = frame.size();
|
||||
//! let area = frame.area();
|
||||
//! frame.render_widget(Paragraph::new("Hello world!"), area);
|
||||
//! })?;
|
||||
//! # std::io::Result::Ok(())
|
||||
@@ -32,7 +32,6 @@
|
||||
//! [`Buffer`]: crate::buffer::Buffer
|
||||
|
||||
mod frame;
|
||||
#[allow(clippy::module_inception)]
|
||||
mod terminal;
|
||||
mod viewport;
|
||||
|
||||
@@ -16,7 +16,7 @@ pub struct Frame<'a> {
|
||||
///
|
||||
/// If `None`, the cursor is hidden and its position is controlled by the backend. If `Some((x,
|
||||
/// y))`, the cursor is shown and placed at `(x, y)` after the call to `Terminal::draw()`.
|
||||
pub(crate) cursor_position: Option<(u16, u16)>,
|
||||
pub(crate) cursor_position: Option<Position>,
|
||||
|
||||
/// The area of the viewport
|
||||
pub(crate) viewport_area: Rect,
|
||||
@@ -42,13 +42,25 @@ pub struct CompletedFrame<'a> {
|
||||
}
|
||||
|
||||
impl Frame<'_> {
|
||||
/// The size of the current frame
|
||||
/// The area of the current frame
|
||||
///
|
||||
/// This is guaranteed not to change during rendering, so may be called multiple times.
|
||||
///
|
||||
/// If your app listens for a resize event from the backend, it should ignore the values from
|
||||
/// the event for any calculations that are used to render the current frame and use this value
|
||||
/// instead as this is the size of the buffer that is used to render the current frame.
|
||||
/// instead as this is the area of the buffer that is used to render the current frame.
|
||||
pub const fn area(&self) -> Rect {
|
||||
self.viewport_area
|
||||
}
|
||||
|
||||
/// The area of the current frame
|
||||
///
|
||||
/// This is guaranteed not to change during rendering, so may be called multiple times.
|
||||
///
|
||||
/// If your app listens for a resize event from the backend, it should ignore the values from
|
||||
/// the event for any calculations that are used to render the current frame and use this value
|
||||
/// instead as this is the area of the buffer that is used to render the current frame.
|
||||
#[deprecated = "use .area() as it's the more correct name"]
|
||||
pub const fn size(&self) -> Rect {
|
||||
self.viewport_area
|
||||
}
|
||||
@@ -94,7 +106,7 @@ impl Frame<'_> {
|
||||
/// # }
|
||||
/// ```
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
#[stability::unstable(feature = "widget-ref")]
|
||||
#[instability::unstable(feature = "widget-ref")]
|
||||
pub fn render_widget_ref<W: WidgetRef>(&mut self, widget: W, area: Rect) {
|
||||
widget.render_ref(area, self.buffer);
|
||||
}
|
||||
@@ -152,7 +164,7 @@ impl Frame<'_> {
|
||||
/// # }
|
||||
/// ```
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
#[stability::unstable(feature = "widget-ref")]
|
||||
#[instability::unstable(feature = "widget-ref")]
|
||||
pub fn render_stateful_widget_ref<W>(&mut self, widget: W, area: Rect, state: &mut W::State)
|
||||
where
|
||||
W: StatefulWidgetRef,
|
||||
@@ -163,11 +175,22 @@ impl Frame<'_> {
|
||||
/// After drawing this frame, make the cursor visible and put it at the specified (x, y)
|
||||
/// coordinates. If this method is not called, the cursor will be hidden.
|
||||
///
|
||||
/// Note that this will interfere with calls to `Terminal::hide_cursor()`,
|
||||
/// `Terminal::show_cursor()`, and `Terminal::set_cursor()`. Pick one of the APIs and stick
|
||||
/// with it.
|
||||
/// Note that this will interfere with calls to [`Terminal::hide_cursor`],
|
||||
/// [`Terminal::show_cursor`], and [`Terminal::set_cursor_position`]. Pick one of the APIs and
|
||||
/// stick with it.
|
||||
pub fn set_cursor_position<P: Into<Position>>(&mut self, position: P) {
|
||||
self.cursor_position = Some(position.into());
|
||||
}
|
||||
|
||||
/// After drawing this frame, make the cursor visible and put it at the specified (x, y)
|
||||
/// coordinates. If this method is not called, the cursor will be hidden.
|
||||
///
|
||||
/// Note that this will interfere with calls to [`Terminal::hide_cursor`],
|
||||
/// [`Terminal::show_cursor`], and [`Terminal::set_cursor_position`]. Pick one of the APIs and
|
||||
/// stick with it.
|
||||
#[deprecated = "the method set_cursor_position indicates more clearly what about the cursor to set"]
|
||||
pub fn set_cursor(&mut self, x: u16, y: u16) {
|
||||
self.cursor_position = Some((x, y));
|
||||
self.set_cursor_position(Position { x, y });
|
||||
}
|
||||
|
||||
/// Gets the buffer that this `Frame` draws into as a mutable reference.
|
||||
727
ratatui-core/src/terminal/terminal.rs
Normal file
727
ratatui-core/src/terminal/terminal.rs
Normal file
@@ -0,0 +1,727 @@
|
||||
use std::io;
|
||||
|
||||
use crate::{
|
||||
backend::ClearType, buffer::Cell, prelude::*, CompletedFrame, TerminalOptions, Viewport,
|
||||
};
|
||||
|
||||
/// An interface to interact and draw [`Frame`]s on the user's terminal.
|
||||
///
|
||||
/// This is the main entry point for Ratatui. It is responsible for drawing and maintaining the
|
||||
/// state of the buffers, cursor and viewport.
|
||||
///
|
||||
/// The [`Terminal`] is generic over a [`Backend`] implementation which is used to interface with
|
||||
/// the underlying terminal library. The [`Backend`] trait is implemented for three popular Rust
|
||||
/// terminal libraries: [Crossterm], [Termion] and [Termwiz]. See the [`backend`] module for more
|
||||
/// information.
|
||||
///
|
||||
/// The `Terminal` struct maintains two buffers: the current and the previous.
|
||||
/// When the widgets are drawn, the changes are accumulated in the current buffer.
|
||||
/// At the end of each draw pass, the two buffers are compared, and only the changes
|
||||
/// between these buffers are written to the terminal, avoiding any redundant operations.
|
||||
/// After flushing these changes, the buffers are swapped to prepare for the next draw cycle.
|
||||
///
|
||||
/// The terminal also has a viewport which is the area of the terminal that is currently visible to
|
||||
/// the user. It can be either fullscreen, inline or fixed. See [`Viewport`] for more information.
|
||||
///
|
||||
/// Applications should detect terminal resizes and call [`Terminal::draw`] to redraw the
|
||||
/// application with the new size. This will automatically resize the internal buffers to match the
|
||||
/// new size for inline and fullscreen viewports. Fixed viewports are not resized automatically.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use std::io::stdout;
|
||||
///
|
||||
/// use ratatui::widgets::Paragraph;
|
||||
///
|
||||
/// let backend = CrosstermBackend::new(stdout());
|
||||
/// let mut terminal = Terminal::new(backend)?;
|
||||
/// terminal.draw(|frame| {
|
||||
/// let area = frame.area();
|
||||
/// frame.render_widget(Paragraph::new("Hello World!"), area);
|
||||
/// })?;
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
///
|
||||
/// [Crossterm]: https://crates.io/crates/crossterm
|
||||
/// [Termion]: https://crates.io/crates/termion
|
||||
/// [Termwiz]: https://crates.io/crates/termwiz
|
||||
/// [`backend`]: crate::backend
|
||||
/// [`Backend`]: crate::backend::Backend
|
||||
/// [`Buffer`]: crate::buffer::Buffer
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Terminal<B>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
/// The backend used to interface with the terminal
|
||||
backend: B,
|
||||
/// Holds the results of the current and previous draw calls. The two are compared at the end
|
||||
/// of each draw pass to output the necessary updates to the terminal
|
||||
buffers: [Buffer; 2],
|
||||
/// Index of the current buffer in the previous array
|
||||
current: usize,
|
||||
/// Whether the cursor is currently hidden
|
||||
hidden_cursor: bool,
|
||||
/// Viewport
|
||||
viewport: Viewport,
|
||||
/// Area of the viewport
|
||||
viewport_area: Rect,
|
||||
/// Last known area of the terminal. Used to detect if the internal buffers have to be resized.
|
||||
last_known_area: Rect,
|
||||
/// Last known position of the cursor. Used to find the new area when the viewport is inlined
|
||||
/// and the terminal resized.
|
||||
last_known_cursor_pos: Position,
|
||||
/// Number of frames rendered up until current time.
|
||||
frame_count: usize,
|
||||
}
|
||||
|
||||
/// Options to pass to [`Terminal::with_options`]
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Options {
|
||||
/// Viewport used to draw to the terminal
|
||||
pub viewport: Viewport,
|
||||
}
|
||||
|
||||
impl<B> Drop for Terminal<B>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
fn drop(&mut self) {
|
||||
// Attempt to restore the cursor state
|
||||
if self.hidden_cursor {
|
||||
if let Err(err) = self.show_cursor() {
|
||||
eprintln!("Failed to show the cursor: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> Terminal<B>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
/// Creates a new [`Terminal`] with the given [`Backend`] with a full screen viewport.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use std::io::stdout;
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let backend = CrosstermBackend::new(stdout());
|
||||
/// let terminal = Terminal::new(backend)?;
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
pub fn new(backend: B) -> io::Result<Self> {
|
||||
Self::with_options(
|
||||
backend,
|
||||
TerminalOptions {
|
||||
viewport: Viewport::Fullscreen,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a new [`Terminal`] with the given [`Backend`] and [`TerminalOptions`].
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use std::io::stdout;
|
||||
/// # use ratatui::{prelude::*, backend::TestBackend, Viewport, TerminalOptions};
|
||||
/// let backend = CrosstermBackend::new(stdout());
|
||||
/// let viewport = Viewport::Fixed(Rect::new(0, 0, 10, 10));
|
||||
/// let terminal = Terminal::with_options(backend, TerminalOptions { viewport })?;
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
pub fn with_options(mut backend: B, options: TerminalOptions) -> io::Result<Self> {
|
||||
let area = match options.viewport {
|
||||
Viewport::Fullscreen | Viewport::Inline(_) => {
|
||||
Rect::from((Position::ORIGIN, backend.size()?))
|
||||
}
|
||||
Viewport::Fixed(area) => area,
|
||||
};
|
||||
let (viewport_area, cursor_pos) = match options.viewport {
|
||||
Viewport::Fullscreen => (area, Position::ORIGIN),
|
||||
Viewport::Inline(height) => {
|
||||
compute_inline_size(&mut backend, height, area.as_size(), 0)?
|
||||
}
|
||||
Viewport::Fixed(area) => (area, area.as_position()),
|
||||
};
|
||||
Ok(Self {
|
||||
backend,
|
||||
buffers: [Buffer::empty(viewport_area), Buffer::empty(viewport_area)],
|
||||
current: 0,
|
||||
hidden_cursor: false,
|
||||
viewport: options.viewport,
|
||||
viewport_area,
|
||||
last_known_area: area,
|
||||
last_known_cursor_pos: cursor_pos,
|
||||
frame_count: 0,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get a Frame object which provides a consistent view into the terminal state for rendering.
|
||||
pub fn get_frame(&mut self) -> Frame {
|
||||
let count = self.frame_count;
|
||||
Frame {
|
||||
cursor_position: None,
|
||||
viewport_area: self.viewport_area,
|
||||
buffer: self.current_buffer_mut(),
|
||||
count,
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the current buffer as a mutable reference.
|
||||
pub fn current_buffer_mut(&mut self) -> &mut Buffer {
|
||||
&mut self.buffers[self.current]
|
||||
}
|
||||
|
||||
/// Gets the backend
|
||||
pub const fn backend(&self) -> &B {
|
||||
&self.backend
|
||||
}
|
||||
|
||||
/// Gets the backend as a mutable reference
|
||||
pub fn backend_mut(&mut self) -> &mut B {
|
||||
&mut self.backend
|
||||
}
|
||||
|
||||
/// Obtains a difference between the previous and the current buffer and passes it to the
|
||||
/// current backend for drawing.
|
||||
pub fn flush(&mut self) -> io::Result<()> {
|
||||
let previous_buffer = &self.buffers[1 - self.current];
|
||||
let current_buffer = &self.buffers[self.current];
|
||||
let updates = previous_buffer.diff(current_buffer);
|
||||
if let Some((col, row, _)) = updates.last() {
|
||||
self.last_known_cursor_pos = Position { x: *col, y: *row };
|
||||
}
|
||||
self.backend.draw(updates.into_iter())
|
||||
}
|
||||
|
||||
/// Updates the Terminal so that internal buffers match the requested area.
|
||||
///
|
||||
/// Requested area will be saved to remain consistent when rendering. This leads to a full clear
|
||||
/// of the screen.
|
||||
pub fn resize(&mut self, area: Rect) -> io::Result<()> {
|
||||
let next_area = match self.viewport {
|
||||
Viewport::Inline(height) => {
|
||||
let offset_in_previous_viewport = self
|
||||
.last_known_cursor_pos
|
||||
.y
|
||||
.saturating_sub(self.viewport_area.top());
|
||||
compute_inline_size(
|
||||
&mut self.backend,
|
||||
height,
|
||||
area.as_size(),
|
||||
offset_in_previous_viewport,
|
||||
)?
|
||||
.0
|
||||
}
|
||||
Viewport::Fixed(_) | Viewport::Fullscreen => area,
|
||||
};
|
||||
self.set_viewport_area(next_area);
|
||||
self.clear()?;
|
||||
|
||||
self.last_known_area = area;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_viewport_area(&mut self, area: Rect) {
|
||||
self.buffers[self.current].resize(area);
|
||||
self.buffers[1 - self.current].resize(area);
|
||||
self.viewport_area = area;
|
||||
}
|
||||
|
||||
/// Queries the backend for size and resizes if it doesn't match the previous size.
|
||||
pub fn autoresize(&mut self) -> io::Result<()> {
|
||||
// fixed viewports do not get autoresized
|
||||
if matches!(self.viewport, Viewport::Fullscreen | Viewport::Inline(_)) {
|
||||
let area = Rect::from((Position::ORIGIN, self.size()?));
|
||||
if area != self.last_known_area {
|
||||
self.resize(area)?;
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Draws a single frame to the terminal.
|
||||
///
|
||||
/// Returns a [`CompletedFrame`] if successful, otherwise a [`std::io::Error`].
|
||||
///
|
||||
/// If the render callback passed to this method can fail, use [`try_draw`] instead.
|
||||
///
|
||||
/// Applications should call `draw` or [`try_draw`] in a loop to continuously render the
|
||||
/// terminal. These methods are the main entry points for drawing to the terminal.
|
||||
///
|
||||
/// [`try_draw`]: Terminal::try_draw
|
||||
///
|
||||
/// This method will:
|
||||
///
|
||||
/// - autoresize the terminal if necessary
|
||||
/// - call the render callback, passing it a [`Frame`] reference to render to
|
||||
/// - flush the current internal state by copying the current buffer to the backend
|
||||
/// - move the cursor to the last known position if it was set during the rendering closure
|
||||
/// - return a [`CompletedFrame`] with the current buffer and the area of the terminal
|
||||
///
|
||||
/// The [`CompletedFrame`] returned by this method can be useful for debugging or testing
|
||||
/// purposes, but it is often not used in regular applicationss.
|
||||
///
|
||||
/// The render callback should fully render the entire frame when called, including areas that
|
||||
/// are unchanged from the previous frame. This is because each frame is compared to the
|
||||
/// previous frame to determine what has changed, and only the changes are written to the
|
||||
/// terminal. If the render callback does not fully render the frame, the terminal will not be
|
||||
/// in a consistent state.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::layout::Position;
|
||||
/// # let backend = ratatui::backend::TestBackend::new(10, 10);
|
||||
/// # let mut terminal = ratatui::Terminal::new(backend)?;
|
||||
/// use ratatui::widgets::Paragraph;
|
||||
///
|
||||
/// // with a closure
|
||||
/// terminal.draw(|frame| {
|
||||
/// let area = frame.area();
|
||||
/// frame.render_widget(Paragraph::new("Hello World!"), area);
|
||||
/// frame.set_cursor_position(Position { x: 0, y: 0 });
|
||||
/// })?;
|
||||
///
|
||||
/// // or with a function
|
||||
/// terminal.draw(render)?;
|
||||
///
|
||||
/// fn render(frame: &mut ratatui::Frame) {
|
||||
/// frame.render_widget(Paragraph::new("Hello World!"), frame.area());
|
||||
/// }
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
pub fn draw<F>(&mut self, render_callback: F) -> io::Result<CompletedFrame>
|
||||
where
|
||||
F: FnOnce(&mut Frame),
|
||||
{
|
||||
self.try_draw(|frame| {
|
||||
render_callback(frame);
|
||||
io::Result::Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// Tries to draw a single frame to the terminal.
|
||||
///
|
||||
/// Returns [`Result::Ok`] containing a [`CompletedFrame`] if successful, otherwise
|
||||
/// [`Result::Err`] containing the [`std::io::Error`] that caused the failure.
|
||||
///
|
||||
/// This is the equivalent of [`Terminal::draw`] but the render callback is a function or
|
||||
/// closure that returns a `Result` instead of nothing.
|
||||
///
|
||||
/// Applications should call `try_draw` or [`draw`] in a loop to continuously render the
|
||||
/// terminal. These methods are the main entry points for drawing to the terminal.
|
||||
///
|
||||
/// [`draw`]: Terminal::draw
|
||||
///
|
||||
/// This method will:
|
||||
///
|
||||
/// - autoresize the terminal if necessary
|
||||
/// - call the render callback, passing it a [`Frame`] reference to render to
|
||||
/// - flush the current internal state by copying the current buffer to the backend
|
||||
/// - move the cursor to the last known position if it was set during the rendering closure
|
||||
/// - return a [`CompletedFrame`] with the current buffer and the area of the terminal
|
||||
///
|
||||
/// The render callback passed to `try_draw` can return any [`Result`] with an error type that
|
||||
/// can be converted into an [`std::io::Error`] using the [`Into`] trait. This makes it possible
|
||||
/// to use the `?` operator to propagate errors that occur during rendering. If the render
|
||||
/// callback returns an error, the error will be returned from `try_draw` as an
|
||||
/// [`std::io::Error`] and the terminal will not be updated.
|
||||
///
|
||||
/// The [`CompletedFrame`] returned by this method can be useful for debugging or testing
|
||||
/// purposes, but it is often not used in regular applicationss.
|
||||
///
|
||||
/// The render callback should fully render the entire frame when called, including areas that
|
||||
/// are unchanged from the previous frame. This is because each frame is compared to the
|
||||
/// previous frame to determine what has changed, and only the changes are written to the
|
||||
/// terminal. If the render function does not fully render the frame, the terminal will not be
|
||||
/// in a consistent state.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```should_panic
|
||||
/// # use ratatui::layout::Position;;
|
||||
/// # let backend = ratatui::backend::TestBackend::new(10, 10);
|
||||
/// # let mut terminal = ratatui::Terminal::new(backend)?;
|
||||
/// use std::io;
|
||||
///
|
||||
/// use ratatui::widgets::Paragraph;
|
||||
///
|
||||
/// // with a closure
|
||||
/// terminal.try_draw(|frame| {
|
||||
/// let value: u8 = "not a number".parse().map_err(io::Error::other)?;
|
||||
/// let area = frame.area();
|
||||
/// frame.render_widget(Paragraph::new("Hello World!"), area);
|
||||
/// frame.set_cursor_position(Position { x: 0, y: 0 });
|
||||
/// io::Result::Ok(())
|
||||
/// })?;
|
||||
///
|
||||
/// // or with a function
|
||||
/// terminal.try_draw(render)?;
|
||||
///
|
||||
/// fn render(frame: &mut ratatui::Frame) -> io::Result<()> {
|
||||
/// let value: u8 = "not a number".parse().map_err(io::Error::other)?;
|
||||
/// frame.render_widget(Paragraph::new("Hello World!"), frame.area());
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// # io::Result::Ok(())
|
||||
/// ```
|
||||
pub fn try_draw<F, E>(&mut self, render_callback: F) -> io::Result<CompletedFrame>
|
||||
where
|
||||
F: FnOnce(&mut Frame) -> Result<(), E>,
|
||||
E: Into<io::Error>,
|
||||
{
|
||||
// Autoresize - otherwise we get glitches if shrinking or potential desync between widgets
|
||||
// and the terminal (if growing), which may OOB.
|
||||
self.autoresize()?;
|
||||
|
||||
let mut frame = self.get_frame();
|
||||
|
||||
render_callback(&mut frame).map_err(Into::into)?;
|
||||
|
||||
// We can't change the cursor position right away because we have to flush the frame to
|
||||
// stdout first. But we also can't keep the frame around, since it holds a &mut to
|
||||
// Buffer. Thus, we're taking the important data out of the Frame and dropping it.
|
||||
let cursor_position = frame.cursor_position;
|
||||
|
||||
// Draw to stdout
|
||||
self.flush()?;
|
||||
|
||||
match cursor_position {
|
||||
None => self.hide_cursor()?,
|
||||
Some(position) => {
|
||||
self.show_cursor()?;
|
||||
self.set_cursor_position(position)?;
|
||||
}
|
||||
}
|
||||
|
||||
self.swap_buffers();
|
||||
|
||||
// Flush
|
||||
self.backend.flush()?;
|
||||
|
||||
let completed_frame = CompletedFrame {
|
||||
buffer: &self.buffers[1 - self.current],
|
||||
area: self.last_known_area,
|
||||
count: self.frame_count,
|
||||
};
|
||||
|
||||
// increment frame count before returning from draw
|
||||
self.frame_count = self.frame_count.wrapping_add(1);
|
||||
|
||||
Ok(completed_frame)
|
||||
}
|
||||
|
||||
/// Hides the cursor.
|
||||
pub fn hide_cursor(&mut self) -> io::Result<()> {
|
||||
self.backend.hide_cursor()?;
|
||||
self.hidden_cursor = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Shows the cursor.
|
||||
pub fn show_cursor(&mut self) -> io::Result<()> {
|
||||
self.backend.show_cursor()?;
|
||||
self.hidden_cursor = false;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gets the current cursor position.
|
||||
///
|
||||
/// This is the position of the cursor after the last draw call and is returned as a tuple of
|
||||
/// `(x, y)` coordinates.
|
||||
#[deprecated = "the method get_cursor_position indicates more clearly what about the cursor to get"]
|
||||
pub fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
|
||||
let Position { x, y } = self.get_cursor_position()?;
|
||||
Ok((x, y))
|
||||
}
|
||||
|
||||
/// Sets the cursor position.
|
||||
#[deprecated = "the method set_cursor_position indicates more clearly what about the cursor to set"]
|
||||
pub fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
||||
self.set_cursor_position(Position { x, y })
|
||||
}
|
||||
|
||||
/// Gets the current cursor position.
|
||||
///
|
||||
/// This is the position of the cursor after the last draw call.
|
||||
pub fn get_cursor_position(&mut self) -> io::Result<Position> {
|
||||
self.backend.get_cursor_position()
|
||||
}
|
||||
|
||||
/// Sets the cursor position.
|
||||
pub fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()> {
|
||||
let position = position.into();
|
||||
self.backend.set_cursor_position(position)?;
|
||||
self.last_known_cursor_pos = position;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Clear the terminal and force a full redraw on the next draw call.
|
||||
pub fn clear(&mut self) -> io::Result<()> {
|
||||
match self.viewport {
|
||||
Viewport::Fullscreen => self.backend.clear_region(ClearType::All)?,
|
||||
Viewport::Inline(_) => {
|
||||
self.backend
|
||||
.set_cursor_position(self.viewport_area.as_position())?;
|
||||
self.backend.clear_region(ClearType::AfterCursor)?;
|
||||
}
|
||||
Viewport::Fixed(area) => {
|
||||
for y in area.top()..area.bottom() {
|
||||
self.backend.set_cursor_position(Position { x: 0, y })?;
|
||||
self.backend.clear_region(ClearType::AfterCursor)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Reset the back buffer to make sure the next update will redraw everything.
|
||||
self.buffers[1 - self.current].reset();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Clears the inactive buffer and swaps it with the current buffer
|
||||
pub fn swap_buffers(&mut self) {
|
||||
self.buffers[1 - self.current].reset();
|
||||
self.current = 1 - self.current;
|
||||
}
|
||||
|
||||
/// Queries the real size of the backend.
|
||||
pub fn size(&self) -> io::Result<Size> {
|
||||
self.backend.size()
|
||||
}
|
||||
|
||||
/// Insert some content before the current inline viewport. This has no effect when the
|
||||
/// viewport is not inline.
|
||||
///
|
||||
/// The `draw_fn` closure will be called to draw into a writable `Buffer` that is `height`
|
||||
/// lines tall. The content of that `Buffer` will then be inserted before the viewport.
|
||||
///
|
||||
/// If the viewport isn't yet at the bottom of the screen, inserted lines will push it towards
|
||||
/// the bottom. Once the viewport is at the bottom of the screen, inserted lines will scroll
|
||||
/// the area of the screen above the viewport upwards.
|
||||
///
|
||||
/// Before:
|
||||
/// ```ignore
|
||||
/// +---------------------+
|
||||
/// | pre-existing line 1 |
|
||||
/// | pre-existing line 2 |
|
||||
/// +---------------------+
|
||||
/// | viewport |
|
||||
/// +---------------------+
|
||||
/// | |
|
||||
/// | |
|
||||
/// +---------------------+
|
||||
/// ```
|
||||
///
|
||||
/// After inserting 2 lines:
|
||||
/// ```ignore
|
||||
/// +---------------------+
|
||||
/// | pre-existing line 1 |
|
||||
/// | pre-existing line 2 |
|
||||
/// | inserted line 1 |
|
||||
/// | inserted line 2 |
|
||||
/// +---------------------+
|
||||
/// | viewport |
|
||||
/// +---------------------+
|
||||
/// +---------------------+
|
||||
/// ```
|
||||
///
|
||||
/// After inserting 2 more lines:
|
||||
/// ```ignore
|
||||
/// +---------------------+
|
||||
/// | pre-existing line 2 |
|
||||
/// | inserted line 1 |
|
||||
/// | inserted line 2 |
|
||||
/// | inserted line 3 |
|
||||
/// | inserted line 4 |
|
||||
/// +---------------------+
|
||||
/// | viewport |
|
||||
/// +---------------------+
|
||||
/// ```
|
||||
///
|
||||
/// If more lines are inserted than there is space on the screen, then the top lines will go
|
||||
/// directly into the terminal's scrollback buffer. At the limit, if the viewport takes up the
|
||||
/// whole screen, all lines will be inserted directly into the scrollback buffer.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ## Insert a single line before the current viewport
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{backend::TestBackend, prelude::*, widgets::*};
|
||||
/// # let backend = TestBackend::new(10, 10);
|
||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||
/// terminal.insert_before(1, |buf| {
|
||||
/// Paragraph::new(Line::from(vec![
|
||||
/// Span::raw("This line will be added "),
|
||||
/// Span::styled("before", Style::default().fg(Color::Blue)),
|
||||
/// Span::raw(" the current viewport"),
|
||||
/// ]))
|
||||
/// .render(buf.area, buf);
|
||||
/// });
|
||||
/// ```
|
||||
pub fn insert_before<F>(&mut self, height: u16, draw_fn: F) -> io::Result<()>
|
||||
where
|
||||
F: FnOnce(&mut Buffer),
|
||||
{
|
||||
if !matches!(self.viewport, Viewport::Inline(_)) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// The approach of this function is to first render all of the lines to insert into a
|
||||
// temporary buffer, and then to loop drawing chunks from the buffer to the screen. drawing
|
||||
// this buffer onto the screen.
|
||||
let area = Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: self.viewport_area.width,
|
||||
height,
|
||||
};
|
||||
let mut buffer = Buffer::empty(area);
|
||||
draw_fn(&mut buffer);
|
||||
let mut buffer = buffer.content.as_slice();
|
||||
|
||||
// Use i32 variables so we don't have worry about overflowed u16s when adding, or about
|
||||
// negative results when subtracting.
|
||||
let mut drawn_height: i32 = self.viewport_area.top().into();
|
||||
let mut buffer_height: i32 = height.into();
|
||||
let viewport_height: i32 = self.viewport_area.height.into();
|
||||
let screen_height: i32 = self.last_known_area.height.into();
|
||||
|
||||
// The algorithm here is to loop, drawing large chunks of text (up to a screen-full at a
|
||||
// time), until the remainder of the buffer plus the viewport fits on the screen. We choose
|
||||
// this loop condition because it guarantees that we can write the remainder of the buffer
|
||||
// with just one call to Self::draw_lines().
|
||||
while buffer_height + viewport_height > screen_height {
|
||||
// We will draw as much of the buffer as possible on this iteration in order to make
|
||||
// forward progress. So we have:
|
||||
//
|
||||
// to_draw = min(buffer_height, screen_height)
|
||||
//
|
||||
// We may need to scroll the screen up to make room to draw. We choose the minimal
|
||||
// possible scroll amount so we don't end up with the viewport sitting in the middle of
|
||||
// the screen when this function is done. The amount to scroll by is:
|
||||
//
|
||||
// scroll_up = max(0, drawn_height + to_draw - screen_height)
|
||||
//
|
||||
// We want `scroll_up` to be enough so that, after drawing, we have used the whole
|
||||
// screen (drawn_height - scroll_up + to_draw = screen_height). However, there might
|
||||
// already be enough room on the screen to draw without scrolling (drawn_height +
|
||||
// to_draw <= screen_height). In this case, we just don't scroll at all.
|
||||
let to_draw = buffer_height.min(screen_height);
|
||||
let scroll_up = 0.max(drawn_height + to_draw - screen_height);
|
||||
self.scroll_up(scroll_up as u16)?;
|
||||
buffer = self.draw_lines((drawn_height - scroll_up) as u16, to_draw as u16, buffer)?;
|
||||
drawn_height += to_draw - scroll_up;
|
||||
buffer_height -= to_draw;
|
||||
}
|
||||
|
||||
// There is now enough room on the screen for the remaining buffer plus the viewport,
|
||||
// though we may still need to scroll up some of the existing text first. It's possible
|
||||
// that by this point we've drained the buffer, but we may still need to scroll up to make
|
||||
// room for the viewport.
|
||||
//
|
||||
// We want to scroll up the exact amount that will leave us completely filling the screen.
|
||||
// However, it's possible that the viewport didn't start on the bottom of the screen and
|
||||
// the added lines weren't enough to push it all the way to the bottom. We deal with this
|
||||
// case by just ensuring that our scroll amount is non-negative.
|
||||
//
|
||||
// We want:
|
||||
// screen_height = drawn_height - scroll_up + buffer_height + viewport_height
|
||||
// Or, equivalently:
|
||||
// scroll_up = drawn_height + buffer_height + viewport_height - screen_height
|
||||
let scroll_up = 0.max(drawn_height + buffer_height + viewport_height - screen_height);
|
||||
self.scroll_up(scroll_up as u16)?;
|
||||
self.draw_lines(
|
||||
(drawn_height - scroll_up) as u16,
|
||||
buffer_height as u16,
|
||||
buffer,
|
||||
)?;
|
||||
drawn_height += buffer_height - scroll_up;
|
||||
|
||||
self.set_viewport_area(Rect {
|
||||
y: drawn_height as u16,
|
||||
..self.viewport_area
|
||||
});
|
||||
|
||||
// Clear the viewport off the screen. We didn't clear earlier for two reasons. First, it
|
||||
// wasn't necessary because the buffer we drew out of isn't sparse, so it overwrote
|
||||
// whatever was on the screen. Second, there is a weird bug with tmux where a full screen
|
||||
// clear plus immediate scrolling causes some garbage to go into the scrollback.
|
||||
self.clear()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Draw lines at the given vertical offset. The slice of cells must contain enough cells
|
||||
/// for the requested lines. A slice of the unused cells are returned.
|
||||
fn draw_lines<'a>(
|
||||
&mut self,
|
||||
y_offset: u16,
|
||||
lines_to_draw: u16,
|
||||
cells: &'a [Cell],
|
||||
) -> io::Result<&'a [Cell]> {
|
||||
let width: usize = self.last_known_area.width.into();
|
||||
let (to_draw, remainder) = cells.split_at(width * lines_to_draw as usize);
|
||||
if lines_to_draw > 0 {
|
||||
let iter = to_draw
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, c)| ((i % width) as u16, y_offset + (i / width) as u16, c));
|
||||
self.backend.draw(iter)?;
|
||||
self.backend.flush()?;
|
||||
}
|
||||
Ok(remainder)
|
||||
}
|
||||
|
||||
/// Scroll the whole screen up by the given number of lines.
|
||||
fn scroll_up(&mut self, lines_to_scroll: u16) -> io::Result<()> {
|
||||
if lines_to_scroll > 0 {
|
||||
self.set_cursor_position(Position::new(
|
||||
0,
|
||||
self.last_known_area.height.saturating_sub(1),
|
||||
))?;
|
||||
self.backend.append_lines(lines_to_scroll)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_inline_size<B: Backend>(
|
||||
backend: &mut B,
|
||||
height: u16,
|
||||
size: Size,
|
||||
offset_in_previous_viewport: u16,
|
||||
) -> io::Result<(Rect, Position)> {
|
||||
let pos = backend.get_cursor_position()?;
|
||||
let mut row = pos.y;
|
||||
|
||||
let max_height = size.height.min(height);
|
||||
|
||||
let lines_after_cursor = height
|
||||
.saturating_sub(offset_in_previous_viewport)
|
||||
.saturating_sub(1);
|
||||
|
||||
backend.append_lines(lines_after_cursor)?;
|
||||
|
||||
let available_lines = size.height.saturating_sub(row).saturating_sub(1);
|
||||
let missing_lines = lines_after_cursor.saturating_sub(available_lines);
|
||||
if missing_lines > 0 {
|
||||
row = row.saturating_sub(missing_lines);
|
||||
}
|
||||
row = row.saturating_sub(offset_in_previous_viewport);
|
||||
|
||||
Ok((
|
||||
Rect {
|
||||
x: 0,
|
||||
y: row,
|
||||
width: size.width,
|
||||
height: max_height,
|
||||
},
|
||||
pos,
|
||||
))
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
//! - 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`].
|
||||
//! [`Text`].
|
||||
//!
|
||||
//! These types form a hierarchy: [`Line`] is a collection of [`Span`] and each line of [`Text`]
|
||||
//! is a [`Line`].
|
||||
@@ -48,14 +48,13 @@ mod grapheme;
|
||||
pub use grapheme::StyledGrapheme;
|
||||
|
||||
mod line;
|
||||
pub use line::Line;
|
||||
pub use line::{Line, ToLine};
|
||||
|
||||
mod masked;
|
||||
pub use masked::Masked;
|
||||
|
||||
mod span;
|
||||
pub use span::Span;
|
||||
pub use span::{Span, ToSpan};
|
||||
|
||||
#[allow(clippy::module_inception)]
|
||||
mod text;
|
||||
pub use text::Text;
|
||||
pub use text::{Text, ToText};
|
||||
@@ -1,4 +1,7 @@
|
||||
use crate::prelude::*;
|
||||
use crate::{prelude::*, style::Styled};
|
||||
|
||||
const NBSP: &str = "\u{00a0}";
|
||||
const ZWSP: &str = "\u{200b}";
|
||||
|
||||
/// A grapheme associated to a style.
|
||||
/// Note that, although `StyledGrapheme` is the smallest divisible unit of text,
|
||||
@@ -22,6 +25,11 @@ impl<'a> StyledGrapheme<'a> {
|
||||
style: style.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_whitespace(&self) -> bool {
|
||||
let symbol = self.symbol;
|
||||
symbol == ZWSP || symbol.chars().all(char::is_whitespace) && symbol != NBSP
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Styled for StyledGrapheme<'a> {
|
||||
@@ -4,8 +4,7 @@ use std::{borrow::Cow, fmt};
|
||||
|
||||
use unicode_truncate::UnicodeTruncateStr;
|
||||
|
||||
use super::StyledGrapheme;
|
||||
use crate::prelude::*;
|
||||
use crate::{prelude::*, style::Styled, text::StyledGrapheme};
|
||||
|
||||
/// A line of text, consisting of one or more [`Span`]s.
|
||||
///
|
||||
@@ -13,6 +12,9 @@ use crate::prelude::*;
|
||||
/// text. When a [`Line`] is rendered, it is rendered as a single line of text, with each [`Span`]
|
||||
/// being rendered in order (left to right).
|
||||
///
|
||||
/// Any newlines in the content are removed when creating a [`Line`] using the constructor or
|
||||
/// conversion methods.
|
||||
///
|
||||
/// # Constructor Methods
|
||||
///
|
||||
/// - [`Line::default`] creates a line with empty content and the default style.
|
||||
@@ -147,16 +149,40 @@ use crate::prelude::*;
|
||||
/// ```
|
||||
///
|
||||
/// [`Paragraph`]: crate::widgets::Paragraph
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
#[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.style == Style::default() && self.alignment.is_none() {
|
||||
f.write_str("Line ")?;
|
||||
return f.debug_list().entries(&self.spans).finish();
|
||||
}
|
||||
let mut debug = f.debug_struct("Line");
|
||||
if self.style != Style::default() {
|
||||
debug.field("style", &self.style);
|
||||
}
|
||||
if let Some(alignment) = self.alignment {
|
||||
debug.field("alignment", &format!("Alignment::{alignment}"));
|
||||
}
|
||||
debug.field("spans", &self.spans).finish()
|
||||
}
|
||||
}
|
||||
|
||||
fn cow_to_spans<'a>(content: impl Into<Cow<'a, str>>) -> Vec<Span<'a>> {
|
||||
match content.into() {
|
||||
Cow::Borrowed(s) => s.lines().map(Span::raw).collect(),
|
||||
Cow::Owned(s) => s.lines().map(|v| Span::raw(v.to_string())).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Line<'a> {
|
||||
@@ -184,17 +210,14 @@ impl<'a> Line<'a> {
|
||||
T: Into<Cow<'a, str>>,
|
||||
{
|
||||
Self {
|
||||
spans: content
|
||||
.into()
|
||||
.lines()
|
||||
.map(|v| Span::raw(v.to_string()))
|
||||
.collect(),
|
||||
spans: cow_to_spans(content),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a line with the given style.
|
||||
// `content` can be any type that is convertible to [`Cow<str>`] (e.g. [`&str`], [`String`],
|
||||
///
|
||||
/// `content` can be any type that is convertible to [`Cow<str>`] (e.g. [`&str`], [`String`],
|
||||
/// [`Cow<str>`], or your own type that implements [`Into<Cow<str>>`]).
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
@@ -218,11 +241,7 @@ impl<'a> Line<'a> {
|
||||
S: Into<Style>,
|
||||
{
|
||||
Self {
|
||||
spans: content
|
||||
.into()
|
||||
.lines()
|
||||
.map(|v| Span::raw(v.to_string()))
|
||||
.collect(),
|
||||
spans: cow_to_spans(content),
|
||||
style: style.into(),
|
||||
..Default::default()
|
||||
}
|
||||
@@ -503,13 +522,13 @@ impl<'a> IntoIterator for &'a mut Line<'a> {
|
||||
|
||||
impl<'a> From<String> for Line<'a> {
|
||||
fn from(s: String) -> Self {
|
||||
Self::from(vec![Span::from(s)])
|
||||
Self::raw(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for Line<'a> {
|
||||
fn from(s: &'a str) -> Self {
|
||||
Self::from(vec![Span::from(s)])
|
||||
Self::raw(s)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -546,6 +565,37 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a `Span` to a `Line`, returning a new `Line` with the `Span` added.
|
||||
impl<'a> std::ops::Add<Span<'a>> for Line<'a> {
|
||||
type Output = Self;
|
||||
|
||||
fn add(mut self, rhs: Span<'a>) -> Self::Output {
|
||||
self.spans.push(rhs);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds two `Line`s together, returning a new `Text` with the contents of the two `Line`s.
|
||||
impl<'a> std::ops::Add<Self> for Line<'a> {
|
||||
type Output = Text<'a>;
|
||||
|
||||
fn add(self, rhs: Self) -> Self::Output {
|
||||
Text::from(vec![self, rhs])
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> std::ops::AddAssign<Span<'a>> for Line<'a> {
|
||||
fn add_assign(&mut self, rhs: Span<'a>) {
|
||||
self.spans.push(rhs);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Extend<Span<'a>> for Line<'a> {
|
||||
fn extend<T: IntoIterator<Item = Span<'a>>>(&mut self, iter: T) {
|
||||
self.spans.extend(iter);
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Line<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.render_ref(area, buf);
|
||||
@@ -558,6 +608,7 @@ impl WidgetRef for Line<'_> {
|
||||
if area.is_empty() {
|
||||
return;
|
||||
}
|
||||
let area = Rect { height: 1, ..area };
|
||||
let line_width = self.width();
|
||||
if line_width == 0 {
|
||||
return;
|
||||
@@ -647,6 +698,29 @@ fn spans_after_width<'a>(
|
||||
})
|
||||
}
|
||||
|
||||
/// A trait for converting a value to a [`Line`].
|
||||
///
|
||||
/// This trait is automatically implemented for any type that implements the [`Display`] trait. As
|
||||
/// such, `ToLine` shouln't be implemented directly: [`Display`] should be implemented instead, and
|
||||
/// you get the `ToLine` implementation for free.
|
||||
///
|
||||
/// [`Display`]: std::fmt::Display
|
||||
pub trait ToLine {
|
||||
/// Converts the value to a [`Line`].
|
||||
fn to_line(&self) -> Line<'_>;
|
||||
}
|
||||
|
||||
/// # Panics
|
||||
///
|
||||
/// In this implementation, the `to_line` method panics if the `Display` implementation returns an
|
||||
/// error. This indicates an incorrect `Display` implementation since `fmt::Write for String` never
|
||||
/// returns an error itself.
|
||||
impl<T: fmt::Display> ToLine for T {
|
||||
fn to_line(&self) -> Line<'_> {
|
||||
Line::from(self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Line<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
for span in &self.spans {
|
||||
@@ -684,11 +758,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);
|
||||
}
|
||||
|
||||
@@ -697,7 +771,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);
|
||||
}
|
||||
|
||||
@@ -706,7 +780,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);
|
||||
}
|
||||
|
||||
@@ -715,7 +789,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);
|
||||
}
|
||||
|
||||
@@ -804,14 +878,28 @@ mod tests {
|
||||
fn from_string() {
|
||||
let s = String::from("Hello, world!");
|
||||
let line = Line::from(s);
|
||||
assert_eq!(vec![Span::from("Hello, world!")], line.spans);
|
||||
assert_eq!(line.spans, [Span::from("Hello, world!")]);
|
||||
|
||||
let s = String::from("Hello\nworld!");
|
||||
let line = Line::from(s);
|
||||
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!(vec![Span::from("Hello, world!")], line.spans);
|
||||
assert_eq!(line.spans, [Span::from("Hello, world!")]);
|
||||
|
||||
let s = "Hello\nworld!";
|
||||
let line = Line::from(s);
|
||||
assert_eq!(line.spans, [Span::from("Hello"), Span::from("world!")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_line() {
|
||||
let line = 42.to_line();
|
||||
assert_eq!(line.spans, [Span::from("42")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -821,7 +909,7 @@ mod tests {
|
||||
Span::styled(" world!", Style::default().fg(Color::Green)),
|
||||
];
|
||||
let line = Line::from(spans.clone());
|
||||
assert_eq!(spans, line.spans);
|
||||
assert_eq!(line.spans, spans);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -854,7 +942,63 @@ mod tests {
|
||||
fn from_span() {
|
||||
let span = Span::styled("Hello, world!", Style::default().fg(Color::Yellow));
|
||||
let line = Line::from(span.clone());
|
||||
assert_eq!(vec![span], line.spans);
|
||||
assert_eq!(line.spans, [span]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_span() {
|
||||
assert_eq!(
|
||||
Line::raw("Red").red() + Span::raw("blue").blue(),
|
||||
Line {
|
||||
spans: vec![Span::raw("Red"), Span::raw("blue").blue()],
|
||||
style: Style::new().red(),
|
||||
alignment: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_line() {
|
||||
assert_eq!(
|
||||
Line::raw("Red").red() + Line::raw("Blue").blue(),
|
||||
Text {
|
||||
lines: vec![Line::raw("Red").red(), Line::raw("Blue").blue()],
|
||||
style: Style::default(),
|
||||
alignment: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_assign_span() {
|
||||
let mut line = Line::raw("Red").red();
|
||||
line += Span::raw("Blue").blue();
|
||||
assert_eq!(
|
||||
line,
|
||||
Line {
|
||||
spans: vec![Span::raw("Red"), Span::raw("Blue").blue()],
|
||||
style: Style::new().red(),
|
||||
alignment: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extend() {
|
||||
let mut line = Line::from("Hello, ");
|
||||
line.extend([Span::raw("world!")]);
|
||||
assert_eq!(line.spans, [Span::raw("Hello, "), Span::raw("world!")]);
|
||||
|
||||
let mut line = Line::from("Hello, ");
|
||||
line.extend([Span::raw("world! "), Span::raw("How are you?")]);
|
||||
assert_eq!(
|
||||
line.spans,
|
||||
[
|
||||
Span::raw("Hello, "),
|
||||
Span::raw("world! "),
|
||||
Span::raw("How are you?")
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -864,7 +1008,7 @@ mod tests {
|
||||
Span::styled(" world!", Style::default().fg(Color::Green)),
|
||||
]);
|
||||
let s: String = line.into();
|
||||
assert_eq!("Hello, world!", s);
|
||||
assert_eq!(s, "Hello, world!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -997,6 +1141,17 @@ mod tests {
|
||||
assert_eq!(buf, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_only_styles_first_line() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 2));
|
||||
hello_world().render(buf.area, &mut buf);
|
||||
let mut expected = Buffer::with_lines(["Hello world! ", " "]);
|
||||
expected.set_style(Rect::new(0, 0, 20, 1), ITALIC);
|
||||
expected.set_style(Rect::new(0, 0, 6, 1), BLUE);
|
||||
expected.set_style(Rect::new(6, 0, 6, 1), GREEN);
|
||||
assert_eq!(buf, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_truncates() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 1));
|
||||
@@ -1055,7 +1210,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() {
|
||||
@@ -1071,7 +1226,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() {
|
||||
@@ -1082,7 +1237,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")]
|
||||
@@ -1104,7 +1259,7 @@ mod tests {
|
||||
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
|
||||
@@ -1169,7 +1324,7 @@ mod tests {
|
||||
fn render_truncates_away_from_0x0(#[case] alignment: Alignment, #[case] expected: &str) {
|
||||
let line = Line::from(vec![Span::raw("a🦀b"), Span::raw("c🦀d")]).alignment(alignment);
|
||||
// Fill buffer with stuff to ensure the output is indeed padded
|
||||
let mut buf = Buffer::filled(Rect::new(0, 0, 10, 1), Cell::default().set_symbol("X"));
|
||||
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);
|
||||
assert_eq!(buf, Buffer::with_lines([expected]));
|
||||
@@ -1188,12 +1343,12 @@ mod tests {
|
||||
let line = Line::from(vec![Span::raw("a🦀b"), Span::raw("c🦀d")]).right_aligned();
|
||||
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::default().set_symbol("X"));
|
||||
let mut buf = Buffer::filled(area, Cell::new("X"));
|
||||
line.render_ref(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
|
||||
@@ -1207,7 +1362,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, " ")]
|
||||
@@ -1271,6 +1426,13 @@ mod tests {
|
||||
line.render_ref(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines([expected]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_with_newlines() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 11, 1));
|
||||
Line::from("Hello\nworld!").render(Rect::new(0, 0, 11, 1), &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["Helloworld!"]));
|
||||
}
|
||||
}
|
||||
|
||||
mod iterators {
|
||||
@@ -125,10 +125,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,8 +3,7 @@ use std::{borrow::Cow, fmt};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use super::StyledGrapheme;
|
||||
use crate::prelude::*;
|
||||
use crate::{prelude::*, style::Styled, text::StyledGrapheme};
|
||||
|
||||
/// Represents a part of a line that is contiguous and where all characters share the same style.
|
||||
///
|
||||
@@ -82,19 +81,31 @@ use crate::prelude::*;
|
||||
/// use ratatui::prelude::*;
|
||||
///
|
||||
/// # 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.style == Style::default() {
|
||||
return write!(f, "Span({:?})", self.content);
|
||||
}
|
||||
f.debug_struct("Span")
|
||||
.field("style", &self.style)
|
||||
.field("content", &self.content)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Span<'a> {
|
||||
@@ -343,6 +354,14 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> std::ops::Add<Self> for Span<'a> {
|
||||
type Output = Line<'a>;
|
||||
|
||||
fn add(self, rhs: Self) -> Self::Output {
|
||||
Line::from_iter([self, rhs])
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Styled for Span<'a> {
|
||||
type Item = Self;
|
||||
|
||||
@@ -364,45 +383,88 @@ impl Widget for Span<'_> {
|
||||
impl WidgetRef for Span<'_> {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
let area = area.intersection(buf.area);
|
||||
let Rect {
|
||||
x: mut current_x,
|
||||
y,
|
||||
width,
|
||||
..
|
||||
} = area;
|
||||
let max_x = Ord::min(current_x.saturating_add(width), buf.area.right());
|
||||
for g in self.styled_graphemes(Style::default()) {
|
||||
let symbol_width = g.symbol.width();
|
||||
let next_x = current_x.saturating_add(symbol_width as u16);
|
||||
if next_x > max_x {
|
||||
if area.is_empty() {
|
||||
return;
|
||||
}
|
||||
let Rect { mut x, y, .. } = area;
|
||||
for (i, grapheme) in self.styled_graphemes(Style::default()).enumerate() {
|
||||
let symbol_width = grapheme.symbol.width();
|
||||
let next_x = x.saturating_add(symbol_width as u16);
|
||||
if next_x > area.right() {
|
||||
break;
|
||||
}
|
||||
buf.get_mut(current_x, y)
|
||||
.set_symbol(g.symbol)
|
||||
.set_style(g.style);
|
||||
|
||||
if i == 0 {
|
||||
// the first grapheme is always set on the cell
|
||||
buf[(x, y)]
|
||||
.set_symbol(grapheme.symbol)
|
||||
.set_style(grapheme.style);
|
||||
} else if x == area.x {
|
||||
// there is one or more zero-width graphemes in the first cell, so the first cell
|
||||
// must be appended to.
|
||||
buf[(x, y)]
|
||||
.append_symbol(grapheme.symbol)
|
||||
.set_style(grapheme.style);
|
||||
} else if symbol_width == 0 {
|
||||
// append zero-width graphemes to the previous cell
|
||||
buf[(x - 1, y)]
|
||||
.append_symbol(grapheme.symbol)
|
||||
.set_style(grapheme.style);
|
||||
} else {
|
||||
// just a normal grapheme (not first, not zero-width, not overflowing the area)
|
||||
buf[(x, y)]
|
||||
.set_symbol(grapheme.symbol)
|
||||
.set_style(grapheme.style);
|
||||
}
|
||||
|
||||
// multi-width graphemes must clear the cells of characters that are hidden by the
|
||||
// grapheme, otherwise the hidden characters will be re-rendered if the grapheme is
|
||||
// overwritten.
|
||||
for i in (current_x + 1)..next_x {
|
||||
buf.get_mut(i, y).reset();
|
||||
for x_hidden in (x + 1)..next_x {
|
||||
// it may seem odd that the style of the hidden cells are not set to the style of
|
||||
// the grapheme, but this is how the existing buffer.set_span() method works.
|
||||
// buf.get_mut(i, y).set_style(g.style);
|
||||
buf[(x_hidden, y)].reset();
|
||||
}
|
||||
current_x = next_x;
|
||||
x = next_x;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for converting a value to a [`Span`].
|
||||
///
|
||||
/// This trait is automatically implemented for any type that implements the [`Display`] trait. As
|
||||
/// such, `ToSpan` shouln't be implemented directly: [`Display`] should be implemented instead, and
|
||||
/// you get the `ToSpan` implementation for free.
|
||||
///
|
||||
/// [`Display`]: std::fmt::Display
|
||||
pub trait ToSpan {
|
||||
/// Converts the value to a [`Span`].
|
||||
fn to_span(&self) -> Span<'_>;
|
||||
}
|
||||
|
||||
/// # Panics
|
||||
///
|
||||
/// In this implementation, the `to_span` method panics if the `Display` implementation returns an
|
||||
/// error. This indicates an incorrect `Display` implementation since `fmt::Write for String` never
|
||||
/// returns an error itself.
|
||||
impl<T: fmt::Display> ToSpan for T {
|
||||
fn to_span(&self) -> Span<'_> {
|
||||
Span::raw(self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Span<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
fmt::Display::fmt(&self.content, f)
|
||||
for line in self.content.lines() {
|
||||
fmt::Display::fmt(line, f)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use buffer::Cell;
|
||||
use rstest::fixture;
|
||||
|
||||
use super::*;
|
||||
@@ -495,6 +557,12 @@ mod tests {
|
||||
assert_eq!(span.style, Style::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_span() {
|
||||
assert_eq!(42.to_span(), Span::raw("42"));
|
||||
assert_eq!("test".to_span(), Span::raw("test"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_style() {
|
||||
let span = Span::styled("test content", Style::new().green()).reset_style();
|
||||
@@ -513,6 +581,8 @@ 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/ratatui/issues/1271
|
||||
assert_eq!(Span::raw("test\ncontent").width(), 12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -526,6 +596,7 @@ mod tests {
|
||||
assert_eq!(stylized.content, Cow::Borrowed("test content"));
|
||||
assert_eq!(stylized.style, Style::new().green().on_yellow().bold());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_span() {
|
||||
let span = Span::raw("test content");
|
||||
@@ -533,6 +604,12 @@ mod tests {
|
||||
assert_eq!(format!("{span:.4}"), "test");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_newline_span() {
|
||||
let span = Span::raw("test\ncontent");
|
||||
assert_eq!(format!("{span}"), "testcontent");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_styled_span() {
|
||||
let stylized_span = Span::styled("stylized test content", Style::new().green());
|
||||
@@ -580,8 +657,11 @@ mod tests {
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn render_out_of_bounds(mut small_buf: Buffer) {
|
||||
let out_of_bounds = Rect::new(20, 20, 10, 1);
|
||||
#[case::x(20, 0)]
|
||||
#[case::y(0, 20)]
|
||||
#[case::both(20, 20)]
|
||||
fn render_out_of_bounds(mut small_buf: Buffer, #[case] x: u16, #[case] y: u16) {
|
||||
let out_of_bounds = Rect::new(x, y, 10, 1);
|
||||
Span::raw("Hello, World!").render(out_of_bounds, &mut small_buf);
|
||||
assert_eq!(small_buf, Buffer::empty(small_buf.area));
|
||||
}
|
||||
@@ -664,5 +744,103 @@ mod tests {
|
||||
])]);
|
||||
assert_eq!(buf, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_first_zero_width() {
|
||||
let span = Span::raw("\u{200B}abc");
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 3, 1));
|
||||
span.render(buf.area, &mut buf);
|
||||
assert_eq!(
|
||||
buf.content(),
|
||||
[Cell::new("\u{200B}a"), Cell::new("b"), Cell::new("c"),]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_second_zero_width() {
|
||||
let span = Span::raw("a\u{200B}bc");
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 3, 1));
|
||||
span.render(buf.area, &mut buf);
|
||||
assert_eq!(
|
||||
buf.content(),
|
||||
[Cell::new("a\u{200B}"), Cell::new("b"), Cell::new("c")]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_middle_zero_width() {
|
||||
let span = Span::raw("ab\u{200B}c");
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 3, 1));
|
||||
span.render(buf.area, &mut buf);
|
||||
assert_eq!(
|
||||
buf.content(),
|
||||
[Cell::new("a"), Cell::new("b\u{200B}"), Cell::new("c")]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_last_zero_width() {
|
||||
let span = Span::raw("abc\u{200B}");
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 3, 1));
|
||||
span.render(buf.area, &mut buf);
|
||||
assert_eq!(
|
||||
buf.content(),
|
||||
[Cell::new("a"), Cell::new("b"), Cell::new("c\u{200B}")]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_with_newlines() {
|
||||
let span = Span::raw("a\nb");
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 2, 1));
|
||||
span.render(buf.area, &mut buf);
|
||||
assert_eq!(buf.content(), [Cell::new("a"), Cell::new("b")]);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// to be exceeded (due to a position + 1 calculation that fails to account for the possibility
|
||||
/// that the next position might not be available).
|
||||
#[test]
|
||||
fn issue_1160() {
|
||||
let span = Span::raw("Hello\u{200E}");
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 5, 1));
|
||||
span.render(buf.area, &mut buf);
|
||||
assert_eq!(
|
||||
buf.content(),
|
||||
[
|
||||
Cell::new("H"),
|
||||
Cell::new("e"),
|
||||
Cell::new("l"),
|
||||
Cell::new("l"),
|
||||
Cell::new("o\u{200E}"),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add() {
|
||||
assert_eq!(
|
||||
Span::default() + Span::default(),
|
||||
Line::from(vec![Span::default(), Span::default()])
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
Span::default() + Span::raw("test"),
|
||||
Line::from(vec![Span::default(), Span::raw("test")])
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
Span::raw("test") + Span::default(),
|
||||
Line::from(vec![Span::raw("test"), Span::default()])
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
Span::raw("test") + Span::raw("content"),
|
||||
Line::from(vec![Span::raw("test"), Span::raw("content")])
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
#![warn(missing_docs)]
|
||||
use std::{borrow::Cow, fmt};
|
||||
|
||||
use itertools::{Itertools, Position};
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::{prelude::*, style::Styled};
|
||||
|
||||
/// A string split over one or more lines.
|
||||
///
|
||||
@@ -165,14 +163,32 @@ use crate::prelude::*;
|
||||
/// ```
|
||||
///
|
||||
/// [`Paragraph`]: crate::widgets::Paragraph
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
#[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.style == Style::default() && self.alignment.is_none() {
|
||||
f.write_str("Text ")?;
|
||||
f.debug_list().entries(&self.lines).finish()
|
||||
} else {
|
||||
let mut debug = f.debug_struct("Text");
|
||||
if self.style != Style::default() {
|
||||
debug.field("style", &self.style);
|
||||
}
|
||||
if let Some(alignment) = self.alignment {
|
||||
debug.field("alignment", &format!("Alignment::{alignment}"));
|
||||
}
|
||||
debug.field("lines", &self.lines).finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Text<'a> {
|
||||
@@ -570,6 +586,33 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> std::ops::Add<Line<'a>> for Text<'a> {
|
||||
type Output = Self;
|
||||
|
||||
fn add(mut self, line: Line<'a>) -> Self::Output {
|
||||
self.push_line(line);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds two `Text` together.
|
||||
///
|
||||
/// This ignores the style and alignment of the second `Text`.
|
||||
impl<'a> std::ops::Add<Self> for Text<'a> {
|
||||
type Output = Self;
|
||||
|
||||
fn add(mut self, text: Self) -> Self::Output {
|
||||
self.lines.extend(text.lines);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> std::ops::AddAssign<Line<'a>> for Text<'a> {
|
||||
fn add_assign(&mut self, line: Line<'a>) {
|
||||
self.push_line(line);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> Extend<T> for Text<'a>
|
||||
where
|
||||
T: Into<Line<'a>>,
|
||||
@@ -580,14 +623,36 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for converting a value to a [`Text`].
|
||||
///
|
||||
/// This trait is automatically implemented for any type that implements the [`Display`] trait. As
|
||||
/// such, `ToText` shouldn't be implemented directly: [`Display`] should be implemented instead, and
|
||||
/// you get the `ToText` implementation for free.
|
||||
///
|
||||
/// [`Display`]: std::fmt::Display
|
||||
pub trait ToText {
|
||||
/// Converts the value to a [`Text`].
|
||||
fn to_text(&self) -> Text<'_>;
|
||||
}
|
||||
|
||||
/// # Panics
|
||||
///
|
||||
/// In this implementation, the `to_text` method panics if the `Display` implementation returns an
|
||||
/// error. This indicates an incorrect `Display` implementation since `fmt::Write for String` never
|
||||
/// returns an error itself.
|
||||
impl<T: fmt::Display> ToText for T {
|
||||
fn to_text(&self) -> Text {
|
||||
Text::raw(self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
@@ -744,7 +809,25 @@ 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]
|
||||
#[case(42, Text::from("42"))]
|
||||
#[case("just\ntesting", Text::from("just\ntesting"))]
|
||||
#[case(true, Text::from("true"))]
|
||||
#[case(6.66, Text::from("6.66"))]
|
||||
#[case('a', Text::from("a"))]
|
||||
#[case(String::from("hello"), Text::from("hello"))]
|
||||
#[case(-1, Text::from("-1"))]
|
||||
#[case("line1\nline2", Text::from("line1\nline2"))]
|
||||
#[case(
|
||||
"first line\nsecond line\nthird line",
|
||||
Text::from("first line\nsecond line\nthird line")
|
||||
)]
|
||||
#[case("trailing newline\n", Text::from("trailing newline\n"))]
|
||||
fn to_text(#[case] value: impl fmt::Display, #[case] expected: Text) {
|
||||
assert_eq!(value.to_text(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -788,6 +871,44 @@ mod tests {
|
||||
assert_eq!(iter.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_line() {
|
||||
assert_eq!(
|
||||
Text::raw("Red").red() + Line::raw("Blue").blue(),
|
||||
Text {
|
||||
lines: vec![Line::raw("Red"), Line::raw("Blue").blue()],
|
||||
style: Style::new().red(),
|
||||
alignment: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_text() {
|
||||
assert_eq!(
|
||||
Text::raw("Red").red() + Text::raw("Blue").blue(),
|
||||
Text {
|
||||
lines: vec![Line::raw("Red"), Line::raw("Blue")],
|
||||
style: Style::new().red(),
|
||||
alignment: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_assign_line() {
|
||||
let mut text = Text::raw("Red").red();
|
||||
text += Line::raw("Blue").blue();
|
||||
assert_eq!(
|
||||
text,
|
||||
Text {
|
||||
lines: vec![Line::raw("Red"), Line::raw("Blue").blue()],
|
||||
style: Style::new().red(),
|
||||
alignment: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extend() {
|
||||
let mut text = Text::from("The first line\nThe second line");
|
||||
@@ -839,11 +960,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]
|
||||
@@ -935,7 +1057,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]
|
||||
@@ -957,7 +1079,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 {
|
||||
@@ -1128,4 +1250,46 @@ mod tests {
|
||||
assert_eq!(result, "Hello world!");
|
||||
}
|
||||
}
|
||||
|
||||
mod debug {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
#[ignore = "This is just showing the debug output of the assertions"]
|
||||
fn no_style() {
|
||||
let text = Text::from("single unstyled line");
|
||||
assert_eq!(text, Text::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "This is just showing the debug output of the assertions"]
|
||||
fn text_style() {
|
||||
let text = Text::from("single styled line")
|
||||
.red()
|
||||
.on_black()
|
||||
.bold()
|
||||
.not_italic();
|
||||
assert_eq!(text, Text::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "This is just showing the debug output of the assertions"]
|
||||
fn line_style() {
|
||||
let text = Text::from(vec![
|
||||
Line::from("first line").red().alignment(Alignment::Right),
|
||||
Line::from("second line").on_black(),
|
||||
]);
|
||||
assert_eq!(text, Text::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "This is just showing the debug output of the assertions"]
|
||||
fn span_style() {
|
||||
let text = Text::from(Line::from(vec![
|
||||
Span::from("first span").red(),
|
||||
Span::from("second span").on_black(),
|
||||
]));
|
||||
assert_eq!(text, Text::default());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,38 +21,8 @@
|
||||
//! - [`Tabs`]: displays a tab bar and allows selection.
|
||||
//!
|
||||
//! [`Canvas`]: crate::widgets::canvas::Canvas
|
||||
mod barchart;
|
||||
pub mod block;
|
||||
mod borders;
|
||||
#[cfg(feature = "widget-calendar")]
|
||||
pub mod calendar;
|
||||
pub mod canvas;
|
||||
mod chart;
|
||||
mod clear;
|
||||
mod gauge;
|
||||
mod list;
|
||||
mod paragraph;
|
||||
mod reflow;
|
||||
mod scrollbar;
|
||||
mod sparkline;
|
||||
mod table;
|
||||
mod tabs;
|
||||
|
||||
pub use self::{
|
||||
barchart::{Bar, BarChart, BarGroup},
|
||||
block::{Block, BorderType, Padding},
|
||||
borders::*,
|
||||
chart::{Axis, Chart, Dataset, GraphType, LegendPosition},
|
||||
clear::Clear,
|
||||
gauge::{Gauge, LineGauge},
|
||||
list::{List, ListDirection, ListItem, ListState},
|
||||
paragraph::{Paragraph, Wrap},
|
||||
scrollbar::{ScrollDirection, Scrollbar, ScrollbarOrientation, ScrollbarState},
|
||||
sparkline::{RenderDirection, Sparkline},
|
||||
table::{Cell, HighlightSpacing, Row, Table, TableState},
|
||||
tabs::Tabs,
|
||||
};
|
||||
use crate::{buffer::Buffer, layout::Rect};
|
||||
use crate::{buffer::Buffer, layout::Rect, style::Style};
|
||||
|
||||
/// A `Widget` is a type that can be drawn on a [`Buffer`] in a given [`Rect`].
|
||||
///
|
||||
@@ -60,14 +30,22 @@ use crate::{buffer::Buffer, layout::Rect};
|
||||
/// 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, we added a new [`WidgetRef`] trait and implemented this on all the
|
||||
/// internal widgets. This allows you to store a reference to a widget and render it later. It also
|
||||
/// 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.
|
||||
///
|
||||
/// The `Widget` trait can still be implemented, however, it is recommended to implement `WidgetRef`
|
||||
/// and add an implementation of `Widget` that calls `WidgetRef::render_ref`. This pattern should be
|
||||
/// used where backwards compatibility is required (all the internal widgets use this approach).
|
||||
/// 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.
|
||||
@@ -80,7 +58,7 @@ use crate::{buffer::Buffer, layout::Rect};
|
||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||
///
|
||||
/// terminal.draw(|frame| {
|
||||
/// frame.render_widget(Clear, frame.size());
|
||||
/// frame.render_widget(Clear, frame.area());
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
@@ -235,9 +213,9 @@ pub trait StatefulWidget {
|
||||
/// useful when you want to store a collection of widgets with different types. You can then iterate
|
||||
/// over the collection and render each widget.
|
||||
///
|
||||
/// This trait was introduced in Ratatui 0.26.0 and is implemented for all the internal widgets.
|
||||
/// Implementors should prefer to implement this over the `Widget` trait and add an implementation
|
||||
/// of `Widget` that calls `WidgetRef::render_ref` where backwards compatibility is required.
|
||||
/// This trait was introduced in Ratatui 0.26.0 and is implemented for all the internal widgets. It
|
||||
/// is currently marked as unstable as we are still evaluating the API and may make changes in the
|
||||
/// future. See <https://github.com/ratatui/ratatui/issues/1287> for more information.
|
||||
///
|
||||
/// A blanket implementation of `Widget` for `&W` where `W` implements `WidgetRef` is provided.
|
||||
///
|
||||
@@ -297,7 +275,7 @@ pub trait StatefulWidget {
|
||||
/// # }
|
||||
/// # }
|
||||
/// ```
|
||||
#[stability::unstable(feature = "widget-ref")]
|
||||
#[instability::unstable(feature = "widget-ref")]
|
||||
pub trait WidgetRef {
|
||||
/// Draws the current state of the widget in the given buffer. That is the only method required
|
||||
/// to implement a custom widget.
|
||||
@@ -359,9 +337,9 @@ impl<W: WidgetRef> WidgetRef for Option<W> {
|
||||
/// to a stateful widget and render it later. It also allows you to render boxed stateful widgets.
|
||||
///
|
||||
/// This trait was introduced in Ratatui 0.26.0 and is implemented for all the internal stateful
|
||||
/// widgets. Implementors should prefer to implement this over the `StatefulWidget` trait and add an
|
||||
/// implementation of `StatefulWidget` that calls `StatefulWidgetRef::render_ref` where backwards
|
||||
/// compatibility is required.
|
||||
/// widgets. It is currently marked as unstable as we are still evaluating the API and may make
|
||||
/// changes in the future. See <https://github.com/ratatui/ratatui/issues/1287> for more
|
||||
/// information.
|
||||
///
|
||||
/// A blanket implementation of `StatefulWidget` for `&W` where `W` implements `StatefulWidgetRef`
|
||||
/// is provided.
|
||||
@@ -398,7 +376,7 @@ impl<W: WidgetRef> WidgetRef for Option<W> {
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
#[stability::unstable(feature = "widget-ref")]
|
||||
#[instability::unstable(feature = "widget-ref")]
|
||||
pub trait StatefulWidgetRef {
|
||||
/// State associated with the stateful widget.
|
||||
///
|
||||
@@ -444,7 +422,7 @@ impl Widget for &str {
|
||||
/// [`Rect`].
|
||||
impl WidgetRef for &str {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
buf.set_string(area.x, area.y, self, crate::style::Style::default());
|
||||
buf.set_stringn(area.x, area.y, self, area.width as usize, Style::new());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -465,7 +443,7 @@ impl Widget for String {
|
||||
/// without the need to give up ownership of the underlying text.
|
||||
impl WidgetRef for String {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
buf.set_string(area.x, area.y, self, crate::style::Style::default());
|
||||
buf.set_stringn(area.x, area.y, self, area.width as usize, Style::new());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -474,7 +452,7 @@ mod tests {
|
||||
use rstest::{fixture, rstest};
|
||||
|
||||
use super::*;
|
||||
use crate::prelude::*;
|
||||
use crate::text::Line;
|
||||
|
||||
#[fixture]
|
||||
fn buf() -> Buffer {
|
||||
@@ -650,6 +628,13 @@ mod tests {
|
||||
assert_eq!(buf, Buffer::with_lines(["hello world "]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn render_area(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_ref(mut buf: Buffer) {
|
||||
"hello world".render_ref(buf.area, &mut buf);
|
||||
@@ -677,6 +662,13 @@ mod tests {
|
||||
assert_eq!(buf, Buffer::with_lines(["hello world "]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn render_area(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_ref(mut buf: Buffer) {
|
||||
String::from("hello world").render_ref(buf.area, &mut buf);
|
||||
20
ratatui-crossterm/Cargo.toml
Normal file
20
ratatui-crossterm/Cargo.toml
Normal file
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "ratatui-crossterm"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[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 = []
|
||||
|
||||
[dependencies]
|
||||
ratatui-core = { workspace = true }
|
||||
crossterm.workspace = true
|
||||
instability.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
rstest.workspace = true
|
||||
681
ratatui-crossterm/src/lib.rs
Normal file
681
ratatui-crossterm/src/lib.rs
Normal file
@@ -0,0 +1,681 @@
|
||||
//! 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
|
||||
use std::io::{self, Write};
|
||||
|
||||
#[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, 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 ratatui::{
|
||||
/// crossterm::{
|
||||
/// terminal::{
|
||||
/// disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
|
||||
/// },
|
||||
/// ExecutableCommand,
|
||||
/// },
|
||||
/// prelude::*,
|
||||
/// };
|
||||
///
|
||||
/// 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`]: crate::terminal::Terminal
|
||||
/// [`backend`]: crate::backend
|
||||
/// [Crossterm]: https://crates.io/crates/crossterm
|
||||
/// [Examples]: https://github.com/ratatui/ratatui/tree/main/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::prelude::*;
|
||||
/// 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(Colors::new(
|
||||
from_ratatui_color(cell.fg),
|
||||
from_ratatui_color(cell.bg)
|
||||
))
|
||||
)?;
|
||||
fg = cell.fg;
|
||||
bg = cell.bg;
|
||||
}
|
||||
#[cfg(feature = "underline-color")]
|
||||
if cell.underline_color != underline_color {
|
||||
let color = from_ratatui_color(cell.underline_color);
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
fn from_ratatui_color(color: Color) -> CrosstermColor {
|
||||
match color {
|
||||
Color::Reset => CrosstermColor::Reset,
|
||||
Color::Black => CrosstermColor::Black,
|
||||
Color::Red => CrosstermColor::DarkRed,
|
||||
Color::Green => CrosstermColor::DarkGreen,
|
||||
Color::Yellow => CrosstermColor::DarkYellow,
|
||||
Color::Blue => CrosstermColor::DarkBlue,
|
||||
Color::Magenta => CrosstermColor::DarkMagenta,
|
||||
Color::Cyan => CrosstermColor::DarkCyan,
|
||||
Color::Gray => CrosstermColor::Grey,
|
||||
Color::DarkGray => CrosstermColor::DarkGrey,
|
||||
Color::LightRed => CrosstermColor::Red,
|
||||
Color::LightGreen => CrosstermColor::Green,
|
||||
Color::LightBlue => CrosstermColor::Blue,
|
||||
Color::LightYellow => CrosstermColor::Yellow,
|
||||
Color::LightMagenta => CrosstermColor::Magenta,
|
||||
Color::LightCyan => CrosstermColor::Cyan,
|
||||
Color::White => CrosstermColor::White,
|
||||
Color::Indexed(i) => CrosstermColor::AnsiValue(i),
|
||||
Color::Rgb(r, g, b) => CrosstermColor::Rgb { r, g, b },
|
||||
}
|
||||
}
|
||||
|
||||
fn from_crossterm_color(value: CrosstermColor) -> Color {
|
||||
match value {
|
||||
CrosstermColor::Reset => Color::Reset,
|
||||
CrosstermColor::Black => Color::Black,
|
||||
CrosstermColor::DarkRed => Color::Red,
|
||||
CrosstermColor::DarkGreen => Color::Green,
|
||||
CrosstermColor::DarkYellow => Color::Yellow,
|
||||
CrosstermColor::DarkBlue => Color::Blue,
|
||||
CrosstermColor::DarkMagenta => Color::Magenta,
|
||||
CrosstermColor::DarkCyan => Color::Cyan,
|
||||
CrosstermColor::Grey => Color::Gray,
|
||||
CrosstermColor::DarkGrey => Color::DarkGray,
|
||||
CrosstermColor::Red => Color::LightRed,
|
||||
CrosstermColor::Green => Color::LightGreen,
|
||||
CrosstermColor::Blue => Color::LightBlue,
|
||||
CrosstermColor::Yellow => Color::LightYellow,
|
||||
CrosstermColor::Magenta => Color::LightMagenta,
|
||||
CrosstermColor::Cyan => Color::LightCyan,
|
||||
CrosstermColor::White => Color::White,
|
||||
CrosstermColor::Rgb { r, g, b } => Color::Rgb(r, g, b),
|
||||
CrosstermColor::AnsiValue(v) => Color::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(())
|
||||
}
|
||||
}
|
||||
|
||||
fn from_crossterm_attribute(value: CrosstermAttribute) -> Modifier {
|
||||
// `Attribute*s*` (note the *s*) contains multiple `Attribute`
|
||||
// We convert `Attribute` to `Attribute*s*` (containing only 1 value) to avoid implementing
|
||||
// the conversion again
|
||||
from_crossterm_attributes(CrosstermAttributes::from(value))
|
||||
}
|
||||
|
||||
fn from_crossterm_attributes(value: CrosstermAttributes) -> Modifier {
|
||||
let mut res = Modifier::empty();
|
||||
|
||||
if value.has(CrosstermAttribute::Bold) {
|
||||
res |= Modifier::BOLD;
|
||||
}
|
||||
if value.has(CrosstermAttribute::Dim) {
|
||||
res |= Modifier::DIM;
|
||||
}
|
||||
if value.has(CrosstermAttribute::Italic) {
|
||||
res |= Modifier::ITALIC;
|
||||
}
|
||||
if value.has(CrosstermAttribute::Underlined)
|
||||
|| value.has(CrosstermAttribute::DoubleUnderlined)
|
||||
|| value.has(CrosstermAttribute::Undercurled)
|
||||
|| value.has(CrosstermAttribute::Underdotted)
|
||||
|| value.has(CrosstermAttribute::Underdashed)
|
||||
{
|
||||
res |= Modifier::UNDERLINED;
|
||||
}
|
||||
if value.has(CrosstermAttribute::SlowBlink) {
|
||||
res |= Modifier::SLOW_BLINK;
|
||||
}
|
||||
if value.has(CrosstermAttribute::RapidBlink) {
|
||||
res |= Modifier::RAPID_BLINK;
|
||||
}
|
||||
if value.has(CrosstermAttribute::Reverse) {
|
||||
res |= Modifier::REVERSED;
|
||||
}
|
||||
if value.has(CrosstermAttribute::Hidden) {
|
||||
res |= Modifier::HIDDEN;
|
||||
}
|
||||
if value.has(CrosstermAttribute::CrossedOut) {
|
||||
res |= Modifier::CROSSED_OUT;
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
fn from_crossterm_style(value: ContentStyle) -> Style {
|
||||
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;
|
||||
}
|
||||
|
||||
Style {
|
||||
fg: value.foreground_color.map(from_crossterm_color),
|
||||
bg: value.background_color.map(from_crossterm_color),
|
||||
#[cfg(feature = "underline-color")]
|
||||
underline_color: value.underline_color.map(from_crossterm_color),
|
||||
add_modifier: from_crossterm_attributes(value.attributes),
|
||||
sub_modifier,
|
||||
}
|
||||
}
|
||||
|
||||
#[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 convert_from_crossterm_color(#[case] value: CrosstermColor, #[case] expected: Color) {
|
||||
assert_eq!(from_crossterm_color(value), expected);
|
||||
}
|
||||
|
||||
mod modifier {
|
||||
use super::*;
|
||||
|
||||
#[rstest]
|
||||
#[rstest]
|
||||
#[case(CrosstermAttribute::Reset, Modifier::empty())]
|
||||
#[case(CrosstermAttribute::Bold, Modifier::BOLD)]
|
||||
#[case(CrosstermAttribute::Italic, Modifier::ITALIC)]
|
||||
#[case(CrosstermAttribute::Underlined, Modifier::UNDERLINED)]
|
||||
#[case(CrosstermAttribute::DoubleUnderlined, Modifier::UNDERLINED)]
|
||||
#[case(CrosstermAttribute::Underdotted, Modifier::UNDERLINED)]
|
||||
#[case(CrosstermAttribute::Dim, Modifier::DIM)]
|
||||
#[case(CrosstermAttribute::NormalIntensity, Modifier::empty())]
|
||||
#[case(CrosstermAttribute::CrossedOut, Modifier::CROSSED_OUT)]
|
||||
#[case(CrosstermAttribute::NoUnderline, Modifier::empty())]
|
||||
#[case(CrosstermAttribute::OverLined, 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)]
|
||||
fn convert_from_crossterm_attribute(
|
||||
#[case] value: CrosstermAttribute,
|
||||
#[case] expected: Modifier,
|
||||
) {
|
||||
assert_eq!(from_crossterm_attribute(value), expected);
|
||||
}
|
||||
|
||||
#[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
|
||||
)]
|
||||
#[case(
|
||||
&[CrosstermAttribute::DoubleUnderlined, CrosstermAttribute::OverLined],
|
||||
Modifier::UNDERLINED
|
||||
)]
|
||||
#[case(
|
||||
&[CrosstermAttribute::Undercurled, CrosstermAttribute::Underdashed],
|
||||
Modifier::UNDERLINED
|
||||
)]
|
||||
#[case(
|
||||
&[CrosstermAttribute::NoBold, CrosstermAttribute::NoItalic],
|
||||
Modifier::empty()
|
||||
)]
|
||||
#[case(
|
||||
&[CrosstermAttribute::NoBlink, CrosstermAttribute::NoReverse],
|
||||
Modifier::empty()
|
||||
)]
|
||||
fn convert_from_crossterm_attributes(
|
||||
#[case] value: &[CrosstermAttribute],
|
||||
#[case] expected: Modifier,
|
||||
) {
|
||||
assert_eq!(
|
||||
from_crossterm_attributes(CrosstermAttributes::from(value)),
|
||||
expected
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[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)
|
||||
)]
|
||||
#[cfg(feature = "underline-color")]
|
||||
#[case(
|
||||
ContentStyle {
|
||||
underline_color: Some(CrosstermColor::DarkRed),
|
||||
..Default::default()
|
||||
},
|
||||
Style::default().underline_color(Color::Red)
|
||||
)]
|
||||
fn convert_from_crossterm_content_style(#[case] value: ContentStyle, #[case] expected: Style) {
|
||||
assert_eq!(from_crossterm_style(value), expected);
|
||||
}
|
||||
}
|
||||
9
ratatui-termion/Cargo.toml
Normal file
9
ratatui-termion/Cargo.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "ratatui-termion"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
instability = { workspace = true }
|
||||
ratatui-core = { workspace = true }
|
||||
termion.workspace = true
|
||||
@@ -9,14 +9,13 @@ use std::{
|
||||
io::{self, Write},
|
||||
};
|
||||
|
||||
use termion::{color as tcolor, style as tstyle};
|
||||
|
||||
use crate::{
|
||||
use ratatui_core::{
|
||||
backend::{Backend, ClearType, WindowSize},
|
||||
buffer::Cell,
|
||||
prelude::Rect,
|
||||
layout::{Position, Size},
|
||||
style::{Color, Modifier, Style},
|
||||
};
|
||||
use termion::{color as tcolor, color::Color as _, style as tstyle};
|
||||
|
||||
/// A [`Backend`] implementation that uses [Termion] to render to the terminal.
|
||||
///
|
||||
@@ -40,8 +39,10 @@ use crate::{
|
||||
/// ```rust,no_run
|
||||
/// use std::io::{stderr, stdout};
|
||||
///
|
||||
/// use ratatui::prelude::*;
|
||||
/// use termion::{raw::IntoRawMode, screen::IntoAlternateScreen};
|
||||
/// use ratatui::{
|
||||
/// prelude::*,
|
||||
/// termion::{raw::IntoRawMode, screen::IntoAlternateScreen},
|
||||
/// };
|
||||
///
|
||||
/// let writer = stdout().into_raw_mode()?.into_alternate_screen()?;
|
||||
/// let mut backend = TermionBackend::new(writer);
|
||||
@@ -75,6 +76,11 @@ 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
|
||||
@@ -85,6 +91,26 @@ where
|
||||
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 TermionBackend<W>
|
||||
@@ -136,11 +162,13 @@ where
|
||||
self.writer.flush()
|
||||
}
|
||||
|
||||
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
|
||||
termion::cursor::DetectCursorPos::cursor_pos(&mut self.writer).map(|(x, y)| (x - 1, y - 1))
|
||||
fn get_cursor_position(&mut self) -> io::Result<Position> {
|
||||
termion::cursor::DetectCursorPos::cursor_pos(&mut self.writer)
|
||||
.map(|(x, y)| Position { x: x - 1, y: y - 1 })
|
||||
}
|
||||
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
||||
fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()> {
|
||||
let Position { x, y } = position.into();
|
||||
write!(self.writer, "{}", termion::cursor::Goto(x + 1, y + 1))?;
|
||||
self.writer.flush()
|
||||
}
|
||||
@@ -155,13 +183,13 @@ where
|
||||
let mut fg = Color::Reset;
|
||||
let mut bg = Color::Reset;
|
||||
let mut modifier = Modifier::empty();
|
||||
let mut last_pos: Option<(u16, u16)> = None;
|
||||
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.0 + 1 && y == p.1) {
|
||||
if !matches!(last_pos, Some(p) if x == p.x + 1 && y == p.y) {
|
||||
write!(string, "{}", termion::cursor::Goto(x + 1, y + 1)).unwrap();
|
||||
}
|
||||
last_pos = Some((x, y));
|
||||
last_pos = Some(Position { x, y });
|
||||
if cell.modifier != modifier {
|
||||
write!(
|
||||
string,
|
||||
@@ -193,9 +221,9 @@ where
|
||||
)
|
||||
}
|
||||
|
||||
fn size(&self) -> io::Result<Rect> {
|
||||
fn size(&self) -> io::Result<Size> {
|
||||
let terminal = termion::terminal_size()?;
|
||||
Ok(Rect::new(0, 0, terminal.0, terminal.1))
|
||||
Ok(Size::new(terminal.0, terminal.1))
|
||||
}
|
||||
|
||||
fn window_size(&mut self) -> io::Result<WindowSize> {
|
||||
@@ -223,7 +251,6 @@ struct ModifierDiff {
|
||||
|
||||
impl fmt::Display for Fg {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
use termion::color::Color as TermionColor;
|
||||
match self.0 {
|
||||
Color::Reset => termion::color::Reset.write_fg(f),
|
||||
Color::Black => termion::color::Black.write_fg(f),
|
||||
@@ -249,7 +276,6 @@ impl fmt::Display for Fg {
|
||||
}
|
||||
impl fmt::Display for Bg {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
use termion::color::Color as TermionColor;
|
||||
match self.0 {
|
||||
Color::Reset => termion::color::Reset.write_bg(f),
|
||||
Color::Black => termion::color::Black.write_bg(f),
|
||||
@@ -275,7 +301,7 @@ impl fmt::Display for Bg {
|
||||
}
|
||||
|
||||
macro_rules! from_termion_for_color {
|
||||
($termion_color:ident, $color: ident) => {
|
||||
($termion_color:ident, $color:ident) => {
|
||||
impl From<tcolor::$termion_color> for Color {
|
||||
fn from(_: tcolor::$termion_color) -> Self {
|
||||
Color::$color
|
||||
@@ -416,7 +442,7 @@ impl fmt::Display for ModifierDiff {
|
||||
}
|
||||
|
||||
macro_rules! from_termion_for_modifier {
|
||||
($termion_modifier:ident, $modifier: ident) => {
|
||||
($termion_modifier:ident, $modifier:ident) => {
|
||||
impl From<tstyle::$termion_modifier> for Modifier {
|
||||
fn from(_: tstyle::$termion_modifier) -> Self {
|
||||
Modifier::$modifier
|
||||
7
ratatui-termwiz/Cargo.toml
Normal file
7
ratatui-termwiz/Cargo.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
[package]
|
||||
name = "ratatui-termwiz"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
ratatui-core = { workspace = true }
|
||||
@@ -7,20 +7,18 @@
|
||||
|
||||
use std::{error::Error, io};
|
||||
|
||||
use termwiz::{
|
||||
caps::Capabilities,
|
||||
cell::{AttributeChange, Blink, CellAttributes, Intensity, Underline},
|
||||
color::{AnsiColor, ColorAttribute, ColorSpec, LinearRgba, RgbColor, SrgbaTuple},
|
||||
surface::{Change, CursorVisibility, Position},
|
||||
terminal::{buffered::BufferedTerminal, ScreenSize, SystemTerminal, Terminal},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
backend::{Backend, WindowSize},
|
||||
buffer::Cell,
|
||||
layout::Size,
|
||||
prelude::Rect,
|
||||
style::{Color, Modifier, Style},
|
||||
termwiz::{
|
||||
caps::Capabilities,
|
||||
cell::{AttributeChange, Blink, CellAttributes, Intensity, Underline},
|
||||
color::{AnsiColor, ColorAttribute, ColorSpec, LinearRgba, RgbColor, SrgbaTuple},
|
||||
surface::{Change, CursorVisibility, Position},
|
||||
terminal::{buffered::BufferedTerminal, ScreenSize, SystemTerminal, Terminal},
|
||||
},
|
||||
};
|
||||
|
||||
/// A [`Backend`] implementation that uses [Termwiz] to render to the terminal.
|
||||
@@ -59,7 +57,7 @@ use crate::{
|
||||
/// [`Terminal`]: crate::terminal::Terminal
|
||||
/// [`BufferedTerminal`]: termwiz::terminal::buffered::BufferedTerminal
|
||||
/// [Termwiz]: https://crates.io/crates/termwiz
|
||||
/// [Examples]: https://github.com/ratatui-org/ratatui/tree/main/examples/README.md
|
||||
/// [Examples]: https://github.com/ratatui/ratatui/tree/main/examples/README.md
|
||||
pub struct TermwizBackend {
|
||||
buffered_terminal: BufferedTerminal<SystemTerminal>,
|
||||
}
|
||||
@@ -193,12 +191,16 @@ impl Backend for TermwizBackend {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
|
||||
fn get_cursor_position(&mut self) -> io::Result<crate::layout::Position> {
|
||||
let (x, y) = self.buffered_terminal.cursor_position();
|
||||
Ok((x as u16, y as u16))
|
||||
Ok((x as u16, y as u16).into())
|
||||
}
|
||||
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
||||
fn set_cursor_position<P: Into<crate::layout::Position>>(
|
||||
&mut self,
|
||||
position: P,
|
||||
) -> io::Result<()> {
|
||||
let crate::layout::Position { x, y } = position.into();
|
||||
self.buffered_terminal.add_change(Change::CursorPosition {
|
||||
x: Position::Absolute(x as usize),
|
||||
y: Position::Absolute(y as usize),
|
||||
@@ -213,9 +215,9 @@ impl Backend for TermwizBackend {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn size(&self) -> io::Result<Rect> {
|
||||
fn size(&self) -> io::Result<Size> {
|
||||
let (cols, rows) = self.buffered_terminal.dimensions();
|
||||
Ok(Rect::new(0, 0, u16_max(cols), u16_max(rows)))
|
||||
Ok(Size::new(u16_max(cols), u16_max(rows)))
|
||||
}
|
||||
|
||||
fn window_size(&mut self) -> io::Result<WindowSize> {
|
||||
40
ratatui-widgets/Cargo.toml
Normal file
40
ratatui-widgets/Cargo.toml
Normal file
@@ -0,0 +1,40 @@
|
||||
[package]
|
||||
name = "ratatui-widgets"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[features]
|
||||
# TODO: remove unstable-widget-ref, consider whether to keep all-widgets
|
||||
default = ["all-widgets", "unstable-widget-ref"]
|
||||
|
||||
## 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"]
|
||||
|
||||
|
||||
## 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"]
|
||||
|
||||
unstable-widget-ref = ["ratatui-core/unstable-widget-ref"]
|
||||
|
||||
[dependencies]
|
||||
bitflags.workspace = true
|
||||
instability.workspace = true
|
||||
itertools.workspace = true
|
||||
ratatui-core.workspace = true
|
||||
serde = { workspace = true, optional = true }
|
||||
strum.workspace = true
|
||||
time = { workspace = true, optional = true }
|
||||
unicode-segmentation.workspace = true
|
||||
unicode-width.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
rstest.workspace = true
|
||||
indoc.workspace = true
|
||||
pretty_assertions.workspace = true
|
||||
time.workspace = true
|
||||
@@ -1,4 +1,8 @@
|
||||
use crate::{prelude::*, widgets::Block};
|
||||
use ratatui_core::{
|
||||
prelude::{symbols, Buffer, Direction, Line, Rect, Style, Stylize, Widget},
|
||||
style::Styled,
|
||||
widgets::WidgetRef,
|
||||
};
|
||||
|
||||
mod bar;
|
||||
mod bar_group;
|
||||
@@ -6,6 +10,8 @@ mod bar_group;
|
||||
pub use bar::Bar;
|
||||
pub use bar_group::BarGroup;
|
||||
|
||||
use crate::{block::BlockExt, Block};
|
||||
|
||||
/// A chart showing values as [bars](Bar).
|
||||
///
|
||||
/// Here is a possible `BarChart` output.
|
||||
@@ -431,7 +437,7 @@ impl BarChart<'_> {
|
||||
} else {
|
||||
self.bar_set.empty
|
||||
};
|
||||
buf.get_mut(bars_area.left() + x, bar_y)
|
||||
buf[(bars_area.left() + x, bar_y)]
|
||||
.set_symbol(symbol)
|
||||
.set_style(bar_style);
|
||||
}
|
||||
@@ -507,7 +513,7 @@ impl BarChart<'_> {
|
||||
let bar_style = self.bar_style.patch(bar.style);
|
||||
|
||||
for x in 0..self.bar_width {
|
||||
buf.get_mut(bar_x + x, area.top() + j)
|
||||
buf[(bar_x + x, area.top() + j)]
|
||||
.set_symbol(symbol)
|
||||
.set_style(bar_style);
|
||||
}
|
||||
@@ -613,9 +619,14 @@ impl<'a> Styled for BarChart<'a> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use itertools::iproduct;
|
||||
use ratatui_core::{
|
||||
layout::Alignment,
|
||||
style::{Color, Modifier},
|
||||
text::Span,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
use crate::widgets::BorderType;
|
||||
use crate::BorderType;
|
||||
|
||||
#[test]
|
||||
fn default() {
|
||||
@@ -698,7 +709,7 @@ mod tests {
|
||||
"f b ",
|
||||
]);
|
||||
for (x, y) in iproduct!([0, 2], [0, 1]) {
|
||||
expected.get_mut(x, y).set_fg(Color::Red);
|
||||
expected[(x, y)].set_fg(Color::Red);
|
||||
}
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
@@ -790,8 +801,8 @@ mod tests {
|
||||
"█1█ █2█ ",
|
||||
"foo bar ",
|
||||
]);
|
||||
expected.get_mut(1, 1).set_fg(Color::Red);
|
||||
expected.get_mut(5, 1).set_fg(Color::Red);
|
||||
expected[(1, 1)].set_fg(Color::Red);
|
||||
expected[(5, 1)].set_fg(Color::Red);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
@@ -808,8 +819,8 @@ mod tests {
|
||||
"1 2 ",
|
||||
"f b ",
|
||||
]);
|
||||
expected.get_mut(0, 2).set_fg(Color::Red);
|
||||
expected.get_mut(2, 2).set_fg(Color::Red);
|
||||
expected[(0, 2)].set_fg(Color::Red);
|
||||
expected[(2, 2)].set_fg(Color::Red);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
@@ -827,7 +838,7 @@ mod tests {
|
||||
"f b ",
|
||||
]);
|
||||
for (x, y) in iproduct!(0..10, 0..3) {
|
||||
expected.get_mut(x, y).set_fg(Color::Red);
|
||||
expected[(x, y)].set_fg(Color::Red);
|
||||
}
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
@@ -958,27 +969,23 @@ mod tests {
|
||||
let mut expected = Buffer::with_lines(["label", "5████"]);
|
||||
|
||||
// first line has a yellow foreground. first cell contains italic "5"
|
||||
expected.get_mut(0, 1).modifier.insert(Modifier::ITALIC);
|
||||
expected[(0, 1)].modifier.insert(Modifier::ITALIC);
|
||||
for x in 0..5 {
|
||||
expected.get_mut(x, 1).set_fg(Color::Yellow);
|
||||
expected[(x, 1)].set_fg(Color::Yellow);
|
||||
}
|
||||
|
||||
let expected_color = if let Some(color) = bar_color {
|
||||
color
|
||||
} else {
|
||||
Color::Yellow
|
||||
};
|
||||
let expected_color = bar_color.unwrap_or(Color::Yellow);
|
||||
|
||||
// second line contains the word "label". Since the bar value is 2,
|
||||
// then the first 2 characters of "label" are italic red.
|
||||
// the rest is white (using the Bar's style).
|
||||
let cell = expected.get_mut(0, 0).set_fg(Color::Red);
|
||||
let cell = expected[(0, 0)].set_fg(Color::Red);
|
||||
cell.modifier.insert(Modifier::ITALIC);
|
||||
let cell = expected.get_mut(1, 0).set_fg(Color::Red);
|
||||
let cell = expected[(1, 0)].set_fg(Color::Red);
|
||||
cell.modifier.insert(Modifier::ITALIC);
|
||||
expected.get_mut(2, 0).set_fg(expected_color);
|
||||
expected.get_mut(3, 0).set_fg(expected_color);
|
||||
expected.get_mut(4, 0).set_fg(expected_color);
|
||||
expected[(2, 0)].set_fg(expected_color);
|
||||
expected[(3, 0)].set_fg(expected_color);
|
||||
expected[(4, 0)].set_fg(expected_color);
|
||||
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
@@ -1031,9 +1038,9 @@ mod tests {
|
||||
// bold: because of BarChart::label_style
|
||||
// red: is included with the label itself
|
||||
let mut expected = Buffer::with_lines(["2████", "G1 "]);
|
||||
let cell = expected.get_mut(0, 1).set_fg(Color::Red);
|
||||
let cell = expected[(0, 1)].set_fg(Color::Red);
|
||||
cell.modifier.insert(Modifier::BOLD);
|
||||
let cell = expected.get_mut(1, 1).set_fg(Color::Red);
|
||||
let cell = expected[(1, 1)].set_fg(Color::Red);
|
||||
cell.modifier.insert(Modifier::BOLD);
|
||||
|
||||
assert_eq!(buffer, expected);
|
||||
@@ -1,8 +1,7 @@
|
||||
use ratatui_core::prelude::{Buffer, Line, Rect, Style, 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`](crate::BarChart) widget.
|
||||
///
|
||||
/// Here is an explanation of a `Bar`'s components.
|
||||
/// ```plain
|
||||
@@ -61,7 +60,7 @@ impl<'a> Bar<'a> {
|
||||
/// display the label **under** the bar.
|
||||
/// For [`Horizontal`](crate::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::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);
|
||||
@@ -1,5 +1,6 @@
|
||||
use ratatui_core::prelude::*;
|
||||
|
||||
use super::Bar;
|
||||
use crate::prelude::*;
|
||||
|
||||
/// A group of bars to be shown by the Barchart.
|
||||
///
|
||||
@@ -78,7 +79,8 @@ impl<'a> From<&[(&'a str, u64)]> for BarGroup<'a> {
|
||||
|
||||
impl<'a, const N: usize> From<&[(&'a str, u64); N]> for BarGroup<'a> {
|
||||
fn from(value: &[(&'a str, u64); N]) -> Self {
|
||||
Self::from(value.as_ref())
|
||||
let value: &[(&'a str, u64)] = value.as_ref();
|
||||
Self::from(value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,15 +6,23 @@
|
||||
//! [title](Block::title) and [padding](Block::padding).
|
||||
|
||||
use itertools::Itertools;
|
||||
use ratatui_core::{
|
||||
prelude::{Alignment, Buffer, Line, Rect, Style, Stylize, Widget},
|
||||
style::Styled,
|
||||
symbols::border,
|
||||
widgets::WidgetRef,
|
||||
};
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
use crate::{prelude::*, symbols::border, widgets::Borders};
|
||||
use self::title::Position;
|
||||
|
||||
mod padding;
|
||||
pub mod title;
|
||||
|
||||
pub use padding::Padding;
|
||||
pub use title::{Position, Title};
|
||||
pub use title::Title;
|
||||
|
||||
use crate::Borders;
|
||||
|
||||
/// Base widget to be used to display a box border around all [upper level ones](crate::widgets).
|
||||
///
|
||||
@@ -27,14 +35,42 @@ pub use title::{Position, Title};
|
||||
/// both centered and non-centered titles are rendered, the centered space is calculated based on
|
||||
/// the full width of the block, rather than the leftover width.
|
||||
///
|
||||
/// Titles are not rendered in the corners of the block unless there is no border on that edge.
|
||||
/// If the block is too small and multiple titles overlap, the border may get cut off at a corner.
|
||||
/// Titles are not rendered in the corners of the block unless there is no border on that edge. If
|
||||
/// the block is too small and multiple titles overlap, the border may get cut off at a corner.
|
||||
///
|
||||
/// ```plain
|
||||
/// ┌With at least a left border───
|
||||
///
|
||||
/// Without left border───
|
||||
/// ```
|
||||
/// # Constructor methods
|
||||
///
|
||||
/// - [`Block::new`] creates a new [`Block`] with no border or paddings.
|
||||
/// - [`Block::bordered`] Create a new block with all borders shown.
|
||||
///
|
||||
/// # Setter methods
|
||||
///
|
||||
/// These methods are fluent setters. They return a new [`Block`] with the specified property set.
|
||||
///
|
||||
/// - [`Block::borders`] Defines which borders to display.
|
||||
/// - [`Block::border_style`] Defines the style of the borders.
|
||||
/// - [`Block::border_type`] Sets the symbols used to display the border (e.g. single line, double
|
||||
/// line, thick or rounded borders).
|
||||
/// - [`Block::padding`] Defines the padding inside a [`Block`].
|
||||
/// - [`Block::style`] Sets the base style of the widget.
|
||||
/// - [`Block::title`] Adds a title to the block.
|
||||
/// - [`Block::title_alignment`] Sets the default [`Alignment`] for all block titles.
|
||||
/// - [`Block::title_style`] Applies the style to all titles.
|
||||
/// - [`Block::title_top`] Adds a title to the top of the block.
|
||||
/// - [`Block::title_bottom`] Adds a title to the bottom of the block.
|
||||
/// - [`Block::title_position`] Adds a title to the block.
|
||||
///
|
||||
/// # Other Methods
|
||||
/// - [`Block::inner`] Compute the inner area of a block based on its border visibility rules.
|
||||
///
|
||||
/// [`Style`]s are applied first to the entire block, then to the borders, and finally to the
|
||||
/// titles. If the block is used as a container for another widget, the inner widget can also be
|
||||
/// styled. See [`Style`] for more information on how merging styles works.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
@@ -53,17 +89,34 @@ pub use title::{Position, Title};
|
||||
/// ```
|
||||
/// use ratatui::{
|
||||
/// prelude::*,
|
||||
/// widgets::{block::*, *},
|
||||
/// widgets::{
|
||||
/// block::{Position, Title},
|
||||
/// Block,
|
||||
/// },
|
||||
/// };
|
||||
///
|
||||
/// Block::new()
|
||||
/// .title("Title 1")
|
||||
/// .title(Title::from("Title 2").position(Position::Bottom));
|
||||
/// ```
|
||||
///
|
||||
/// You can also pass it as parameters of another widget so that the block surrounds them:
|
||||
/// ```
|
||||
/// use ratatui::{
|
||||
/// prelude::*,
|
||||
/// widgets::{Block, Borders, List},
|
||||
/// };
|
||||
///
|
||||
/// let surrounding_block = Block::default()
|
||||
/// .borders(Borders::ALL)
|
||||
/// .title("Here is a list of items");
|
||||
/// let items = ["Item 1", "Item 2", "Item 3"];
|
||||
/// let list = List::new(items).block(surrounding_block);
|
||||
/// ```
|
||||
#[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
|
||||
@@ -168,7 +221,7 @@ impl<'a> Block<'a> {
|
||||
border_style: Style::new(),
|
||||
border_set: BorderType::Plain.to_border_set(),
|
||||
style: Style::new(),
|
||||
padding: Padding::zero(),
|
||||
padding: Padding::ZERO,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,9 +251,9 @@ impl<'a> Block<'a> {
|
||||
/// [spans](crate::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───
|
||||
@@ -211,12 +264,15 @@ impl<'a> Block<'a> {
|
||||
/// Note: If the block is too small and multiple titles overlap, the border might get cut off at
|
||||
/// a corner.
|
||||
///
|
||||
/// # Example
|
||||
/// # Examples
|
||||
///
|
||||
/// See the [Block example] for a visual representation of how the various borders and styles
|
||||
/// look when rendered.
|
||||
///
|
||||
/// The following example demonstrates:
|
||||
/// - Default title alignment
|
||||
/// - Multiple titles (notice "Center" is centered according to the full with of the block, not
|
||||
/// the leftover space)
|
||||
/// the leftover space)
|
||||
/// - Two titles with the same alignment (notice the left titles are separated)
|
||||
/// ```
|
||||
/// use ratatui::{
|
||||
@@ -226,9 +282,9 @@ impl<'a> Block<'a> {
|
||||
///
|
||||
/// 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┐
|
||||
/// ```
|
||||
@@ -239,12 +295,27 @@ impl<'a> Block<'a> {
|
||||
/// - [`Block::title_style`]
|
||||
/// - [`Block::title_alignment`]
|
||||
/// - [`Block::title_position`]
|
||||
///
|
||||
/// # Future improvements
|
||||
///
|
||||
/// In a future release of Ratatui this method will be changed to accept `Into<Line>` instead of
|
||||
/// `Into<Title>`. This allows us to remove the unnecessary `Title` struct and store the
|
||||
/// position in the block itself. For more information see
|
||||
/// <https://github.com/ratatui/ratatui/issues/738>.
|
||||
///
|
||||
/// [Block example]: https://github.com/ratatui/ratatui/blob/main/examples/README.md#block
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn title<T>(mut self, title: T) -> Self
|
||||
where
|
||||
T: Into<Title<'a>>,
|
||||
{
|
||||
self.titles.push(title.into());
|
||||
let title = title.into();
|
||||
let position = title.position;
|
||||
let mut content = title.content;
|
||||
if let Some(alignment) = title.alignment {
|
||||
content = content.alignment(alignment);
|
||||
}
|
||||
self.titles.push((position, content));
|
||||
self
|
||||
}
|
||||
|
||||
@@ -271,8 +342,8 @@ 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
|
||||
}
|
||||
|
||||
@@ -299,17 +370,21 @@ 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
|
||||
}
|
||||
|
||||
/// Applies the style to all titles.
|
||||
///
|
||||
/// This style will be applied to all titles of the block. If a title has a style set, it will
|
||||
/// be applied after this style. This style will be applied after any [`Block::style`] or
|
||||
/// [`Block::border_style`] is applied.
|
||||
///
|
||||
/// See [`Style`] for more information on how merging styles works.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// If a [`Title`] already has a style, the title's style will add on top of this one.
|
||||
#[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();
|
||||
@@ -333,7 +408,7 @@ impl<'a> Block<'a> {
|
||||
/// 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");
|
||||
/// ```
|
||||
@@ -354,13 +429,16 @@ impl<'a> Block<'a> {
|
||||
/// ```
|
||||
/// use ratatui::{
|
||||
/// prelude::*,
|
||||
/// widgets::{block::*, *},
|
||||
/// widgets::{
|
||||
/// block::{Position, Title},
|
||||
/// 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");
|
||||
/// ```
|
||||
@@ -372,7 +450,10 @@ impl<'a> Block<'a> {
|
||||
|
||||
/// Defines the style of the borders.
|
||||
///
|
||||
/// If a [`Block::style`] is defined, `border_style` will be applied on top of it.
|
||||
/// This style is applied only to the areas covered by borders, and is applied to the block
|
||||
/// after any [`Block::style`] is applied.
|
||||
///
|
||||
/// See [`Style`] for more information on how merging styles works.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
@@ -390,16 +471,39 @@ impl<'a> Block<'a> {
|
||||
self
|
||||
}
|
||||
|
||||
/// Defines the block style.
|
||||
/// Defines the style of the entire block.
|
||||
///
|
||||
/// This is the most generic [`Style`] a block can receive, it will be merged with any other
|
||||
/// more specific style. Elements can be styled further with [`Block::title_style`] and
|
||||
/// [`Block::border_style`].
|
||||
/// more specific styles. Elements can be styled further with [`Block::title_style`] and
|
||||
/// [`Block::border_style`], which will be applied on top of this style. If the block is used as
|
||||
/// a container for another widget (e.g. a [`Paragraph`]), then the style of the widget is
|
||||
/// generally applied before this style.
|
||||
///
|
||||
/// See [`Style`] for more information on how merging styles works.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// This will also apply to the widget inside that block, unless the inner widget is styled.
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// 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.
|
||||
/// let block = Block::new()
|
||||
/// .style(Style::new().red().bold().italic())
|
||||
/// .border_style(Style::new().not_italic()) // will be red and bold
|
||||
/// .title_style(Style::new().not_bold()) // will be red and italic
|
||||
/// .title("Title");
|
||||
///
|
||||
/// // To style the inner widget, you can style the widget itself.
|
||||
/// let paragraph = Paragraph::new("Content")
|
||||
/// .block(block)
|
||||
/// .style(Style::new().white().not_bold()); // will be white, and italic
|
||||
/// ```
|
||||
///
|
||||
/// [`Paragraph`]: crate::Paragraph
|
||||
#[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();
|
||||
@@ -469,6 +573,38 @@ impl<'a> Block<'a> {
|
||||
self
|
||||
}
|
||||
|
||||
/// Defines the padding inside a `Block`.
|
||||
///
|
||||
/// See [`Padding`] for more information.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// This renders a `Block` with no padding (the default).
|
||||
/// ```
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// Block::bordered().padding(Padding::ZERO);
|
||||
/// // Renders
|
||||
/// // ┌───────┐
|
||||
/// // │content│
|
||||
/// // └───────┘
|
||||
/// ```
|
||||
///
|
||||
/// 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::*};
|
||||
/// Block::bordered().padding(Padding::horizontal(2));
|
||||
/// // Renders
|
||||
/// // ┌───────────┐
|
||||
/// // │ content │
|
||||
/// // └───────────┘
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub const fn padding(mut self, padding: Padding) -> Self {
|
||||
self.padding = padding;
|
||||
self
|
||||
}
|
||||
|
||||
/// Compute the inner area of a block based on its border visibility rules.
|
||||
///
|
||||
/// # Examples
|
||||
@@ -480,7 +616,7 @@ impl<'a> Block<'a> {
|
||||
/// 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);
|
||||
@@ -527,39 +663,7 @@ 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)
|
||||
}
|
||||
|
||||
/// Defines the padding inside a `Block`.
|
||||
///
|
||||
/// See [`Padding`] for more information.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// This renders a `Block` with no padding (the default).
|
||||
/// ```
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// Block::bordered().padding(Padding::zero());
|
||||
/// // Renders
|
||||
/// // ┌───────┐
|
||||
/// // │content│
|
||||
/// // └───────┘
|
||||
/// ```
|
||||
///
|
||||
/// 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::*};
|
||||
/// Block::bordered().padding(Padding::horizontal(2));
|
||||
/// // Renders
|
||||
/// // ┌───────────┐
|
||||
/// // │ content │
|
||||
/// // └───────────┘
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub const fn padding(mut self, padding: Padding) -> Self {
|
||||
self.padding = padding;
|
||||
self
|
||||
.any(|(pos, _)| pos.unwrap_or(self.titles_position) == position)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -628,7 +732,7 @@ impl Block<'_> {
|
||||
fn render_left_side(&self, area: Rect, buf: &mut Buffer) {
|
||||
if self.borders.contains(Borders::LEFT) {
|
||||
for y in area.top()..area.bottom() {
|
||||
buf.get_mut(area.left(), y)
|
||||
buf[(area.left(), y)]
|
||||
.set_symbol(self.border_set.vertical_left)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
@@ -638,7 +742,7 @@ impl Block<'_> {
|
||||
fn render_top_side(&self, area: Rect, buf: &mut Buffer) {
|
||||
if self.borders.contains(Borders::TOP) {
|
||||
for x in area.left()..area.right() {
|
||||
buf.get_mut(x, area.top())
|
||||
buf[(x, area.top())]
|
||||
.set_symbol(self.border_set.horizontal_top)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
@@ -649,7 +753,7 @@ impl Block<'_> {
|
||||
if self.borders.contains(Borders::RIGHT) {
|
||||
let x = area.right() - 1;
|
||||
for y in area.top()..area.bottom() {
|
||||
buf.get_mut(x, y)
|
||||
buf[(x, y)]
|
||||
.set_symbol(self.border_set.vertical_right)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
@@ -660,7 +764,7 @@ impl Block<'_> {
|
||||
if self.borders.contains(Borders::BOTTOM) {
|
||||
let y = area.bottom() - 1;
|
||||
for x in area.left()..area.right() {
|
||||
buf.get_mut(x, y)
|
||||
buf[(x, y)]
|
||||
.set_symbol(self.border_set.horizontal_bottom)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
@@ -669,7 +773,7 @@ impl Block<'_> {
|
||||
|
||||
fn render_bottom_right_corner(&self, buf: &mut Buffer, area: Rect) {
|
||||
if self.borders.contains(Borders::RIGHT | Borders::BOTTOM) {
|
||||
buf.get_mut(area.right() - 1, area.bottom() - 1)
|
||||
buf[(area.right() - 1, area.bottom() - 1)]
|
||||
.set_symbol(self.border_set.bottom_right)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
@@ -677,7 +781,7 @@ impl Block<'_> {
|
||||
|
||||
fn render_top_right_corner(&self, buf: &mut Buffer, area: Rect) {
|
||||
if self.borders.contains(Borders::RIGHT | Borders::TOP) {
|
||||
buf.get_mut(area.right() - 1, area.top())
|
||||
buf[(area.right() - 1, area.top())]
|
||||
.set_symbol(self.border_set.top_right)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
@@ -685,7 +789,7 @@ impl Block<'_> {
|
||||
|
||||
fn render_bottom_left_corner(&self, buf: &mut Buffer, area: Rect) {
|
||||
if self.borders.contains(Borders::LEFT | Borders::BOTTOM) {
|
||||
buf.get_mut(area.left(), area.bottom() - 1)
|
||||
buf[(area.left(), area.bottom() - 1)]
|
||||
.set_symbol(self.border_set.bottom_left)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
@@ -693,7 +797,7 @@ impl Block<'_> {
|
||||
|
||||
fn render_top_left_corner(&self, buf: &mut Buffer, area: Rect) {
|
||||
if self.borders.contains(Borders::LEFT | Borders::TOP) {
|
||||
buf.get_mut(area.left(), area.top())
|
||||
buf[(area.left(), area.top())]
|
||||
.set_symbol(self.border_set.top_left)
|
||||
.set_style(self.border_style);
|
||||
}
|
||||
@@ -704,7 +808,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);
|
||||
@@ -715,7 +819,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()
|
||||
@@ -725,7 +829,7 @@ impl Block<'_> {
|
||||
..titles_area
|
||||
};
|
||||
buf.set_style(title_area, self.titles_style);
|
||||
title.content.render_ref(title_area, buf);
|
||||
title.render_ref(title_area, buf);
|
||||
|
||||
// bump the width of the titles area to the left
|
||||
titles_area.width = titles_area
|
||||
@@ -747,7 +851,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
|
||||
|
||||
@@ -760,13 +864,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_ref(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);
|
||||
@@ -783,13 +887,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_ref(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);
|
||||
@@ -802,11 +906,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
|
||||
@@ -827,6 +932,35 @@ impl Block<'_> {
|
||||
height: 1,
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate the left, and right space the [`Block`] will take up.
|
||||
///
|
||||
/// The result takes the [`Block`]'s, [`Borders`], and [`Padding`] into account.
|
||||
pub(crate) fn horizontal_space(&self) -> (u16, u16) {
|
||||
let left = self
|
||||
.padding
|
||||
.left
|
||||
.saturating_add(u16::from(self.borders.contains(Borders::LEFT)));
|
||||
let right = self
|
||||
.padding
|
||||
.right
|
||||
.saturating_add(u16::from(self.borders.contains(Borders::RIGHT)));
|
||||
(left, right)
|
||||
}
|
||||
|
||||
/// Calculate the top, and bottom space that the [`Block`] will take up.
|
||||
///
|
||||
/// Takes the [`Padding`], [`Title`]'s position, and the [`Borders`] that are selected into
|
||||
/// account when calculating the result.
|
||||
pub(crate) fn vertical_space(&self) -> (u16, u16) {
|
||||
let has_top =
|
||||
self.borders.contains(Borders::TOP) || self.has_title_at_position(Position::Top);
|
||||
let top = self.padding.top + u16::from(has_top);
|
||||
let has_bottom =
|
||||
self.borders.contains(Borders::BOTTOM) || self.has_title_at_position(Position::Bottom);
|
||||
let bottom = self.padding.bottom + u16::from(has_bottom);
|
||||
(top, bottom)
|
||||
}
|
||||
}
|
||||
|
||||
/// An extension trait for [`Block`] that provides some convenience methods.
|
||||
@@ -860,6 +994,7 @@ impl<'a> Styled for Block<'a> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ratatui_core::style::{Color, Modifier};
|
||||
use rstest::rstest;
|
||||
use strum::ParseError;
|
||||
|
||||
@@ -911,24 +1046,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);
|
||||
}
|
||||
|
||||
@@ -938,32 +1066,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"))
|
||||
@@ -971,6 +1100,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))
|
||||
@@ -979,6 +1109,117 @@ mod tests {
|
||||
assert!(block.has_title_at_position(Position::Bottom));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::none(Borders::NONE, (0, 0))]
|
||||
#[case::top(Borders::TOP, (1, 0))]
|
||||
#[case::right(Borders::RIGHT, (0, 0))]
|
||||
#[case::bottom(Borders::BOTTOM, (0, 1))]
|
||||
#[case::left(Borders::LEFT, (0, 0))]
|
||||
#[case::top_right(Borders::TOP | Borders::RIGHT, (1, 0))]
|
||||
#[case::top_bottom(Borders::TOP | Borders::BOTTOM, (1, 1))]
|
||||
#[case::top_left(Borders::TOP | Borders::LEFT, (1, 0))]
|
||||
#[case::bottom_right(Borders::BOTTOM | Borders::RIGHT, (0, 1))]
|
||||
#[case::bottom_left(Borders::BOTTOM | Borders::LEFT, (0, 1))]
|
||||
#[case::left_right(Borders::LEFT | Borders::RIGHT, (0, 0))]
|
||||
fn vertical_space_takes_into_account_borders(
|
||||
#[case] borders: Borders,
|
||||
#[case] vertical_space: (u16, u16),
|
||||
) {
|
||||
let block = Block::new().borders(borders);
|
||||
assert_eq!(block.vertical_space(), vertical_space);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::top_border_top_p1(Borders::TOP, Padding::new(0, 0, 1, 0), (2, 0))]
|
||||
#[case::right_border_top_p1(Borders::RIGHT, Padding::new(0, 0, 1, 0), (1, 0))]
|
||||
#[case::bottom_border_top_p1(Borders::BOTTOM, Padding::new(0, 0, 1, 0), (1, 1))]
|
||||
#[case::left_border_top_p1(Borders::LEFT, Padding::new(0, 0, 1, 0), (1, 0))]
|
||||
#[case::top_bottom_border_all_p3(Borders::TOP | Borders::BOTTOM, Padding::new(100, 100, 4, 5), (5, 6))]
|
||||
#[case::no_border(Borders::NONE, Padding::new(100, 100, 10, 13), (10, 13))]
|
||||
#[case::all(Borders::ALL, Padding::new(100, 100, 1, 3), (2, 4))]
|
||||
fn vertical_space_takes_into_account_padding(
|
||||
#[case] borders: Borders,
|
||||
#[case] padding: Padding,
|
||||
#[case] vertical_space: (u16, u16),
|
||||
) {
|
||||
let block = Block::new().borders(borders).padding(padding);
|
||||
assert_eq!(block.vertical_space(), vertical_space);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vertical_space_takes_into_account_titles() {
|
||||
let block = Block::new().title_top("Test");
|
||||
assert_eq!(block.vertical_space(), (1, 0));
|
||||
|
||||
let block = Block::new().title_bottom("Test");
|
||||
assert_eq!(block.vertical_space(), (0, 1));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::top_border_top_title(Block::new(), Borders::TOP, Position::Top, (1, 0))]
|
||||
#[case::right_border_top_title(Block::new(), Borders::RIGHT, Position::Top, (1, 0))]
|
||||
#[case::bottom_border_top_title(Block::new(), Borders::BOTTOM, Position::Top, (1, 1))]
|
||||
#[case::left_border_top_title(Block::new(), Borders::LEFT, Position::Top, (1, 0))]
|
||||
#[case::top_border_top_title(Block::new(), Borders::TOP, Position::Bottom, (1, 1))]
|
||||
#[case::right_border_top_title(Block::new(), Borders::RIGHT, Position::Bottom, (0, 1))]
|
||||
#[case::bottom_border_top_title(Block::new(), Borders::BOTTOM, Position::Bottom, (0, 1))]
|
||||
#[case::left_border_top_title(Block::new(), Borders::LEFT, Position::Bottom, (0, 1))]
|
||||
fn vertical_space_takes_into_account_borders_and_title(
|
||||
#[case] block: Block,
|
||||
#[case] borders: Borders,
|
||||
#[case] pos: Position,
|
||||
#[case] vertical_space: (u16, u16),
|
||||
) {
|
||||
let block = block.borders(borders).title_position(pos).title("Test");
|
||||
assert_eq!(block.vertical_space(), vertical_space);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn horizontal_space_takes_into_account_borders() {
|
||||
let block = Block::bordered();
|
||||
assert_eq!(block.horizontal_space(), (1, 1));
|
||||
|
||||
let block = Block::new().borders(Borders::LEFT);
|
||||
assert_eq!(block.horizontal_space(), (1, 0));
|
||||
|
||||
let block = Block::new().borders(Borders::RIGHT);
|
||||
assert_eq!(block.horizontal_space(), (0, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn horizontal_space_takes_into_account_padding() {
|
||||
let block = Block::new().padding(Padding::new(1, 1, 100, 100));
|
||||
assert_eq!(block.horizontal_space(), (1, 1));
|
||||
|
||||
let block = Block::new().padding(Padding::new(3, 5, 0, 0));
|
||||
assert_eq!(block.horizontal_space(), (3, 5));
|
||||
|
||||
let block = Block::new().padding(Padding::new(0, 1, 100, 100));
|
||||
assert_eq!(block.horizontal_space(), (0, 1));
|
||||
|
||||
let block = Block::new().padding(Padding::new(1, 0, 100, 100));
|
||||
assert_eq!(block.horizontal_space(), (1, 0));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::all_bordered_all_padded(Block::bordered(), Padding::new(1, 1, 1, 1), (2, 2))]
|
||||
#[case::all_bordered_left_padded(Block::bordered(), Padding::new(1, 0, 0, 0), (2, 1))]
|
||||
#[case::all_bordered_right_padded(Block::bordered(), Padding::new(0, 1, 0, 0), (1, 2))]
|
||||
#[case::all_bordered_top_padded(Block::bordered(), Padding::new(0, 0, 1, 0), (1, 1))]
|
||||
#[case::all_bordered_bottom_padded(Block::bordered(), Padding::new(0, 0, 0, 1), (1, 1))]
|
||||
#[case::left_bordered_left_padded(Block::new().borders(Borders::LEFT), Padding::new(1, 0, 0, 0), (2, 0))]
|
||||
#[case::left_bordered_right_padded(Block::new().borders(Borders::LEFT), Padding::new(0, 1, 0, 0), (1, 1))]
|
||||
#[case::right_bordered_right_padded(Block::new().borders(Borders::RIGHT), Padding::new(0, 1, 0, 0), (0, 2))]
|
||||
#[case::right_bordered_left_padded(Block::new().borders(Borders::RIGHT), Padding::new(1, 0, 0, 0), (1, 1))]
|
||||
fn horizontal_space_takes_into_account_borders_and_padding(
|
||||
#[case] block: Block,
|
||||
#[case] padding: Padding,
|
||||
#[case] horizontal_space: (u16, u16),
|
||||
) {
|
||||
let block = block.padding(padding);
|
||||
assert_eq!(block.horizontal_space(), horizontal_space);
|
||||
}
|
||||
|
||||
#[test]
|
||||
const fn border_type_can_be_const() {
|
||||
const _PLAIN: border::Set = BorderType::border_symbols(BorderType::Plain);
|
||||
@@ -997,7 +1238,7 @@ mod tests {
|
||||
border_style: Style::new(),
|
||||
border_set: BorderType::Plain.to_border_set(),
|
||||
style: Style::new(),
|
||||
padding: Padding::zero(),
|
||||
padding: Padding::ZERO,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -1012,7 +1253,7 @@ mod tests {
|
||||
// .border_style(_DEFAULT_STYLE) // no longer const
|
||||
// .title_style(_DEFAULT_STYLE) // no longer const
|
||||
.title_alignment(Alignment::Left)
|
||||
.title_position(Position::Top)
|
||||
.title_position(crate::block::Position::Top)
|
||||
.padding(_DEFAULT_PADDING);
|
||||
}
|
||||
|
||||
@@ -1078,6 +1319,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))
|
||||
@@ -1143,13 +1385,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));
|
||||
@@ -19,8 +19,8 @@
|
||||
/// Padding::symmetric(5, 6);
|
||||
/// ```
|
||||
///
|
||||
/// [`Block`]: crate::widgets::Block
|
||||
/// [`padding`]: crate::widgets::Block::padding
|
||||
/// [`Block`]: crate::Block
|
||||
/// [`padding`]: crate::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 {
|
||||
@@ -35,6 +35,14 @@ pub struct Padding {
|
||||
}
|
||||
|
||||
impl Padding {
|
||||
/// `Padding` with all fields set to `0`
|
||||
pub const ZERO: Self = Self {
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
};
|
||||
|
||||
/// Creates a new `Padding` by specifying every field individually.
|
||||
///
|
||||
/// Note: the order of the fields does not match the order of the CSS properties.
|
||||
@@ -48,13 +56,9 @@ impl Padding {
|
||||
}
|
||||
|
||||
/// Creates a `Padding` with all fields set to `0`.
|
||||
#[deprecated = "use Padding::ZERO"]
|
||||
pub const fn zero() -> Self {
|
||||
Self {
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
}
|
||||
Self::ZERO
|
||||
}
|
||||
|
||||
/// Creates a `Padding` with the same value for `left` and `right`.
|
||||
@@ -173,7 +177,6 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn constructors() {
|
||||
assert_eq!(Padding::zero(), Padding::new(0, 0, 0, 0));
|
||||
assert_eq!(Padding::horizontal(1), Padding::new(1, 1, 0, 0));
|
||||
assert_eq!(Padding::vertical(1), Padding::new(0, 0, 1, 1));
|
||||
assert_eq!(Padding::uniform(1), Padding::new(1, 1, 1, 1));
|
||||
@@ -189,7 +192,6 @@ mod tests {
|
||||
const fn can_be_const() {
|
||||
const _PADDING: Padding = Padding::new(1, 1, 1, 1);
|
||||
const _UNI_PADDING: Padding = Padding::uniform(1);
|
||||
const _NO_PADDING: Padding = Padding::zero();
|
||||
const _HORIZONTAL: Padding = Padding::horizontal(1);
|
||||
const _VERTICAL: Padding = Padding::vertical(1);
|
||||
const _PROPORTIONAL: Padding = Padding::proportional(1);
|
||||
@@ -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) 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) 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::title_top) or
|
||||
/// [`Block::title_bottom`](crate::Block::title_bottom) instead.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// Title with no style.
|
||||
@@ -36,7 +46,10 @@ use crate::{layout::Alignment, text::Line};
|
||||
/// ```
|
||||
/// use ratatui::{
|
||||
/// prelude::*,
|
||||
/// widgets::{block::*, *},
|
||||
/// widgets::{
|
||||
/// block::{Position, Title},
|
||||
/// Block,
|
||||
/// },
|
||||
/// };
|
||||
///
|
||||
/// Title::from("Title")
|
||||
@@ -50,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::title_alignment) in the associated
|
||||
/// [`Block`](crate::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::title_position) in the associated
|
||||
/// [`Block`](crate::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`].
|
||||
@@ -85,6 +98,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"]
|
||||
@@ -55,7 +55,7 @@ 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::bordered) instead.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
@@ -10,9 +10,13 @@
|
||||
//! [`Monthly`] has several controls for what should be displayed
|
||||
use std::collections::HashMap;
|
||||
|
||||
use ratatui_core::{
|
||||
prelude::{Alignment, Buffer, Color, Constraint, Layout, Line, Rect, Span, Style, Widget},
|
||||
widgets::WidgetRef,
|
||||
};
|
||||
use time::{Date, Duration, OffsetDateTime};
|
||||
|
||||
use crate::{prelude::*, widgets::Block};
|
||||
use crate::{block::BlockExt, Block};
|
||||
|
||||
/// Display a month calendar for the month containing `display_date`
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
@@ -177,7 +181,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;
|
||||
}
|
||||
}
|
||||
@@ -203,7 +209,7 @@ impl CalendarEventStore {
|
||||
let mut res = Self::default();
|
||||
res.add(
|
||||
OffsetDateTime::now_local()
|
||||
.unwrap_or(OffsetDateTime::now_utc())
|
||||
.unwrap_or_else(|_| OffsetDateTime::now_utc())
|
||||
.date(),
|
||||
style.into(),
|
||||
);
|
||||
@@ -22,6 +22,12 @@ mod world;
|
||||
use std::{fmt, iter::zip};
|
||||
|
||||
use itertools::Itertools;
|
||||
use ratatui_core::{
|
||||
prelude::{Buffer, Color, Rect, Style, Widget},
|
||||
symbols::{self, Marker},
|
||||
text::Line as TextLine,
|
||||
widgets::WidgetRef,
|
||||
};
|
||||
|
||||
pub use self::{
|
||||
circle::Circle,
|
||||
@@ -30,7 +36,7 @@ pub use self::{
|
||||
points::Points,
|
||||
rectangle::Rectangle,
|
||||
};
|
||||
use crate::{prelude::*, text::Line as TextLine, widgets::Block};
|
||||
use crate::block::{Block, BlockExt};
|
||||
|
||||
/// Something that can be drawn on a [`Canvas`].
|
||||
///
|
||||
@@ -425,7 +431,7 @@ impl<'a, 'b> From<&'a mut Context<'b>> for Painter<'a, 'b> {
|
||||
/// 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
|
||||
/// [`Frame`]: ratatui_core::prelude::Frame
|
||||
#[derive(Debug)]
|
||||
pub struct Context<'a> {
|
||||
x_bounds: [f64; 2],
|
||||
@@ -464,17 +470,17 @@ impl<'a> Context<'a> {
|
||||
height: u16,
|
||||
x_bounds: [f64; 2],
|
||||
y_bounds: [f64; 2],
|
||||
marker: symbols::Marker,
|
||||
marker: Marker,
|
||||
) -> Self {
|
||||
let dot = symbols::DOT.chars().next().unwrap();
|
||||
let block = symbols::block::FULL.chars().next().unwrap();
|
||||
let bar = symbols::bar::HALF.chars().next().unwrap();
|
||||
let grid: Box<dyn Grid> = match marker {
|
||||
symbols::Marker::Dot => Box::new(CharGrid::new(width, height, dot)),
|
||||
symbols::Marker::Block => Box::new(CharGrid::new(width, height, block)),
|
||||
symbols::Marker::Bar => Box::new(CharGrid::new(width, height, bar)),
|
||||
symbols::Marker::Braille => Box::new(BrailleGrid::new(width, height)),
|
||||
symbols::Marker::HalfBlock => Box::new(HalfBlockGrid::new(width, height)),
|
||||
Marker::Dot => Box::new(CharGrid::new(width, height, dot)),
|
||||
Marker::Block => Box::new(CharGrid::new(width, height, block)),
|
||||
Marker::Bar => Box::new(CharGrid::new(width, height, bar)),
|
||||
Marker::Braille => Box::new(BrailleGrid::new(width, height)),
|
||||
Marker::HalfBlock => Box::new(HalfBlockGrid::new(width, height)),
|
||||
};
|
||||
Self {
|
||||
x_bounds,
|
||||
@@ -604,7 +610,7 @@ where
|
||||
y_bounds: [f64; 2],
|
||||
paint_func: Option<F>,
|
||||
background_color: Color,
|
||||
marker: symbols::Marker,
|
||||
marker: Marker,
|
||||
}
|
||||
|
||||
impl<'a, F> Default for Canvas<'a, F>
|
||||
@@ -618,7 +624,7 @@ where
|
||||
y_bounds: [0.0, 0.0],
|
||||
paint_func: None,
|
||||
background_color: Color::Reset,
|
||||
marker: symbols::Marker::Braille,
|
||||
marker: Marker::Braille,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -717,7 +723,7 @@ where
|
||||
/// .paint(|ctx| {});
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub const fn marker(mut self, marker: symbols::Marker) -> Self {
|
||||
pub const fn marker(mut self, marker: Marker) -> Self {
|
||||
self.marker = marker;
|
||||
self
|
||||
}
|
||||
@@ -771,7 +777,7 @@ where
|
||||
(index % width) as u16 + canvas_area.left(),
|
||||
(index / width) as u16 + canvas_area.top(),
|
||||
);
|
||||
let cell = buf.get_mut(x, y).set_char(ch);
|
||||
let cell = buf[(x, y)].set_char(ch);
|
||||
if colors.0 != Color::Reset {
|
||||
cell.set_fg(colors.0);
|
||||
}
|
||||
@@ -809,17 +815,15 @@ 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
|
||||
fn test_marker(marker: Marker, expected: &str) {
|
||||
let area = Rect::new(0, 0, 5, 5);
|
||||
let mut cell = Cell::default();
|
||||
cell.set_char('x');
|
||||
let mut buf = Buffer::filled(area, &cell);
|
||||
let mut buf = Buffer::filled(area, Cell::new("x"));
|
||||
let horizontal_line = Line {
|
||||
x1: 0.0,
|
||||
y1: 0.0,
|
||||
@@ -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));
|
||||
@@ -1,7 +1,6 @@
|
||||
use crate::{
|
||||
style::Color,
|
||||
widgets::canvas::{Painter, Shape},
|
||||
};
|
||||
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)]
|
||||
@@ -112,10 +111,18 @@ fn draw_line_high(painter: &mut Painter, x1: usize, y1: usize, x2: usize, y2: us
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ratatui_core::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::{Style, Stylize},
|
||||
symbols::Marker,
|
||||
text,
|
||||
widgets::Widget,
|
||||
};
|
||||
use rstest::rstest;
|
||||
|
||||
use super::{super::*, *};
|
||||
use crate::{buffer::Buffer, layout::Rect};
|
||||
use super::*;
|
||||
use crate::canvas::Canvas;
|
||||
|
||||
#[rstest]
|
||||
#[case::off_grid(&Line::new(-1.0, -1.0, 10.0, 10.0, Color::Red), [" "; 10])]
|
||||
@@ -201,7 +208,7 @@ 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<text::Line<'expected_line>>,
|
||||
{
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 10));
|
||||
let canvas = Canvas::default()
|
||||
@@ -1,13 +1,10 @@
|
||||
use ratatui_core::style::Color;
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
use crate::{
|
||||
style::Color,
|
||||
widgets::canvas::{
|
||||
world::{WORLD_HIGH_RESOLUTION, WORLD_LOW_RESOLUTION},
|
||||
Painter, Shape,
|
||||
},
|
||||
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.
|
||||
@@ -62,10 +59,14 @@ impl Shape for Map {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ratatui_core::{
|
||||
prelude::{Buffer, Color, Rect, Widget},
|
||||
symbols::Marker,
|
||||
};
|
||||
use strum::ParseError;
|
||||
|
||||
use super::*;
|
||||
use crate::{prelude::*, widgets::canvas::Canvas};
|
||||
use crate::canvas::Canvas;
|
||||
|
||||
#[test]
|
||||
fn map_resolution_to_string() {
|
||||
@@ -1,7 +1,6 @@
|
||||
use crate::{
|
||||
style::Color,
|
||||
widgets::canvas::{Painter, Shape},
|
||||
};
|
||||
use ratatui_core::style::Color;
|
||||
|
||||
use crate::canvas::{Painter, Shape};
|
||||
|
||||
/// A group of points with a given color
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
@@ -1,7 +1,6 @@
|
||||
use crate::{
|
||||
style::Color,
|
||||
widgets::canvas::{Line, Painter, Shape},
|
||||
};
|
||||
use ratatui_core::style::Color;
|
||||
|
||||
use crate::canvas::{Line, Painter, Shape};
|
||||
|
||||
/// A rectangle to draw on a [`Canvas`](super::Canvas)
|
||||
///
|
||||
@@ -65,8 +64,10 @@ impl Shape for Rectangle {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ratatui_core::{prelude::*, symbols::Marker};
|
||||
|
||||
use super::*;
|
||||
use crate::{prelude::*, widgets::canvas::Canvas};
|
||||
use crate::canvas::Canvas;
|
||||
|
||||
#[test]
|
||||
fn draw_block_lines() {
|
||||
@@ -98,7 +99,7 @@ mod tests {
|
||||
"██████████",
|
||||
]);
|
||||
expected.set_style(buffer.area, Style::new().red());
|
||||
expected.set_style(buffer.area.inner(&Margin::new(1, 1)), Style::reset());
|
||||
expected.set_style(buffer.area.inner(Margin::new(1, 1)), Style::reset());
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
@@ -132,8 +133,8 @@ mod tests {
|
||||
"█▄▄▄▄▄▄▄▄█",
|
||||
]);
|
||||
expected.set_style(buffer.area, Style::new().red().on_red());
|
||||
expected.set_style(buffer.area.inner(&Margin::new(1, 0)), Style::reset().red());
|
||||
expected.set_style(buffer.area.inner(&Margin::new(1, 1)), Style::reset());
|
||||
expected.set_style(buffer.area.inner(Margin::new(1, 0)), Style::reset().red());
|
||||
expected.set_style(buffer.area.inner(Margin::new(1, 1)), Style::reset());
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
@@ -176,8 +177,8 @@ mod tests {
|
||||
"⣇⣀⣀⣀⣀⣀⣀⣀⣀⣸",
|
||||
]);
|
||||
expected.set_style(buffer.area, Style::new().red());
|
||||
expected.set_style(buffer.area.inner(&Margin::new(1, 1)), Style::new().green());
|
||||
expected.set_style(buffer.area.inner(&Margin::new(2, 2)), Style::reset());
|
||||
expected.set_style(buffer.area.inner(Margin::new(1, 1)), Style::new().green());
|
||||
expected.set_style(buffer.area.inner(Margin::new(2, 2)), Style::reset());
|
||||
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