Compare commits
1 Commits
main
...
release-pl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9614ee96e7 |
115
.cliffignore
115
.cliffignore
@@ -1,115 +0,0 @@
|
||||
# skip entries from <https://github.com/ratatui/ratatui/pull/1652>
|
||||
50af9a5d80ed5446f3e6cc554911f606580edde9
|
||||
272e9709c6eed45cd7e0c183624b7898f4e0ae69
|
||||
adc8fdc35aa57d6dad2ae8dd30ec2e9256576c09
|
||||
31711dbf82a4c7bb3b78692da34d9f469725dd6e
|
||||
b6356aa7a529a491d63dd6628b8985adae337f16
|
||||
885558b6f89df642317d39c5b44c94c742d1e0c8
|
||||
6440eb9d76379340953420629ab0a3d9039d6c48
|
||||
3870583ea868452191857f9bda97a3d5c35d0a4f
|
||||
487edc8399683fb8a9a66359729c015780d248f2
|
||||
250c222cc4aaab09184a28efc68f75ca03133794
|
||||
590a392ab11c1a215767614931036781f4cf6a29
|
||||
6443f7408af4a8834bc68cd35d2ba9be47e45f38
|
||||
8339cce10a51c9c951b3c9750d527d80168626eb
|
||||
ac342231c344e893f2f630ee38167aab28c736a6
|
||||
0fb103680028a6e26a1923f87b60bde51acaec4f
|
||||
4335a90a00aeeedb69a92442c7f2711727944017
|
||||
71480242a926f98e9081ed6e2dc8c381757b3a42
|
||||
e1a31db55913bd690bcaef380e9dbc3b6a5dc175
|
||||
38490ff8da6f11e309ba1bcb3e88a562f7c953c5
|
||||
a4bb143e4767137d6a9e9927d3da66562611f86f
|
||||
bc73f2dcbe5ea48fc4d1555a8e931f40d7b0e03f
|
||||
77dea441e5637b1c428c2aa71ea67fb3aac20c12
|
||||
06af14107e70e49a4cba3babb8ab0a0c57b4bdf8
|
||||
9acdab32df69517b93dd2b861b85586d47c71540
|
||||
2e684c6500be61fbe69744d183c8086564ac0051
|
||||
b7f8ec0ff9659473d936eae53a57cd9de38cec1f
|
||||
31a2c4548c304270a8c852f19baa7a4eaac5e75c
|
||||
57b681b053c019b66e0ed92959638997fea731b1
|
||||
131b9ec41751163d43d94564363247b60f031486
|
||||
8b32f82b4dd526580d00fa13f053bf507e8ea933
|
||||
76d1e5b1733d47a7f05acf563db26cb1a66b540d
|
||||
57a0a34f924e0d488c9e9f917900e677c3488dc4
|
||||
12aa58601ddd0704256c56019bd2c7139d41f7a2
|
||||
2dc81833c60951d16f9bd60f3da003edfc9a11f5
|
||||
20e41f1d1da6db8abbc2504814531d4d97bdf94b
|
||||
68b55a12a29e70ebfcc063c2d5d5845de3b5a27e
|
||||
693003314a25e792ffc5d53146b28bfb6a4582e3
|
||||
63441e259bc38b56e0369197bed14788b2cb3d54
|
||||
fa88152c808eeb6c9d9b3662361aab1e57e1b1bf
|
||||
30d9daa59b1843786cde00e25c3e69cfe818b80b
|
||||
92540b2f6ab25f3a5400aebb28af3c498ac793a4
|
||||
29edc3a7a38c512611a80cf5d8d42027558419b2
|
||||
819499d6ffc0e8453ed3220067645933a4882a74
|
||||
4756526829a4e849d9e256b6cf821eb66afe3ade
|
||||
8f35437d5ad78d31cd45c4af888f20f0b5ab4196
|
||||
39bd72b1f702dadb1ebbaf4e77ad2fada166ac49
|
||||
c6d2cee3e967c9234176c5229858512b0c79d6a9
|
||||
bd90e3d928e0f9f0b915933ebbc32c2256fe8cfa
|
||||
b820c0c7e4c576c1e39b5e482a8aac08076a039c
|
||||
74194759756bb111b4da3e9a5cdb968275a2fab0
|
||||
38b2f27efe0e1829bc503df7fd64b94b7bb80d97
|
||||
21aa3232d762d6e3f81f15fc5b66ba462385ac05
|
||||
903bb0ae32d22393783edfda96db900739864f0a
|
||||
5cee13ab6d9c49751cf9283d9099e37f0cc3632c
|
||||
0a0997702dd4cf2217160f5652f5c39cbd4a1010
|
||||
778f2f5ec511bef431b54157242b91d083ea9840
|
||||
7d23bd2ceaf96e81972b5f746fdcba0d17f6391f
|
||||
2c02a56bce31519386303571e0b66b7d4beb378e
|
||||
f33d51e7d9ccf9fe52ec3289d04d97c722d9ee17
|
||||
91cd81aaa032887bb2327bc3fe3cad6b3c9fbacf
|
||||
5d5a1ccb0b4e2f293f215ce026fba33f1c069689
|
||||
6b9417db5f2adbdc60e9dd8dc5acbbe2a1f77ba9
|
||||
dce1e4b138eb1333c9e773bacd579a8cdddd73fb
|
||||
b4aacb045e2200896b0d0136a2b8688b47828d73
|
||||
dcba0bcd5d5d6e33ddc1fa94ebb94819fdda600e
|
||||
6f52350ecfb62e3a5bac16f0824e74b757cc6cd2
|
||||
2c6f324b9aa5034771e00758b143fd8df94d859d
|
||||
bf0210602948f8d26ae323996fe7b22fb218a446
|
||||
07aff91b015b5e7e0504680c12edbce70d7dba1e
|
||||
f6d49dde14af73ed467d75d8f6ec0f502db2908f
|
||||
9a7467b30576d5cb7491ea6e09efcae97eadf9bb
|
||||
a0c35f1d7bcce10e092582b95f5b0a3f20ad7bf3
|
||||
d24747d46982192b575a40b8cc18d1c948fac3d7
|
||||
8060f7bc578b29dde6ca0c4c64569f9c73218f46
|
||||
0dc5b2d2e0aa6438ffc1b3965b1ab31c721adbcc
|
||||
8ecdd892f53d7db95bbb53a61700d36e3fcefdd4
|
||||
570c35868147a2400a13331e85d562d1ef96a011
|
||||
3855c3a84a77037aeee40dbe9e52454fb1f9afee
|
||||
93372f35c1669da0138ca776890f3ff3d38a6539
|
||||
6cf08d4a2f0398856fd593f50bf077fd59b08230
|
||||
f78d3bfec32d07c1124eee8d0249477ce3fb0884
|
||||
204307fa50aaaa373946342084f7fd3af39f3cd3
|
||||
c50b01d098e5ab405a50c3e14e858da27d606e8f
|
||||
f71d1ac73e8290f37d55a67d6a6507a3653ec174
|
||||
ae2868c0e0b1ac8b5126fd43269383fa533d87b5
|
||||
be8def963956c605bca28bcd8df673bd7ec3740b
|
||||
4ac4d9d3ab97176d71e287bcdd6d41e66f2f7ccb
|
||||
fafabb8dab84e9460a076199ea646262e51c855b
|
||||
2f97d35bd8618e8c0cc006cb1d4a9b151c1b9b4a
|
||||
39d5a745acbbd3510de707d4e7c471c17e02ae59
|
||||
a1acdcdc4c002390a76f01699cfa006a36cf3f56
|
||||
ad54cf29ad1a4335ba208fb94a8fc5dfbba260e3
|
||||
c7649575e7b199794be4252f79da80aaecdcee28
|
||||
8913e2ce1f40d451ddb4527f08ec75f198d5063c
|
||||
1b9e310300f22bfc72364f027a9caeddadf61a99
|
||||
89d7dd46031511f0556b2d29ab34035f42e3a24c
|
||||
ca4fa0b9bf5ba707aa0447ba7c38fbacdadb7eec
|
||||
8cecfdf2f6dd5b0de507f79b469517cc0fb42add
|
||||
7eeb6afb3dbc56e52f9387a74f826b186cd19137
|
||||
d0f75eb371a96f8d5f174e23de074efd840e9e44
|
||||
f8a70ea9da8e6df2bf7a5f74cce45615fc292afe
|
||||
f28b9730061bffadb9d87ad63edf7d10b245d2c1
|
||||
afc5cf2140f22fea6bd6933dd0f9c302229a1980
|
||||
b75df78cdca58d5dca0c51fb8e106067aa6cb752
|
||||
28f5a6dbd4091aa3efa86eed6767eeb44a655f0f
|
||||
345e6a1ebd853858463a33953585ce407a60378c
|
||||
c45a4de47c601554f6b981d211181468b4798e41
|
||||
bbaa9a5432ff6ad5518344123c3b56f349347e99
|
||||
f804c90f96221f334371ccd01b0e6df7b1cfc1e8
|
||||
16ba867c5877d8c97968987ecb5f8bff966d0a82
|
||||
38a1474ca12aa6a796afc1e277882d997a999e14
|
||||
92c4078413fc79fcc83f5d3d8708abb58696ff1a
|
||||
d4415204e1eb3aed2a74a722aeaaa274975dd2d7
|
||||
e48bcf5f21f14acb27996fdc02231c140f5b817c
|
||||
17
.github/ISSUE_TEMPLATE/bug_report.md
vendored
17
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -13,43 +13,27 @@ A detailed and complete issue is more likely to be processed quickly.
|
||||
-->
|
||||
|
||||
## Description
|
||||
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
|
||||
## Are you willing to contribute a fix?
|
||||
|
||||
<!--
|
||||
If you would like to work on a fix, check one of the boxes below. Maintainers can help point
|
||||
you to the right place in the codebase.
|
||||
-->
|
||||
|
||||
- [ ] I am willing to open a PR for this bug.
|
||||
- [ ] I can try to investigate, but I will need guidance.
|
||||
- [ ] I am not able to work on a fix right now.
|
||||
|
||||
## Environment
|
||||
|
||||
<!--
|
||||
Add a description of the systems where you are observing the issue. For example:
|
||||
- OS: Linux
|
||||
@@ -66,7 +50,6 @@ Add a description of the systems where you are observing the issue. For example:
|
||||
- Backend:
|
||||
|
||||
## Additional context
|
||||
|
||||
<!--
|
||||
Add any other context about the problem here.
|
||||
If you already looked into the issue, include all the leads you have explored.
|
||||
|
||||
15
.github/ISSUE_TEMPLATE/feature_request.md
vendored
15
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -8,13 +8,11 @@ assignees: ''
|
||||
---
|
||||
|
||||
## Problem
|
||||
|
||||
<!--
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
-->
|
||||
|
||||
## Solution
|
||||
|
||||
<!--
|
||||
A clear and concise description of what you want to happen.
|
||||
Things to consider:
|
||||
@@ -24,24 +22,11 @@ Things to consider:
|
||||
-->
|
||||
|
||||
## Alternatives
|
||||
|
||||
<!--
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
-->
|
||||
|
||||
## Are you willing to contribute an implementation?
|
||||
|
||||
<!--
|
||||
If you would like to work on this, check one of the boxes below. Maintainers can help refine
|
||||
the scope and discuss approach.
|
||||
-->
|
||||
|
||||
- [ ] I am willing to open a PR implementing this.
|
||||
- [ ] I can try to implement it, but I will need guidance.
|
||||
- [ ] I am not able to implement this right now.
|
||||
|
||||
## Additional context
|
||||
|
||||
<!--
|
||||
Add any other context or screenshots about the feature request here.
|
||||
-->
|
||||
|
||||
4
.github/workflows/check-pr.yml
vendored
4
.github/workflows/check-pr.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
steps:
|
||||
- name: Check PR title
|
||||
if: github.event_name == 'pull_request_target'
|
||||
uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v5
|
||||
uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5
|
||||
id: check_pr_title
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
fi
|
||||
- name: Add label
|
||||
if: steps.check_breaking_change.outputs.breaking_change == 'true'
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
|
||||
2
.github/workflows/check-semver.yml
vendored
2
.github/workflows/check-semver.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Check semver
|
||||
|
||||
120
.github/workflows/ci.yml
vendored
120
.github/workflows/ci.yml
vendored
@@ -29,15 +29,15 @@ jobs:
|
||||
name: Check Formatting
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master
|
||||
- uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b # master
|
||||
with:
|
||||
toolchain: nightly
|
||||
components: rustfmt
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
|
||||
- uses: taiki-e/install-action@3522286d40783523f9c7880e33f785905b4c20d0 # v2
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2
|
||||
- uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2
|
||||
with:
|
||||
tool: taplo-cli
|
||||
- run: cargo xtask format --check
|
||||
@@ -48,32 +48,27 @@ jobs:
|
||||
name: Check Typos
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: crate-ci/typos@bb4666ad77b539a6b4ce4eda7ebb6de553704021 # master
|
||||
- uses: crate-ci/typos@392b78fe18a52790c53f42456e46124f77346842 # master
|
||||
|
||||
# Check for any disallowed dependencies in the codebase due to license / security issues.
|
||||
# See <https://github.com/EmbarkStudios/cargo-deny>
|
||||
cargo-deny:
|
||||
dependencies:
|
||||
name: Check Dependencies
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
checks:
|
||||
- advisories
|
||||
- bans licenses sources
|
||||
# Prevent sudden announcement of a new advisory from failing ci:
|
||||
continue-on-error: ${{ matrix.checks == 'advisories' }}
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: EmbarkStudios/cargo-deny-action@3fd3802e88374d3fe9159b834c7714ec57d6c979 # v2
|
||||
- uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b # master
|
||||
with:
|
||||
rust-toolchain: stable
|
||||
log-level: info
|
||||
arguments: --all-features --exclude-unpublished
|
||||
command: check ${{ matrix.checks }}
|
||||
toolchain: stable
|
||||
- uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2
|
||||
with:
|
||||
tool: cargo-deny
|
||||
- run: cargo deny --log-level info --all-features check
|
||||
|
||||
# Check for any unused dependencies in the codebase.
|
||||
# See <https://github.com/bnjbvr/cargo-machete/>
|
||||
@@ -81,10 +76,10 @@ jobs:
|
||||
name: Check Unused Dependencies
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: bnjbvr/cargo-machete@7959c845782fed02ee69303126d4a12d64f1db18 # v0.9.1
|
||||
- uses: bnjbvr/cargo-machete@b54422fa3319b3cac180f6030b663fe57af51635 # v0.8.0
|
||||
|
||||
# Run cargo clippy.
|
||||
#
|
||||
@@ -100,27 +95,25 @@ jobs:
|
||||
toolchain: ["stable", "beta"]
|
||||
continue-on-error: ${{ matrix.toolchain == 'beta' }}
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master
|
||||
- uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b # master
|
||||
with:
|
||||
toolchain: ${{ matrix.toolchain }}
|
||||
components: clippy
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2
|
||||
- run: cargo xtask clippy
|
||||
env:
|
||||
RUSTUP_TOOLCHAIN: ${{ matrix.toolchain }}
|
||||
|
||||
# Run markdownlint on all markdown files in the repository.
|
||||
lint-markdown:
|
||||
name: Check Markdown
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: DavidAnson/markdownlint-cli2-action@07035fd053f7be764496c0f8d8f9f41f98305101 # v21
|
||||
- uses: DavidAnson/markdownlint-cli2-action@992badcdf24e3b8eb7e87ff9287fe931bcb00c6e # v20
|
||||
with:
|
||||
globs: |
|
||||
'**/*.md'
|
||||
@@ -132,19 +125,19 @@ jobs:
|
||||
name: Coverage Report
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master
|
||||
- uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b # master
|
||||
with:
|
||||
toolchain: stable
|
||||
components: llvm-tools
|
||||
- uses: taiki-e/install-action@3522286d40783523f9c7880e33f785905b4c20d0 # v2
|
||||
- uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2
|
||||
with:
|
||||
tool: cargo-llvm-cov
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2
|
||||
- run: cargo xtask coverage
|
||||
- uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5
|
||||
- uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: true
|
||||
@@ -156,35 +149,30 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
toolchain: ["1.86.0", "stable"]
|
||||
toolchain: ["1.85.0", "stable"]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master
|
||||
- uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b # master
|
||||
with:
|
||||
toolchain: ${{ matrix.toolchain }}
|
||||
- uses: taiki-e/install-action@3522286d40783523f9c7880e33f785905b4c20d0 # v2
|
||||
with:
|
||||
tool: cargo-hack
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2
|
||||
- run: cargo xtask check --all-features
|
||||
env:
|
||||
RUSTUP_TOOLCHAIN: ${{ matrix.toolchain }}
|
||||
|
||||
build-no-std:
|
||||
name: Build No-Std
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master
|
||||
- uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b # master
|
||||
with:
|
||||
toolchain: stable
|
||||
targets: x86_64-unknown-none
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2
|
||||
# This makes it easier to debug the exact versions of the dependencies
|
||||
- run: cargo tree --target x86_64-unknown-none -p ratatui-core
|
||||
- run: cargo tree --target x86_64-unknown-none -p ratatui-widgets
|
||||
@@ -200,11 +188,11 @@ jobs:
|
||||
name: Check README
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
|
||||
- uses: taiki-e/install-action@3522286d40783523f9c7880e33f785905b4c20d0 # v2
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2
|
||||
- uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2
|
||||
with:
|
||||
tool: cargo-rdme
|
||||
- run: cargo xtask readme --check
|
||||
@@ -217,19 +205,19 @@ jobs:
|
||||
env:
|
||||
RUSTDOCFLAGS: -Dwarnings
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master
|
||||
- uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b # master
|
||||
with:
|
||||
toolchain: nightly
|
||||
- uses: dtolnay/install@74f735cdf643820234e37ae1c4089a08fd266d8a # master
|
||||
with:
|
||||
crate: cargo-docs-rs
|
||||
- uses: taiki-e/install-action@3522286d40783523f9c7880e33f785905b4c20d0 # v2
|
||||
- uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2
|
||||
with:
|
||||
tool: cargo-hack
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2
|
||||
- run: cargo xtask docs
|
||||
|
||||
# Run cargo test on the documentation of the crate. This will catch any code examples that don't
|
||||
@@ -238,16 +226,13 @@ jobs:
|
||||
name: Test Docs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master
|
||||
- uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b # master
|
||||
with:
|
||||
toolchain: stable
|
||||
- uses: taiki-e/install-action@3522286d40783523f9c7880e33f785905b4c20d0 # v2
|
||||
with:
|
||||
tool: cargo-hack
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2
|
||||
- run: cargo xtask test-docs
|
||||
|
||||
# Run cargo test on the libraries of the crate.
|
||||
@@ -257,18 +242,15 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
toolchain: ["1.86.0", "stable"]
|
||||
toolchain: ["1.85.0", "stable"]
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master
|
||||
- uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b # master
|
||||
with:
|
||||
toolchain: stable
|
||||
- uses: taiki-e/install-action@3522286d40783523f9c7880e33f785905b4c20d0 # v2
|
||||
with:
|
||||
tool: cargo-hack
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2
|
||||
- run: cargo xtask test-libs
|
||||
|
||||
# Run cargo test on all the backends.
|
||||
@@ -285,11 +267,11 @@ jobs:
|
||||
- os: windows-latest
|
||||
backend: termion
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master
|
||||
- uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b # master
|
||||
with:
|
||||
toolchain: stable
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2
|
||||
- run: cargo xtask test-backend ${{ matrix.backend }}
|
||||
|
||||
15
.github/workflows/release-plz.yml
vendored
15
.github/workflows/release-plz.yml
vendored
@@ -23,18 +23,18 @@ jobs:
|
||||
if: ${{ github.repository_owner == 'ratatui' }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master
|
||||
uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b # master
|
||||
with:
|
||||
toolchain: stable
|
||||
- uses: rust-lang/crates-io-auth-action@b7e9a28eded4986ec6b1fa40eeee8f8f165559ec # v1
|
||||
- uses: rust-lang/crates-io-auth-action@e919bc7605cde86df457cf5b93c5e103838bd879 # v1
|
||||
id: auth
|
||||
- name: Run release-plz
|
||||
uses: release-plz/action@487eb7b5c085a664d5c5ca05f4159bd9b591182a # v0.5
|
||||
uses: release-plz/action@068d76d2aa32d3c9cd0b1ccdd9ac921e28ba2be9 # v0.5
|
||||
with:
|
||||
command: release
|
||||
env:
|
||||
@@ -46,7 +46,6 @@ jobs:
|
||||
name: Release-plz PR
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository_owner == 'ratatui' }}
|
||||
concurrency:
|
||||
@@ -54,16 +53,16 @@ jobs:
|
||||
cancel-in-progress: false
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master
|
||||
uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b # master
|
||||
with:
|
||||
toolchain: stable
|
||||
- name: Run release-plz
|
||||
uses: release-plz/action@487eb7b5c085a664d5c5ca05f4159bd9b591182a # v0.5
|
||||
uses: release-plz/action@068d76d2aa32d3c9cd0b1ccdd9ac921e28ba2be9 # v0.5
|
||||
with:
|
||||
command: release-pr
|
||||
env:
|
||||
|
||||
4
.github/workflows/zizmor.yml
vendored
4
.github/workflows/zizmor.yml
vendored
@@ -18,9 +18,9 @@ jobs:
|
||||
security-events: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run zizmor 🌈
|
||||
uses: zizmorcore/zizmor-action@e639db99335bc9038abc0e066dfcd72e23d26fb4 # v0.3.0
|
||||
uses: zizmorcore/zizmor-action@f52a838cfabf134edcbaa7c8b3677dde20045018 # v0.1.1
|
||||
|
||||
@@ -10,9 +10,7 @@ GitHub with a [breaking change] label.
|
||||
|
||||
This is a quick summary of the sections below:
|
||||
|
||||
- [v0.30.1](#v0301)
|
||||
- Adding `AsRef` impls for widgets may affect type inference in rare cases
|
||||
- [v0.30.0](#v0300)
|
||||
- [v0.30.0 Unreleased](#v0300-unreleased)
|
||||
- `Flex::SpaceAround` now mirrors flexbox: space between items is twice the size of the outer gaps
|
||||
are twice the size of first and last elements
|
||||
- `block::Title` no longer exists
|
||||
@@ -20,7 +18,7 @@ This is a quick summary of the sections below:
|
||||
- `FrameExt` trait for `unstable-widget-ref` feature
|
||||
- `List::highlight_symbol` now accepts `Into<Line>` instead of `&str`
|
||||
- 'layout::Alignment' is renamed to 'layout::HorizontalAlignment'
|
||||
- MSRV is now 1.86.0
|
||||
- The MSRV is now 1.85.0
|
||||
- `Backend` now requires an associated `Error` type and `clear_region` method
|
||||
- `TestBackend` now uses `core::convert::Infallible` for error handling instead of `std::io::Error`
|
||||
- Disabling `default-features` will now disable layout cache, which can have a negative impact on performance
|
||||
@@ -29,9 +27,6 @@ This is a quick summary of the sections below:
|
||||
- Disabling `default-features` suppresses the error message if `show_cursor()` fails when dropping
|
||||
`Terminal`
|
||||
- Support a broader range for `unicode-width` version
|
||||
- `Marker` is now non-exhaustive
|
||||
- `symbols::braille::BLANK` and `symbols::braille::DOTS` have been removed in favor of an ordered
|
||||
array of all Braille characters
|
||||
- [v0.29.0](#v0290)
|
||||
- `Sparkline::data` takes `IntoIterator<Item = SparklineBar>` instead of `&[u64]` and is no longer
|
||||
const
|
||||
@@ -95,36 +90,7 @@ This is a quick summary of the sections below:
|
||||
- MSRV is now 1.63.0
|
||||
- `List` no longer ignores empty strings
|
||||
|
||||
## [v0.30.1](https://github.com/ratatui/ratatui/releases/tag/ratatui-v0.30.1)
|
||||
|
||||
### Adding `AsRef` impls for widgets may affect type inference ([#2297])
|
||||
|
||||
[#2297]: https://github.com/ratatui/ratatui/pull/2297
|
||||
|
||||
Adding `AsRef<Self>` for built-in widgets can change type inference outcomes in rare cases where
|
||||
`AsRef` is part of a trait bound, and can also conflict with downstream blanket or manual `AsRef`
|
||||
impls for widget types. If you hit new ambiguity errors, add explicit type annotations or specify
|
||||
the concrete widget type to guide inference, and remove any redundant `AsRef` impls.
|
||||
|
||||
## [v0.30.0](https://github.com/ratatui/ratatui/releases/tag/ratatui-v0.30.0)
|
||||
|
||||
### `Marker` is now non-exhaustive ([#2236])
|
||||
|
||||
[#2236]: https://github.com/ratatui/ratatui/pull/2236
|
||||
|
||||
The `Marker` enum is now marked as `#[non_exhaustive]`, if you were matching on `Marker` exhaustively,
|
||||
you will need to add a wildcard arm:
|
||||
|
||||
```diff
|
||||
match marker {
|
||||
Marker::Dot => { /* ... */ }
|
||||
Marker::Block => { /* ... */ }
|
||||
Marker::Bar => { /* ... */ }
|
||||
Marker::Braille => { /* ... */ }
|
||||
Marker::HalfBlock => { /* ... */ }
|
||||
+ _ => { /* ... */ }
|
||||
}
|
||||
```
|
||||
## v0.30.0 Unreleased
|
||||
|
||||
### `Flex::SpaceAround` now mirrors flexbox: space between items is twice the size of the outer gaps ([#1952])
|
||||
|
||||
@@ -269,11 +235,11 @@ instead.
|
||||
+ fn run(mut terminal: DefaultTerminal) -> io::Result<()> {
|
||||
```
|
||||
|
||||
### MSRV is now 1.86.0 ([#2230])
|
||||
### The MSRV is now 1.85.0 ([#1860])
|
||||
|
||||
[#2230]: https://github.com/ratatui/ratatui/pull/2230
|
||||
[#1860]: https://github.com/ratatui/ratatui/pull/1860
|
||||
|
||||
The minimum supported Rust version (MSRV) is now 1.86.0.
|
||||
The minimum supported Rust version (MSRV) is now 1.85.0.
|
||||
|
||||
### `layout::Alignment` is renamed to `layout::HorizontalAlignment` ([#1735])
|
||||
|
||||
@@ -1121,7 +1087,7 @@ previously did not need to use type annotations to fail to compile. To fix this,
|
||||
|
||||
[#133]: https://github.com/ratatui/ratatui/issues/133
|
||||
|
||||
Code using the `Block` marker that previously rendered using a half block character (`'▀'`) now
|
||||
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
|
||||
the existing code.
|
||||
|
||||
|
||||
3392
CHANGELOG.md
3392
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -135,14 +135,6 @@ Changes to project configuration files require special consideration:
|
||||
|
||||
Please discuss these changes in an issue before implementing them.
|
||||
|
||||
### Collaborative development
|
||||
|
||||
We may occasionally make changes directly to your branch—such as force-pushes—to help move a PR
|
||||
forward, speed up review, or ensure it meets our quality standards. If you would prefer we do not do
|
||||
this, or if your workflow depends on us avoiding force-pushes (for example, if your app points to
|
||||
your branch in `Cargo.toml`), please mention this in your PR description and we will respect your
|
||||
preference.
|
||||
|
||||
## Implementation Guidelines
|
||||
|
||||
### Setup
|
||||
@@ -181,7 +173,7 @@ good, but this can always be improved. Focus on keeping the tests simple and obv
|
||||
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
|
||||
content of the output buffer that would have been flushed to the terminal after a given draw call.
|
||||
See `widgets_block_renders` in [ratatui/tests/widgets_block.rs](./ratatui/tests/widgets_block.rs) for an example.
|
||||
See `widgets_block_renders` in [tests/widgets_block.rs](./tests/widget_block.rs) for an example.
|
||||
|
||||
When writing tests, generally prefer to write unit tests and doc tests directly in the code file
|
||||
being tested rather than integration tests in the `tests/` folder.
|
||||
|
||||
1355
Cargo.lock
generated
1355
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
35
Cargo.toml
35
Cargo.toml
@@ -25,48 +25,45 @@ readme = "README.md"
|
||||
license = "MIT"
|
||||
exclude = ["assets/*", ".github", "Makefile.toml", "CONTRIBUTING.md", "*.log", "tags"]
|
||||
edition = "2024"
|
||||
rust-version = "1.86.0"
|
||||
rust-version = "1.85.0"
|
||||
|
||||
[workspace.dependencies]
|
||||
anstyle = "1"
|
||||
bitflags = "2.10"
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
bitflags = "2.9"
|
||||
color-eyre = "0.6"
|
||||
compact_str = { version = "0.9", default-features = false }
|
||||
criterion = { version = "0.8", features = ["html_reports"] }
|
||||
criterion = { version = "0.5", features = ["html_reports"] }
|
||||
crossterm = "0.29"
|
||||
document-features = "0.2"
|
||||
fakeit = "1"
|
||||
futures = "0.3"
|
||||
hashbrown = "0.16"
|
||||
hashbrown = "0.15"
|
||||
indoc = "2"
|
||||
instability = "0.3"
|
||||
itertools = { version = "0.14", default-features = false, features = ["use_alloc"] }
|
||||
kasuari = { version = "0.4", default-features = false }
|
||||
line-clipping = "0.3"
|
||||
lru = "0.16"
|
||||
octocrab = "0.49"
|
||||
lru = "0.14"
|
||||
palette = "0.7"
|
||||
pretty_assertions = "1"
|
||||
rand = "0.9"
|
||||
rand_chacha = "0.9"
|
||||
ratatui = { path = "ratatui", version = "0.30.0" }
|
||||
ratatui-core = { path = "ratatui-core", version = "0.1.0" }
|
||||
ratatui-crossterm = { path = "ratatui-crossterm", version = "0.1.0" }
|
||||
ratatui-macros = { path = "ratatui-macros", version = "0.7.0" }
|
||||
ratatui-termion = { path = "ratatui-termion", version = "0.1.0" }
|
||||
ratatui-termwiz = { path = "ratatui-termwiz", version = "0.1.0" }
|
||||
ratatui-widgets = { path = "ratatui-widgets", version = "0.3.0" }
|
||||
rstest = "0.26"
|
||||
ratatui = { path = "ratatui", version = "0.30.0-alpha.6" }
|
||||
ratatui-core = { path = "ratatui-core", version = "0.1.0-alpha.7" }
|
||||
ratatui-crossterm = { path = "ratatui-crossterm", version = "0.1.0-alpha.6" }
|
||||
ratatui-macros = { path = "ratatui-macros", version = "0.7.0-alpha.5" }
|
||||
ratatui-termion = { path = "ratatui-termion", version = "0.1.0-alpha.6" }
|
||||
ratatui-termwiz = { path = "ratatui-termwiz", version = "0.1.0-alpha.6" }
|
||||
ratatui-widgets = { path = "ratatui-widgets", version = "0.3.0-alpha.6" }
|
||||
rstest = "0.25"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
strum = { version = "0.27", default-features = false, features = ["derive"] }
|
||||
termion = "4"
|
||||
termwiz = "0.23"
|
||||
thiserror = { version = "2", default-features = false }
|
||||
time = { version = "0.3.37", default-features = false }
|
||||
time = { version = "0.3", default-features = false }
|
||||
tokio = "1"
|
||||
tokio-stream = "0.1"
|
||||
tracing = "0.1"
|
||||
tracing-appender = "0.2"
|
||||
tracing-subscriber = "0.3"
|
||||
@@ -74,7 +71,7 @@ trybuild = "1"
|
||||
unicode-segmentation = "1"
|
||||
unicode-truncate = { version = "2", default-features = false }
|
||||
# See <https://github.com/ratatui/ratatui/issues/1271> for information about why we pin unicode-width
|
||||
unicode-width = ">=0.2.0, <=0.2.2"
|
||||
unicode-width = ">=0.2.0, <=0.2.1"
|
||||
|
||||
# Improve benchmark consistency
|
||||
[profile.bench]
|
||||
@@ -107,7 +104,6 @@ empty_line_after_doc_comments = "warn"
|
||||
equatable_if_let = "warn"
|
||||
fn_to_numeric_cast_any = "warn"
|
||||
format_push_string = "warn"
|
||||
implicit_clone = "warn"
|
||||
map_err_ignore = "warn"
|
||||
missing_const_for_fn = "warn"
|
||||
mixed_read_write_in_expression = "warn"
|
||||
@@ -119,5 +115,6 @@ redundant_type_annotations = "warn"
|
||||
rest_pat_in_fully_bound_structs = "warn"
|
||||
string_lit_chars_any = "warn"
|
||||
string_slice = "warn"
|
||||
string_to_string = "warn"
|
||||
unnecessary_self_imports = "warn"
|
||||
use_self = "warn"
|
||||
|
||||
@@ -5,7 +5,7 @@ This file documents current and past maintainers.
|
||||
- [orhun](https://github.com/orhun)
|
||||
- [joshka](https://github.com/joshka)
|
||||
- [kdheepak](https://github.com/kdheepak)
|
||||
- [j-g00da](https://github.com/j-g00da)
|
||||
- [Valentin271](https://github.com/Valentin271)
|
||||
|
||||
## Past Maintainers
|
||||
|
||||
@@ -13,4 +13,3 @@ This file documents current and past maintainers.
|
||||
- [mindoodoo](https://github.com/mindoodoo)
|
||||
- [sayanarijit](https://github.com/sayanarijit)
|
||||
- [EdJoPaTo](https://github.com/EdJoPaTo)
|
||||
- [Valentin271](https://github.com/Valentin271)
|
||||
|
||||
@@ -119,10 +119,10 @@ guidelines to ensure compliance.
|
||||
If you'd like to show your support, you can add the Ratatui badge to your project's README:
|
||||
|
||||
```md
|
||||
[](https://ratatui.rs/)
|
||||
[](https://ratatui.rs/)
|
||||
```
|
||||
|
||||
[](https://ratatui.rs/)
|
||||
[](https://ratatui.rs/)
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
|
||||
46
cliff.toml
46
cliff.toml
@@ -24,17 +24,13 @@ body = """
|
||||
{%- if not version %}
|
||||
## [unreleased]
|
||||
{% else -%}
|
||||
{%- if package -%} {# release-plz specific variable #}
|
||||
## {{ package }} - [{{ version }}]({{ release_link }}) - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||
{%- else -%}
|
||||
## [{{ version }}]({{ self::remote_url() }}/releases/tag/{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||
{%- endif %}
|
||||
{% endif -%}
|
||||
|
||||
{% macro commit(commit) -%}
|
||||
- [{{ commit.id | truncate(length=7, end="") }}]({{ "https://github.com/ratatui/ratatui/commit/" ~ commit.id }}) \
|
||||
*({{commit.scope | default(value = "uncategorized") | lower }})* {{ commit.message | upper_first | trim }}\
|
||||
{% if commit.remote.username %} by `@{{ commit.remote.username }}`{%- endif -%}\
|
||||
{% if commit.remote.username %} by @{{ commit.remote.username }}{%- endif -%}\
|
||||
{% if commit.remote.pr_number %} in [#{{ commit.remote.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.remote.pr_number }}){%- endif %}\
|
||||
{%- if commit.breaking %} [**breaking**]{% endif %}
|
||||
{%- if commit.body %}\n\n{{ commit.body | indent(prefix=" > ", first=true, blank=true) }}
|
||||
@@ -61,36 +57,16 @@ body = """
|
||||
{%- endfor -%}
|
||||
{%- endfor %}
|
||||
|
||||
{%- if not release_link -%}
|
||||
{% if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}
|
||||
### New Contributors
|
||||
{%- endif %}\
|
||||
{% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}
|
||||
* @{{ contributor.username }} made their first contribution
|
||||
{%- if contributor.pr_number %} in \
|
||||
[#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \
|
||||
{%- endif %}
|
||||
{%- endfor -%}
|
||||
{%- endif -%}
|
||||
|
||||
{% if version %}
|
||||
{% if previous.version %}
|
||||
{%- if release_link -%}
|
||||
**Full Changelog**: {{ release_link }} {# release-plz specific variable #}
|
||||
{%- else -%}
|
||||
**Full Changelog**: {{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }}
|
||||
{% endif %}
|
||||
**Full Changelog**: {{ release_link }}
|
||||
{% endif %}
|
||||
{% else -%}
|
||||
{% raw %}\n{% endraw %}
|
||||
{% endif %}
|
||||
|
||||
{%- macro remote_url() -%}
|
||||
{%- if remote.owner -%} {# release-plz specific variable #}
|
||||
https://github.com/{{ remote.owner }}/{{ remote.repo }}\
|
||||
{%- else -%}
|
||||
https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}\
|
||||
{%- endif -%}
|
||||
{% endmacro %}
|
||||
"""
|
||||
# remove the leading and trailing whitespace from the template
|
||||
@@ -116,16 +92,10 @@ commit_preprocessors = [
|
||||
{ pattern = '(Clarify README.md)', replace = "docs(readme): ${1}" },
|
||||
{ pattern = '(Update README.md)', replace = "docs(readme): ${1}" },
|
||||
{ pattern = '(fix typos|Fix typos)', replace = "fix: ${1}" },
|
||||
# Typos that squeaked through and which would otherwise trigger the typos linter.
|
||||
# a small typo that squeaked through and which would otherwise trigger the typos linter.
|
||||
# Regex obsfucation is to avoid triggering the linter in this file until there's a per file config
|
||||
# See https://github.com/crate-ci/typos/issues/724
|
||||
{ pattern = '\<[d]eatil\>', replace = "detail" },
|
||||
{ pattern = '\<[f]eatuers\>', replace = "features" },
|
||||
{ pattern = '\<[s]pecically\>', replace = "specially" },
|
||||
{ pattern = '\<[g]ague\>', replace = "gauge" },
|
||||
{ pattern = '\<[a]rithmentic\>', replace = "arithmetic" },
|
||||
{ pattern = '\<[i]ntructions\>', replace = "instructions" },
|
||||
{ pattern = '\<[i]mplementated\>', replace = "implemented" },
|
||||
]
|
||||
# regex for parsing and grouping commits
|
||||
commit_parsers = [
|
||||
@@ -139,16 +109,14 @@ commit_parsers = [
|
||||
{ message = "^style", group = "<!-- 05 -->Styling" },
|
||||
{ message = "^test", group = "<!-- 06 -->Testing" },
|
||||
{ message = "^chore\\(release\\): prepare for", skip = true },
|
||||
{ message = "^chore: release", skip = true },
|
||||
{ message = "^chore\\(pr\\)", skip = true },
|
||||
{ message = "^chore\\(pull\\)", skip = true },
|
||||
{ message = "^chore\\(deps\\)", skip = true },
|
||||
{ message = "^chore\\(changelog\\)", skip = true },
|
||||
{ message = "^[cC]hore", group = "<!-- 07 -->Miscellaneous Tasks" },
|
||||
{ body = ".*security", group = "<!-- 08 -->Security" },
|
||||
{ message = "^build\\(deps\\)", skip = true },
|
||||
{ message = "^build\\(release\\)", skip = true },
|
||||
{ message = "^build", group = "<!-- 08 -->Build" },
|
||||
{ body = ".*security", group = "<!-- 09 -->Security" },
|
||||
{ message = "^build", group = "<!-- 09 -->Build" },
|
||||
{ message = "^ci", group = "<!-- 10 -->Continuous Integration" },
|
||||
{ message = "^revert", group = "<!-- 11 -->Reverted Commits" },
|
||||
# handle some old commits styles from pre 0.4
|
||||
@@ -163,9 +131,9 @@ filter_commits = false
|
||||
# glob pattern for matching git tags
|
||||
tag_pattern = "v[0-9]*"
|
||||
# regex for skipping tags
|
||||
skip_tags = "beta|alpha|v0.1.0-rc.1"
|
||||
skip_tags = "v0.1.0-rc.1"
|
||||
# regex for ignoring tags
|
||||
ignore_tags = "rc"
|
||||
ignore_tags = "alpha"
|
||||
# sort the tags topologically
|
||||
topo_order = false
|
||||
# sort the commits inside sections by oldest/newest order
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
# Examples
|
||||
|
||||
This folder contains examples that are more application focused.
|
||||
|
||||
> [!TIP]
|
||||
> There are also [widget examples] in `ratatui-widgets`.
|
||||
There are also [widget examples] in `ratatui-widgets`.
|
||||
|
||||
[widget examples]: ../ratatui-widgets/examples
|
||||
|
||||
@@ -13,9 +11,8 @@ You can run these examples using:
|
||||
cargo run -p example-name
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> This folder might use unreleased code. Consider viewing the examples in the `latest` branch instead
|
||||
> of the `main` branch for code which is guaranteed to work with the released Ratatui version.
|
||||
This folder might use unreleased code. Consider viewing the examples in the `latest` branch instead
|
||||
of the `main` branch for code which is guaranteed to work with the released ratatui version.
|
||||
|
||||
> [!WARNING]
|
||||
>
|
||||
@@ -206,7 +203,7 @@ Shows how to use the [tracing](https://crates.io/crates/tracing) crate to log to
|
||||
|
||||
## User Input
|
||||
|
||||
Shows how to handle user input. [Source](./apps/user-input/).
|
||||
Shows how to handle user input. [Source](./apps/user-input/). [Source](./apps/user-input/).
|
||||
|
||||
![User input demo][user-input.gif]
|
||||
|
||||
|
||||
@@ -14,9 +14,9 @@ edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
color-eyre = "0.6.5"
|
||||
crossterm = { workspace = true, features = ["event-stream"] }
|
||||
octocrab.workspace = true
|
||||
octocrab = "0.44.0"
|
||||
ratatui.workspace = true
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
tokio-stream.workspace = true
|
||||
tokio = { version = "1.47.0", features = ["rt-multi-thread", "macros"] }
|
||||
tokio-stream = "0.1.17"
|
||||
|
||||
@@ -9,7 +9,7 @@ rust-version.workspace = true
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
ratatui.workspace = true
|
||||
time = { workspace = true, features = ["formatting", "parsing"] }
|
||||
time = { version = "0.3.39", features = ["formatting", "parsing"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -122,12 +122,8 @@ impl App {
|
||||
Marker::Dot => Marker::Braille,
|
||||
Marker::Braille => Marker::Block,
|
||||
Marker::Block => Marker::HalfBlock,
|
||||
Marker::HalfBlock => Marker::Quadrant,
|
||||
Marker::Quadrant => Marker::Sextant,
|
||||
Marker::Sextant => Marker::Octant,
|
||||
Marker::Octant => Marker::Bar,
|
||||
Marker::HalfBlock => Marker::Bar,
|
||||
Marker::Bar => Marker::Dot,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ rust-version.workspace = true
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
palette.workspace = true
|
||||
palette = "0.7.6"
|
||||
ratatui.workspace = true
|
||||
|
||||
[lints]
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use std::cmp::Ordering;
|
||||
|
||||
/// A Ratatui example that demonstrates how different layout constraints work.
|
||||
///
|
||||
/// It also supports swapping constraints, adding and removing blocks, and changing the spacing
|
||||
@@ -32,7 +30,7 @@ fn main() -> Result<()> {
|
||||
#[derive(Default)]
|
||||
struct App {
|
||||
mode: AppMode,
|
||||
spacing: i16,
|
||||
spacing: u16,
|
||||
constraints: Vec<Constraint>,
|
||||
selected_index: usize,
|
||||
value: u16,
|
||||
@@ -271,7 +269,7 @@ impl App {
|
||||
}
|
||||
|
||||
fn instructions() -> impl Widget {
|
||||
let text = "◄ ►: select, ▲ ▼: edit, 1-6: swap, a: add, x: delete, q: quit, +/-: spacing";
|
||||
let text = "◄ ►: select, ▲ ▼: edit, 1-6: swap, a: add, x: delete, q: quit, + -: spacing";
|
||||
Paragraph::new(text)
|
||||
.fg(Self::TEXT_COLOR)
|
||||
.centered()
|
||||
@@ -309,12 +307,10 @@ impl App {
|
||||
///
|
||||
/// Only shows the gap when spacing is not zero
|
||||
fn axis(&self, width: u16) -> impl Widget {
|
||||
let label = match self.spacing.cmp(&0) {
|
||||
Ordering::Greater => format!("{width} px (gap: {} px)", self.spacing),
|
||||
Ordering::Less => {
|
||||
format!("{width} px (overlap: {} px)", self.spacing.unsigned_abs())
|
||||
}
|
||||
Ordering::Equal => format!("{width} px"),
|
||||
let label = if self.spacing != 0 {
|
||||
format!("{} px (gap: {} px)", width, self.spacing)
|
||||
} else {
|
||||
format!("{width} px")
|
||||
};
|
||||
let bar_width = width.saturating_sub(2) as usize; // we want to `<` and `>` at the ends
|
||||
let width_bar = format!("<{label:-^bar_width$}>");
|
||||
|
||||
@@ -12,9 +12,9 @@ termion = ["ratatui/termion", "dep:termion"]
|
||||
termwiz = ["ratatui/termwiz", "dep:termwiz"]
|
||||
|
||||
[dependencies]
|
||||
clap.workspace = true
|
||||
clap = { version = "4.5.41", features = ["derive"] }
|
||||
crossterm = { workspace = true, optional = true }
|
||||
rand.workspace = true
|
||||
rand = "0.9.2"
|
||||
ratatui.workspace = true
|
||||
termwiz = { workspace = true, optional = true }
|
||||
|
||||
|
||||
@@ -6,14 +6,14 @@ edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
color-eyre = "0.6.5"
|
||||
crossterm.workspace = true
|
||||
indoc.workspace = true
|
||||
itertools.workspace = true
|
||||
palette.workspace = true
|
||||
rand.workspace = true
|
||||
rand_chacha.workspace = true
|
||||
palette = "0.7.6"
|
||||
rand = "0.9.2"
|
||||
rand_chacha = "0.9.0"
|
||||
ratatui = { workspace = true, features = ["all-widgets"] }
|
||||
strum.workspace = true
|
||||
time.workspace = true
|
||||
unicode-width.workspace = true
|
||||
time = "0.3.39"
|
||||
unicode-width = "0.2.0"
|
||||
|
||||
@@ -8,7 +8,7 @@ rust-version.workspace = true
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
rand.workspace = true
|
||||
rand = "0.9.2"
|
||||
ratatui.workspace = true
|
||||
|
||||
[lints]
|
||||
|
||||
@@ -123,9 +123,9 @@ impl InputForm {
|
||||
frame.render_widget(&self.age, age_area);
|
||||
|
||||
let cursor_position = match self.focus {
|
||||
Focus::FirstName => first_name_area + self.first_name.cursor_offset(),
|
||||
Focus::LastName => last_name_area + self.last_name.cursor_offset(),
|
||||
Focus::Age => age_area + self.age.cursor_offset(),
|
||||
Focus::FirstName => first_name_area.offset(self.first_name.cursor_offset()),
|
||||
Focus::LastName => last_name_area.offset(self.last_name.cursor_offset()),
|
||||
Focus::Age => age_area.offset(self.age.cursor_offset()),
|
||||
};
|
||||
frame.set_cursor_position(cursor_position);
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ rust-version.workspace = true
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
## a collection of line drawing algorithms (e.g. Bresenham's line algorithm)
|
||||
line_drawing = "1"
|
||||
rand.workspace = true
|
||||
line_drawing = "1.0.1"
|
||||
rand = "0.9.2"
|
||||
ratatui.workspace = true
|
||||
|
||||
[lints]
|
||||
|
||||
@@ -11,10 +11,10 @@
|
||||
use color_eyre::Result;
|
||||
use crossterm::event::{self, KeyCode};
|
||||
use ratatui::Frame;
|
||||
use ratatui::layout::{Constraint, Layout};
|
||||
use ratatui::layout::{Constraint, Flex, Layout, Rect};
|
||||
use ratatui::style::Stylize;
|
||||
use ratatui::text::Line;
|
||||
use ratatui::widgets::{Block, Clear, Paragraph};
|
||||
use ratatui::widgets::{Block, Clear};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
@@ -52,14 +52,19 @@ fn render(frame: &mut Frame, show_popup: bool) {
|
||||
frame.render_widget(Block::bordered().title("Content").on_blue(), content);
|
||||
|
||||
if show_popup {
|
||||
let popup_block = Block::bordered().title("Popup");
|
||||
let centered_area = area.centered(Constraint::Percentage(60), Constraint::Percentage(20));
|
||||
let popup = Block::bordered().title("Popup");
|
||||
let popup_area = centered_area(area, 60, 20);
|
||||
// clears out any background in the area before rendering the popup
|
||||
frame.render_widget(Clear, centered_area);
|
||||
let paragraph = Paragraph::new("Lorem ipsum").block(popup_block);
|
||||
frame.render_widget(paragraph, centered_area);
|
||||
// another solution is to use the inner area of the block
|
||||
// let inner_area = popup_block.inner(centered_area);
|
||||
// frame.render_widget(your_widget, inner_area);
|
||||
frame.render_widget(Clear, popup_area);
|
||||
frame.render_widget(popup, popup_area);
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a centered rect using up certain percentage of the available rect
|
||||
fn centered_area(area: Rect, percent_x: u16, percent_y: u16) -> Rect {
|
||||
let vertical = Layout::vertical([Constraint::Percentage(percent_y)]).flex(Flex::Center);
|
||||
let horizontal = Layout::horizontal([Constraint::Percentage(percent_x)]).flex(Flex::Center);
|
||||
let [area] = area.layout(&vertical);
|
||||
let [area] = area.layout(&horizontal);
|
||||
area
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ rust-version.workspace = true
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
fakeit.workspace = true
|
||||
fakeit = "1.1"
|
||||
itertools.workspace = true
|
||||
ratatui.workspace = true
|
||||
unicode-width.workspace = true
|
||||
|
||||
@@ -222,19 +222,6 @@ impl App {
|
||||
let item = data.ref_array();
|
||||
item.into_iter()
|
||||
.map(|content| Cell::from(Text::from(format!("\n{content}\n"))))
|
||||
.enumerate()
|
||||
.map(|(idx, cell)| {
|
||||
if i == 3 && idx == 1 {
|
||||
Cell::from(Text::from(
|
||||
// Gratuitously long error message to demonstrate column_span(2)
|
||||
"\n[no address or email address is available for this person]\n"
|
||||
.to_string(),
|
||||
))
|
||||
.column_span(2)
|
||||
} else {
|
||||
cell
|
||||
}
|
||||
})
|
||||
.collect::<Row>()
|
||||
.style(Style::new().fg(self.colors.row_fg).bg(color))
|
||||
.height(4)
|
||||
|
||||
@@ -9,9 +9,9 @@ rust-version.workspace = true
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
ratatui.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-appender.workspace = true
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
||||
tracing = "0.1.40"
|
||||
tracing-appender = "0.2.3"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -8,7 +8,7 @@ rust-version.workspace = true
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
rand.workspace = true
|
||||
rand = "0.9.2"
|
||||
ratatui.workspace = true
|
||||
|
||||
[lints]
|
||||
|
||||
@@ -6,6 +6,6 @@ edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
color-eyre = "0.6"
|
||||
crossterm = "0.29"
|
||||
ratatui = { workspace = true, features = ["unstable-widget-ref"] }
|
||||
|
||||
@@ -6,8 +6,7 @@ Set Theme "Aardvark Blue"
|
||||
# The reason for this strange size is that the social preview image for this
|
||||
# demo is 1280x64 with 80 pixels of padding on each side. We want a version
|
||||
# without the padding for README.md, etc.
|
||||
# Please note that based on the width the demo may wrap and look corrupted.
|
||||
Set Width 1160
|
||||
Set Width 1120
|
||||
Set Height 480
|
||||
Set Padding 0
|
||||
Hide
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
[package]
|
||||
name = "ratatui-core"
|
||||
version = "0.1.0"
|
||||
description = """
|
||||
Core types and traits for the Ratatui Terminal UI library.
|
||||
Widget libraries should use this crate. Applications should use the main Ratatui crate.
|
||||
"""
|
||||
documentation = "https://docs.rs/ratatui-core"
|
||||
version = "0.1.0-alpha.7"
|
||||
readme = "README.md"
|
||||
authors.workspace = true
|
||||
documentation.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
keywords.workspace = true
|
||||
@@ -43,9 +43,6 @@ anstyle = ["dep:anstyle"]
|
||||
## enables conversions from colors in the [`palette`] crate to [`Color`](crate::style::Color).
|
||||
palette = ["std", "dep:palette"]
|
||||
|
||||
## enables portable-atomic integration for targets that don't support atomic types.
|
||||
portable-atomic = ["kasuari/portable-atomic"]
|
||||
|
||||
## enables the backend code that sets the underline color. Underline color is only supported by
|
||||
## the Crossterm backend, and is not supported on Windows 7.
|
||||
underline-color = []
|
||||
|
||||
@@ -109,28 +109,19 @@ use crate::layout::{Position, Size};
|
||||
mod test;
|
||||
pub use self::test::TestBackend;
|
||||
|
||||
/// Defines which region of the terminal's visible display area is cleared.
|
||||
///
|
||||
/// Clearing operates on character cells in the active display surface. It does not move, hide, or
|
||||
/// reset the cursor position. If the cursor lies inside the cleared region, the character cell at
|
||||
/// the cursor position is cleared as well.
|
||||
///
|
||||
/// Clearing applies to the terminal's visible display area, not just content previously drawn by
|
||||
/// Ratatui. No guarantees are made about scrollback, history, or off-screen buffers.
|
||||
/// Enum representing the different types of clearing operations that can be performed
|
||||
/// on the terminal screen.
|
||||
#[derive(Debug, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub enum ClearType {
|
||||
/// Clears all character cells in the visible display area.
|
||||
/// Clear the entire screen.
|
||||
All,
|
||||
/// Clears all character cells from the cursor position (inclusive) through the end of the
|
||||
/// display area.
|
||||
/// Clear everything after the cursor.
|
||||
AfterCursor,
|
||||
/// Clears all character cells from the start of the display area through the cursor position
|
||||
/// (inclusive).
|
||||
/// Clear everything before the cursor.
|
||||
BeforeCursor,
|
||||
/// Clears all character cells in the cursor's current line.
|
||||
/// Clear the current line.
|
||||
CurrentLine,
|
||||
/// Clears all character cells from the cursor position (inclusive) to the end of the current
|
||||
/// line.
|
||||
/// Clear everything from the cursor until the next newline.
|
||||
UntilNewLine,
|
||||
}
|
||||
|
||||
@@ -246,14 +237,7 @@ pub trait Backend {
|
||||
self.set_cursor_position(Position { x, y })
|
||||
}
|
||||
|
||||
/// Clears all character cells in the terminal's visible display area.
|
||||
///
|
||||
/// This operation preserves the cursor position. If the cursor lies within the cleared
|
||||
/// region, the character cell at the cursor position is cleared. No guarantees are made about
|
||||
/// scrollback, history, or off-screen buffers.
|
||||
///
|
||||
/// This is equivalent to calling [`clear_region`](Self::clear_region) with
|
||||
/// [`ClearType::All`].
|
||||
/// Clears the whole terminal screen
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
@@ -267,13 +251,7 @@ pub trait Backend {
|
||||
/// ```
|
||||
fn clear(&mut self) -> Result<(), Self::Error>;
|
||||
|
||||
/// Clears a specific region of the terminal's visible display area, as defined by
|
||||
/// [`ClearType`].
|
||||
///
|
||||
/// This operation preserves the cursor position. If the cursor lies within the cleared
|
||||
/// region, the character cell at the cursor position is cleared. Clearing applies to the
|
||||
/// active display surface only and does not make guarantees about scrollback, history, or
|
||||
/// off-screen buffers.
|
||||
/// Clears a specific region of the terminal specified by the [`ClearType`] parameter
|
||||
///
|
||||
/// This method is optional and may not be implemented by all backends. The default
|
||||
/// implementation calls [`clear`] if the `clear_type` is [`ClearType::All`] and returns an
|
||||
|
||||
@@ -105,19 +105,6 @@ impl TestBackend {
|
||||
&self.buffer
|
||||
}
|
||||
|
||||
/// Returns whether the cursor is visible.
|
||||
pub const fn cursor_visible(&self) -> bool {
|
||||
self.cursor
|
||||
}
|
||||
|
||||
/// Returns the current cursor position.
|
||||
pub const fn cursor_position(&self) -> Position {
|
||||
Position {
|
||||
x: self.pos.0,
|
||||
y: self.pos.1,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
@@ -288,12 +275,12 @@ impl Backend for TestBackend {
|
||||
let region = match clear_type {
|
||||
ClearType::All => return self.clear(),
|
||||
ClearType::AfterCursor => {
|
||||
let index = self.buffer.index_of(self.pos.0, self.pos.1);
|
||||
let index = self.buffer.index_of(self.pos.0, self.pos.1) + 1;
|
||||
&mut self.buffer.content[index..]
|
||||
}
|
||||
ClearType::BeforeCursor => {
|
||||
let index = self.buffer.index_of(self.pos.0, self.pos.1);
|
||||
&mut self.buffer.content[..=index]
|
||||
&mut self.buffer.content[..index]
|
||||
}
|
||||
ClearType::CurrentLine => {
|
||||
let line_start_index = self.buffer.index_of(0, self.pos.1);
|
||||
@@ -633,7 +620,7 @@ mod tests {
|
||||
backend.assert_buffer_lines([
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaa ",
|
||||
"aaaa ",
|
||||
" ",
|
||||
" ",
|
||||
]);
|
||||
@@ -657,7 +644,7 @@ mod tests {
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" aaaa",
|
||||
" aaaaa",
|
||||
"aaaaaaaaaa",
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ use crate::text::{Line, Span};
|
||||
/// use ratatui_core::layout::{Position, Rect};
|
||||
/// use ratatui_core::style::{Color, Style};
|
||||
///
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// # fn foo() -> Option<()> {
|
||||
/// let mut buf = Buffer::empty(Rect {
|
||||
/// x: 0,
|
||||
/// y: 0,
|
||||
@@ -39,15 +39,13 @@ use crate::text::{Line, Span};
|
||||
///
|
||||
/// // indexing using (x, y) tuple (which is converted to Position)
|
||||
/// buf[(0, 1)].set_symbol("B");
|
||||
/// assert_eq!(buf[(0, 1)].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 })
|
||||
/// .ok_or("cell not found")?;
|
||||
/// let cell = buf.cell_mut(Position { x: 0, y: 2 })?;
|
||||
/// cell.set_symbol("C");
|
||||
///
|
||||
/// let cell = buf.cell(Position { x: 0, y: 2 }).ok_or("cell not found")?;
|
||||
/// let cell = buf.cell(Position { x: 0, y: 2 })?;
|
||||
/// assert_eq!(cell.symbol(), "C");
|
||||
///
|
||||
/// buf.set_string(
|
||||
@@ -60,7 +58,7 @@ use crate::text::{Line, Span};
|
||||
/// assert_eq!(cell.symbol(), "r");
|
||||
/// assert_eq!(cell.fg, Color::Red);
|
||||
/// assert_eq!(cell.bg, Color::White);
|
||||
/// # Ok(())
|
||||
/// # Some(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[derive(Default, Clone, Eq, PartialEq, Hash)]
|
||||
@@ -218,9 +216,6 @@ impl Buffer {
|
||||
///
|
||||
/// Global coordinates are offset by the Buffer's area offset (`x`/`y`).
|
||||
///
|
||||
/// Usage discouraged, as it exposes `self.content` as a linearly indexable array, which limits
|
||||
/// potential future abstractions. See <https://github.com/ratatui/ratatui/issues/1122>.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
@@ -274,13 +269,10 @@ impl Buffer {
|
||||
Some(y * width + x)
|
||||
}
|
||||
|
||||
/// Returns the (global) coordinates of a cell given its index.
|
||||
/// Returns the (global) coordinates of a cell given its index
|
||||
///
|
||||
/// Global coordinates are offset by the Buffer's area offset (`x`/`y`).
|
||||
///
|
||||
/// Usage discouraged, as it exposes `self.content` as a linearly indexable array, which limits
|
||||
/// potential future abstractions. See <https://github.com/ratatui/ratatui/issues/1122>.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
@@ -503,38 +495,6 @@ impl Buffer {
|
||||
if !current.skip && (current != previous || invalidated > 0) && to_skip == 0 {
|
||||
let (x, y) = self.pos_of(i);
|
||||
updates.push((x, y, &next_buffer[i]));
|
||||
|
||||
// If the current cell is multi-width, ensure the trailing cells are explicitly
|
||||
// cleared when they previously contained non-blank content. Some terminals do not
|
||||
// reliably clear the trailing cell(s) when printing a wide grapheme, which can
|
||||
// result in visual artifacts (e.g., leftover characters). Emitting an explicit
|
||||
// update for the trailing cells avoids this.
|
||||
let symbol = current.symbol();
|
||||
let cell_width = symbol.width();
|
||||
// Work around terminals that fail to clear the trailing cell of certain
|
||||
// emoji presentation sequences (those containing VS16 / U+FE0F).
|
||||
// Only emit explicit clears for such sequences to avoid bloating diffs
|
||||
// for standard wide characters (e.g., CJK), which terminals handle well.
|
||||
let contains_vs16 = symbol.chars().any(|c| c == '\u{FE0F}');
|
||||
if cell_width > 1 && contains_vs16 {
|
||||
for k in 1..cell_width {
|
||||
let j = i + k;
|
||||
// Make sure that we are still inside the buffer.
|
||||
if j >= next_buffer.len() || j >= previous_buffer.len() {
|
||||
break;
|
||||
}
|
||||
let prev_trailing = &previous_buffer[j];
|
||||
let next_trailing = &next_buffer[j];
|
||||
if !next_trailing.skip && prev_trailing != next_trailing {
|
||||
let (tx, ty) = self.pos_of(j);
|
||||
// Push an explicit update for the trailing cell.
|
||||
// This is expected to be a blank cell, but we use the actual
|
||||
// content from the next buffer to handle cases where
|
||||
// the user has explicitly set something else.
|
||||
updates.push((tx, ty, next_trailing));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
to_skip = current.symbol().width().saturating_sub(1);
|
||||
@@ -1288,9 +1248,6 @@ mod tests {
|
||||
// Both eye and speech bubble include a 'display as emoji' variation selector
|
||||
// Prior to unicode-width 0.2, this was incorrectly detected as width 4 for some reason
|
||||
#[case::eye_speechbubble("👁️🗨️", "👁️🗨️xxxxx")]
|
||||
// Keyboard keycap emoji: base symbol + VS16 for emoji presentation
|
||||
// This should render as a single grapheme with width 2.
|
||||
#[case::keyboard_emoji("⌨️", "⌨️xxxxx")]
|
||||
fn renders_emoji(#[case] input: &str, #[case] expected: &str) {
|
||||
use unicode_width::UnicodeWidthChar;
|
||||
|
||||
@@ -1340,34 +1297,4 @@ mod tests {
|
||||
assert_eq!(buffer.index_of(255, 256), 65791);
|
||||
assert_eq!(buffer.pos_of(65791), (255, 256)); // previously (255, 0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_clears_trailing_cell_for_wide_grapheme() {
|
||||
// Reproduce: write "ab", then overwrite with a wide emoji like "⌨️"
|
||||
let prev = Buffer::with_lines(["ab"]); // width 2 area inferred
|
||||
assert_eq!(prev.area.width, 2);
|
||||
|
||||
let mut next = Buffer::with_lines([" "]); // start with blanks
|
||||
next.set_string(0, 0, "⌨️", Style::new());
|
||||
|
||||
// The next buffer contains a wide grapheme occupying cell 0 and implicitly cell 1.
|
||||
// The debug formatting shows the hidden trailing space.
|
||||
let expected_next = Buffer::with_lines(["⌨️"]);
|
||||
assert_eq!(next, expected_next);
|
||||
|
||||
// The diff should include an update for (0,0) to draw the emoji. Depending on
|
||||
// terminal behavior, it may or may not be necessary to explicitly clear (1,0).
|
||||
// At minimum, ensure the first cell is updated and nothing incorrect is emitted.
|
||||
let diff = prev.diff(&next);
|
||||
assert!(
|
||||
diff.iter()
|
||||
.any(|(x, y, c)| *x == 0 && *y == 0 && c.symbol() == "⌨️")
|
||||
);
|
||||
// And it should explicitly clear the trailing cell (1,0) to avoid leftovers on terminals
|
||||
// that don't automatically clear the following cell for wide characters.
|
||||
assert!(
|
||||
diff.iter()
|
||||
.any(|(x, y, c)| *x == 1 && *y == 0 && c.symbol() == " ")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,7 +316,6 @@ mod direction;
|
||||
mod flex;
|
||||
mod layout;
|
||||
mod margin;
|
||||
mod offset;
|
||||
mod position;
|
||||
mod rect;
|
||||
mod size;
|
||||
@@ -327,7 +326,6 @@ pub use direction::Direction;
|
||||
pub use flex::Flex;
|
||||
pub use layout::{Layout, Spacing};
|
||||
pub use margin::Margin;
|
||||
pub use offset::Offset;
|
||||
pub use position::Position;
|
||||
pub use rect::{Columns, Positions, Rect, Rows};
|
||||
pub use rect::{Columns, Offset, Positions, Rect, Rows};
|
||||
pub use size::Size;
|
||||
|
||||
@@ -12,27 +12,11 @@ use strum::{Display, EnumString};
|
||||
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum Direction {
|
||||
/// Layout segments are arranged side by side (left to right).
|
||||
Horizontal,
|
||||
/// Layout segments are arranged top to bottom (default).
|
||||
#[default]
|
||||
Vertical,
|
||||
}
|
||||
|
||||
impl Direction {
|
||||
/// The perpendicular direction to this direction.
|
||||
///
|
||||
/// `Horizontal` returns `Vertical`, and `Vertical` returns `Horizontal`.
|
||||
#[inline]
|
||||
#[must_use = "returns the perpendicular direction"]
|
||||
pub const fn perpendicular(self) -> Self {
|
||||
match self {
|
||||
Self::Horizontal => Self::Vertical,
|
||||
Self::Vertical => Self::Horizontal,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use alloc::string::ToString;
|
||||
@@ -53,11 +37,4 @@ mod tests {
|
||||
assert_eq!("Vertical".parse::<Direction>(), Ok(Direction::Vertical));
|
||||
assert_eq!("".parse::<Direction>(), Err(ParseError::VariantNotFound));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn other() {
|
||||
use Direction::*;
|
||||
assert_eq!(Horizontal.perpendicular(), Vertical);
|
||||
assert_eq!(Vertical.perpendicular(), Horizontal);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ impl From<i16> for Spacing {
|
||||
///
|
||||
/// When the layout is computed, the result is cached in a thread-local cache, so that subsequent
|
||||
/// calls with the same parameters are faster. The cache is a `LruCache`, and the size of the cache
|
||||
/// can be configured using [`Layout::init_cache()`] when the `layout-cache` feature is enabled.
|
||||
/// can be configured using [`Layout::init_cache()`].
|
||||
///
|
||||
/// # Construction
|
||||
///
|
||||
@@ -203,8 +203,8 @@ impl Layout {
|
||||
/// on my laptop's terminal (171+51 = 222) and doubling it for good measure and then adding a
|
||||
/// bit more to make it a round number. This gives enough entries to store a layout for every
|
||||
/// row and every column, twice over, which should be enough for most apps. For those that need
|
||||
/// more, the cache size can be set with `Layout::init_cache()` (requires the `layout-cache`
|
||||
/// feature).
|
||||
/// more, the cache size can be set with [`Layout::init_cache()`].
|
||||
/// This const is unused if layout cache is disabled.
|
||||
#[cfg(feature = "layout-cache")]
|
||||
pub const DEFAULT_CACHE_SIZE: usize = 500;
|
||||
|
||||
@@ -636,8 +636,8 @@ impl Layout {
|
||||
///
|
||||
/// This method stores the result of the computation in a thread-local cache keyed on the layout
|
||||
/// and area, so that subsequent calls with the same parameters are faster. The cache is a
|
||||
/// `LruCache`, and grows until [`Self::DEFAULT_CACHE_SIZE`] is reached by default. If the cache
|
||||
/// is initialized with [`Layout::init_cache()`], it grows until the initialized cache size.
|
||||
/// `LruCache`, and grows until [`Self::DEFAULT_CACHE_SIZE`] is reached by default, if the cache
|
||||
/// is initialized with the [`Layout::init_cache()`] grows until the initialized cache size.
|
||||
///
|
||||
/// There is a helper method that can be used to split the whole area into smaller ones based on
|
||||
/// the layout: [`Layout::areas()`]. That method is a shortcut for calling this method. It
|
||||
@@ -673,8 +673,8 @@ impl Layout {
|
||||
///
|
||||
/// This method stores the result of the computation in a thread-local cache keyed on the layout
|
||||
/// and area, so that subsequent calls with the same parameters are faster. The cache is a
|
||||
/// `LruCache`, and grows until [`Self::DEFAULT_CACHE_SIZE`] is reached by default. If the cache
|
||||
/// is initialized with [`Layout::init_cache()`], it grows until the initialized cache size.
|
||||
/// `LruCache`, and grows until [`Self::DEFAULT_CACHE_SIZE`] is reached by default, if the cache
|
||||
/// is initialized with the [`Layout::init_cache()`] grows until the initialized cache size.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
use crate::layout::Position;
|
||||
|
||||
/// Amounts by which to move a [`Rect`](crate::layout::Rect).
|
||||
///
|
||||
/// Positive numbers move to the right/bottom and negative to the left/top.
|
||||
///
|
||||
/// See [`Rect::offset`](crate::layout::Rect::offset) for usage.
|
||||
#[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,
|
||||
|
||||
/// How much to move on the Y axis
|
||||
pub y: i32,
|
||||
}
|
||||
|
||||
impl Offset {
|
||||
/// A zero offset
|
||||
pub const ZERO: Self = Self::new(0, 0);
|
||||
|
||||
/// The minimum offset
|
||||
pub const MIN: Self = Self::new(i32::MIN, i32::MIN);
|
||||
|
||||
/// The maximum offset
|
||||
pub const MAX: Self = Self::new(i32::MAX, i32::MAX);
|
||||
|
||||
/// Creates a new `Offset` with the given values.
|
||||
pub const fn new(x: i32, y: i32) -> Self {
|
||||
Self { x, y }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Position> for Offset {
|
||||
fn from(position: Position) -> Self {
|
||||
Self {
|
||||
x: i32::from(position.x),
|
||||
y: i32::from(position.y),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn new_sets_components() {
|
||||
assert_eq!(Offset::new(-3, 7), Offset { x: -3, y: 7 });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn constants_match_expected_values() {
|
||||
assert_eq!(Offset::ZERO, Offset::new(0, 0));
|
||||
assert_eq!(Offset::MIN, Offset::new(i32::MIN, i32::MIN));
|
||||
assert_eq!(Offset::MAX, Offset::new(i32::MAX, i32::MAX));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_position_converts_coordinates() {
|
||||
let position = Position::new(4, 9);
|
||||
let offset = Offset::from(position);
|
||||
|
||||
assert_eq!(offset, Offset::new(4, 9));
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
#![warn(missing_docs)]
|
||||
use core::fmt;
|
||||
use core::ops::{Add, AddAssign, Sub, SubAssign};
|
||||
|
||||
use crate::layout::{Offset, Rect};
|
||||
use crate::layout::Rect;
|
||||
|
||||
/// Position in the terminal coordinate system.
|
||||
///
|
||||
@@ -24,18 +23,10 @@ use crate::layout::{Offset, Rect};
|
||||
/// - [`from(Rect)`](Self::from) - Create from [`Rect`] (uses top-left corner)
|
||||
/// - [`into((u16, u16))`] - Convert to `(u16, u16)` tuple
|
||||
///
|
||||
/// # Movement
|
||||
///
|
||||
/// - [`offset`](Self::offset) - Move by an [`Offset`]
|
||||
/// - [`Add<Offset>`](core::ops::Add) and [`Sub<Offset>`](core::ops::Sub) - Shift by offsets with
|
||||
/// clamping
|
||||
/// - [`AddAssign<Offset>`](core::ops::AddAssign) and [`SubAssign<Offset>`](core::ops::SubAssign) -
|
||||
/// In-place shifting
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui_core::layout::{Offset, Position, Rect};
|
||||
/// use ratatui_core::layout::{Position, Rect};
|
||||
///
|
||||
/// // the following are all equivalent
|
||||
/// let position = Position { x: 1, y: 2 };
|
||||
@@ -45,10 +36,6 @@ use crate::layout::{Offset, Rect};
|
||||
///
|
||||
/// // position can be converted back into the components when needed
|
||||
/// let (x, y) = position.into();
|
||||
///
|
||||
/// // movement by offsets
|
||||
/// let position = Position::new(5, 5) + Offset::new(2, -3);
|
||||
/// assert_eq!(position, Position::new(7, 2));
|
||||
/// ```
|
||||
///
|
||||
/// For comprehensive layout documentation and examples, see the [`layout`](crate::layout) module.
|
||||
@@ -70,27 +57,12 @@ pub struct Position {
|
||||
|
||||
impl Position {
|
||||
/// Position at the origin, the top left edge at 0,0
|
||||
pub const ORIGIN: Self = Self::new(0, 0);
|
||||
|
||||
/// Position at the minimum x and y values
|
||||
pub const MIN: Self = Self::ORIGIN;
|
||||
|
||||
/// Position at the maximum x and y values
|
||||
pub const MAX: Self = Self::new(u16::MAX, u16::MAX);
|
||||
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 }
|
||||
}
|
||||
|
||||
/// Moves the position by the given offset.
|
||||
///
|
||||
/// Positive offsets move right and down, negative offsets move left and up. Values that would
|
||||
/// move the position outside the `u16` range are clamped to the nearest edge.
|
||||
#[must_use = "method returns the modified value"]
|
||||
pub fn offset(self, offset: Offset) -> Self {
|
||||
self + offset
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(u16, u16)> for Position {
|
||||
@@ -117,68 +89,6 @@ impl fmt::Display for Position {
|
||||
}
|
||||
}
|
||||
|
||||
impl Add<Offset> for Position {
|
||||
type Output = Self;
|
||||
|
||||
/// Moves the position by the given offset.
|
||||
///
|
||||
/// Values that would move the position outside the `u16` range are clamped to the nearest
|
||||
/// edge.
|
||||
fn add(self, offset: Offset) -> Self {
|
||||
let max = i32::from(u16::MAX);
|
||||
let x = i32::from(self.x).saturating_add(offset.x).clamp(0, max) as u16;
|
||||
let y = i32::from(self.y).saturating_add(offset.y).clamp(0, max) as u16;
|
||||
Self { x, y }
|
||||
}
|
||||
}
|
||||
|
||||
impl Add<Position> for Offset {
|
||||
type Output = Position;
|
||||
|
||||
/// Moves the position by the given offset.
|
||||
///
|
||||
/// Values that would move the position outside the `u16` range are clamped to the nearest
|
||||
/// edge.
|
||||
fn add(self, position: Position) -> Position {
|
||||
position + self
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub<Offset> for Position {
|
||||
type Output = Self;
|
||||
|
||||
/// Moves the position by the inverse of the given offset.
|
||||
///
|
||||
/// Values that would move the position outside the `u16` range are clamped to the nearest
|
||||
/// edge.
|
||||
fn sub(self, offset: Offset) -> Self {
|
||||
let max = i32::from(u16::MAX);
|
||||
let x = i32::from(self.x).saturating_sub(offset.x).clamp(0, max) as u16;
|
||||
let y = i32::from(self.y).saturating_sub(offset.y).clamp(0, max) as u16;
|
||||
Self { x, y }
|
||||
}
|
||||
}
|
||||
|
||||
impl AddAssign<Offset> for Position {
|
||||
/// Moves the position in place by the given offset.
|
||||
///
|
||||
/// Values that would move the position outside the `u16` range are clamped to the nearest
|
||||
/// edge.
|
||||
fn add_assign(&mut self, offset: Offset) {
|
||||
*self = *self + offset;
|
||||
}
|
||||
}
|
||||
|
||||
impl SubAssign<Offset> for Position {
|
||||
/// Moves the position in place by the inverse of the given offset.
|
||||
///
|
||||
/// Values that would move the position outside the `u16` range are clamped to the nearest
|
||||
/// edge.
|
||||
fn sub_assign(&mut self, offset: Offset) {
|
||||
*self = *self - offset;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use alloc::string::ToString;
|
||||
@@ -188,15 +98,15 @@ mod tests {
|
||||
#[test]
|
||||
fn new() {
|
||||
let position = Position::new(1, 2);
|
||||
|
||||
assert_eq!(position, Position { x: 1, y: 2 });
|
||||
assert_eq!(position.x, 1);
|
||||
assert_eq!(position.y, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_tuple() {
|
||||
let position = Position::from((1, 2));
|
||||
|
||||
assert_eq!(position, Position { x: 1, y: 2 });
|
||||
assert_eq!(position.x, 1);
|
||||
assert_eq!(position.y, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -211,8 +121,8 @@ mod tests {
|
||||
fn from_rect() {
|
||||
let rect = Rect::new(1, 2, 3, 4);
|
||||
let position = Position::from(rect);
|
||||
|
||||
assert_eq!(position, Position { x: 1, y: 2 });
|
||||
assert_eq!(position.x, 1);
|
||||
assert_eq!(position.y, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -220,34 +130,4 @@ mod tests {
|
||||
let position = Position::new(1, 2);
|
||||
assert_eq!(position.to_string(), "(1, 2)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offset_moves_position() {
|
||||
let position = Position::new(2, 3).offset(Offset::new(5, 7));
|
||||
|
||||
assert_eq!(position, Position::new(7, 10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offset_clamps_to_bounds() {
|
||||
let position = Position::new(1, 1).offset(Offset::MAX);
|
||||
|
||||
assert_eq!(position, Position::MAX);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_and_subtract_offset() {
|
||||
let position = Position::new(10, 10) + Offset::new(-3, 4) - Offset::new(5, 20);
|
||||
|
||||
assert_eq!(position, Position::new(2, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_assign_and_sub_assign_offset() {
|
||||
let mut position = Position::new(5, 5);
|
||||
position += Offset::new(2, 3);
|
||||
position -= Offset::new(10, 1);
|
||||
|
||||
assert_eq!(position, Position::new(0, 7));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,10 @@ use core::array::TryFromSliceError;
|
||||
use core::cmp::{max, min};
|
||||
use core::fmt;
|
||||
|
||||
pub use self::iter::{Columns, Positions, Rows};
|
||||
use crate::layout::{Margin, Offset, Position, Size};
|
||||
use crate::layout::{Margin, Position, Size};
|
||||
|
||||
mod iter;
|
||||
mod ops;
|
||||
pub use iter::*;
|
||||
|
||||
use super::{Constraint, Flex, Layout};
|
||||
|
||||
@@ -46,7 +45,6 @@ use super::{Constraint, Flex, Layout};
|
||||
///
|
||||
/// - [`inner`](Self::inner), [`outer`](Self::outer) - Apply margins to shrink or expand
|
||||
/// - [`offset`](Self::offset) - Move the rectangle by a relative amount
|
||||
/// - [`resize`](Self::resize) - Change the rectangle size while keeping the bottom/right in range
|
||||
/// - [`union`](Self::union) - Combine with another rectangle to create a bounding box
|
||||
/// - [`intersection`](Self::intersection) - Find the overlapping area with another rectangle
|
||||
/// - [`clamp`](Self::clamp) - Constrain the rectangle to fit within another
|
||||
@@ -68,67 +66,21 @@ use super::{Constraint, Flex, Layout};
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// To create a new `Rect`, use [`Rect::new`]. The size of the `Rect` will be clamped to keep the
|
||||
/// right and bottom coordinates within `u16`. Note that this clamping does not occur when creating
|
||||
/// a `Rect` directly.
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui_core::layout::Rect;
|
||||
///
|
||||
/// let rect = Rect::new(1, 2, 3, 4);
|
||||
/// assert_eq!(
|
||||
/// rect,
|
||||
/// Rect {
|
||||
/// x: 1,
|
||||
/// y: 2,
|
||||
/// width: 3,
|
||||
/// height: 4
|
||||
/// }
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// You can also create a `Rect` from a [`Position`] and a [`Size`].
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui_core::layout::{Position, Rect, Size};
|
||||
///
|
||||
/// let position = Position::new(1, 2);
|
||||
/// let size = Size::new(3, 4);
|
||||
/// let rect = Rect::from((position, size));
|
||||
/// assert_eq!(
|
||||
/// rect,
|
||||
/// Rect {
|
||||
/// x: 1,
|
||||
/// y: 2,
|
||||
/// width: 3,
|
||||
/// height: 4
|
||||
/// }
|
||||
/// );
|
||||
/// ```
|
||||
/// // Create a rectangle manually
|
||||
/// let rect = Rect::new(10, 5, 80, 20);
|
||||
/// assert_eq!(rect.x, 10);
|
||||
/// assert_eq!(rect.y, 5);
|
||||
/// assert_eq!(rect.width, 80);
|
||||
/// assert_eq!(rect.height, 20);
|
||||
///
|
||||
/// To move a `Rect` without modifying its size, add or subtract an [`Offset`] to it.
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui_core::layout::{Offset, Rect};
|
||||
///
|
||||
/// let rect = Rect::new(1, 2, 3, 4);
|
||||
/// let offset = Offset::new(5, 6);
|
||||
/// let moved_rect = rect + offset;
|
||||
/// assert_eq!(moved_rect, Rect::new(6, 8, 3, 4));
|
||||
/// ```
|
||||
///
|
||||
/// To resize a `Rect` while ensuring it stays within bounds, use [`Rect::resize`]. The size is
|
||||
/// clamped so that `right()` and `bottom()` do not exceed `u16::MAX`.
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui_core::layout::{Rect, Size};
|
||||
///
|
||||
/// let rect = Rect::new(u16::MAX - 1, u16::MAX - 1, 1, 1).resize(Size::new(10, 10));
|
||||
/// assert_eq!(rect, Rect::new(u16::MAX - 1, u16::MAX - 1, 1, 1));
|
||||
/// // Create from position and size
|
||||
/// let rect = Rect::from((Position::new(10, 5), Size::new(80, 20)));
|
||||
/// ```
|
||||
///
|
||||
/// For comprehensive layout documentation and examples, see the [`layout`](crate::layout) module.
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Rect {
|
||||
@@ -142,6 +94,30 @@ pub struct Rect {
|
||||
pub height: u16,
|
||||
}
|
||||
|
||||
/// Amounts by which to move a [`Rect`](crate::layout::Rect).
|
||||
///
|
||||
/// Positive numbers move to the right/bottom and negative to the left/top.
|
||||
///
|
||||
/// See [`Rect::offset`]
|
||||
#[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,
|
||||
/// How much to move on the Y axis
|
||||
pub y: i32,
|
||||
}
|
||||
|
||||
impl Offset {
|
||||
/// A zero offset
|
||||
pub const ZERO: Self = Self { x: 0, y: 0 };
|
||||
|
||||
/// Creates a new `Offset` with the given values.
|
||||
pub const fn new(x: i32, y: i32) -> Self {
|
||||
Self { x, y }
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Rect {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}x{}+{}+{}", self.width, self.height, self.x, self.y)
|
||||
@@ -157,12 +133,6 @@ impl Rect {
|
||||
height: 0,
|
||||
};
|
||||
|
||||
/// The minimum possible Rect
|
||||
pub const MIN: Self = Self::ZERO;
|
||||
|
||||
/// The maximum possible Rect
|
||||
pub const MAX: Self = Self::new(0, 0, u16::MAX, u16::MAX);
|
||||
|
||||
/// Creates a new `Rect`, with width and height limited to keep both bounds within `u16`.
|
||||
///
|
||||
/// If the width or height would cause the right or bottom coordinate to be larger than the
|
||||
@@ -177,8 +147,15 @@ impl Rect {
|
||||
/// let rect = Rect::new(1, 2, 3, 4);
|
||||
/// ```
|
||||
pub const fn new(x: u16, y: u16, width: u16, height: u16) -> Self {
|
||||
let width = x.saturating_add(width) - x;
|
||||
let height = y.saturating_add(height) - y;
|
||||
// these calculations avoid using min so that this function can be const
|
||||
let max_width = u16::MAX - x;
|
||||
let max_height = u16::MAX - y;
|
||||
let width = if width > max_width { max_width } else { width };
|
||||
let height = if height > max_height {
|
||||
max_height
|
||||
} else {
|
||||
height
|
||||
};
|
||||
Self {
|
||||
x,
|
||||
y,
|
||||
@@ -187,7 +164,8 @@ impl Rect {
|
||||
}
|
||||
}
|
||||
|
||||
/// The area of the `Rect`.
|
||||
/// The area of the `Rect`. If the area is larger than the maximum value of `u16`, it will be
|
||||
/// clamped to `u16::MAX`.
|
||||
pub const fn area(self) -> u32 {
|
||||
(self.width as u32) * (self.height as u32)
|
||||
}
|
||||
@@ -247,7 +225,7 @@ impl Rect {
|
||||
|
||||
/// Returns a new `Rect` outside the current one, with the given margin applied on each side.
|
||||
///
|
||||
/// If the margin causes the `Rect`'s bounds to be outside the range of a `u16`, the `Rect` will
|
||||
/// If the margin causes the `Rect`'s bounds to outsdie the range of a `u16`, the `Rect` will
|
||||
/// be truncated to keep the bounds within `u16`. This will cause the size of the `Rect` to
|
||||
/// change.
|
||||
///
|
||||
@@ -283,19 +261,13 @@ impl Rect {
|
||||
/// See [`Offset`] for details.
|
||||
#[must_use = "method returns the modified value"]
|
||||
pub fn offset(self, offset: Offset) -> Self {
|
||||
self + offset
|
||||
}
|
||||
|
||||
/// Resizes the `Rect`, clamping to keep the right and bottom within `u16::MAX`.
|
||||
///
|
||||
/// The position is preserved. If the requested size would push the `Rect` beyond the bounds of
|
||||
/// `u16`, the width or height is reduced so that [`right`](Self::right) and
|
||||
/// [`bottom`](Self::bottom) remain within range.
|
||||
#[must_use = "method returns the modified value"]
|
||||
pub const fn resize(self, size: Size) -> Self {
|
||||
Self {
|
||||
width: self.x.saturating_add(size.width).saturating_sub(self.x),
|
||||
height: self.y.saturating_add(size.height).saturating_sub(self.y),
|
||||
x: i32::from(self.x)
|
||||
.saturating_add(offset.x)
|
||||
.clamp(0, i32::from(u16::MAX - self.width)) as u16,
|
||||
y: i32::from(self.y)
|
||||
.saturating_add(offset.y)
|
||||
.clamp(0, i32::from(u16::MAX - self.height)) as u16,
|
||||
..self
|
||||
}
|
||||
}
|
||||
@@ -674,18 +646,6 @@ impl From<(Position, Size)> for Rect {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Size> for Rect {
|
||||
/// Creates a new `Rect` with the given size at [`Position::ORIGIN`] (0, 0).
|
||||
fn from(size: Size) -> Self {
|
||||
Self {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use alloc::string::ToString;
|
||||
@@ -840,16 +800,6 @@ mod tests {
|
||||
assert!(!Rect::new(1, 2, 3, 4).intersects(Rect::new(5, 6, 7, 8)));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::corner(Rect::new(0, 0, 10, 10), Rect::new(10, 10, 20, 20))]
|
||||
#[case::edge(Rect::new(0, 0, 10, 10), Rect::new(10, 0, 20, 10))]
|
||||
#[case::no_intersect(Rect::new(0, 0, 10, 10), Rect::new(11, 11, 20, 20))]
|
||||
#[case::contains(Rect::new(0, 0, 20, 20), Rect::new(5, 5, 10, 10))]
|
||||
fn mutual_intersect(#[case] rect0: Rect, #[case] rect1: Rect) {
|
||||
assert_eq!(rect0.intersection(rect1), rect1.intersection(rect0));
|
||||
assert_eq!(rect0.intersects(rect1), rect1.intersects(rect0));
|
||||
}
|
||||
|
||||
// the bounds of this rect are x: [1..=3], y: [2..=5]
|
||||
#[rstest]
|
||||
#[case::inside_top_left(Rect::new(1, 2, 3, 4), Position { x: 1, y: 2 }, true)]
|
||||
@@ -896,18 +846,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resize_updates_size() {
|
||||
let rect = Rect::new(10, 20, 5, 5).resize(Size::new(30, 40));
|
||||
assert_eq!(rect, Rect::new(10, 20, 30, 40));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resize_clamps_at_bounds() {
|
||||
let rect = Rect::new(u16::MAX - 2, u16::MAX - 3, 1, 1).resize(Size::new(10, 10));
|
||||
assert_eq!(rect, Rect::new(u16::MAX - 2, u16::MAX - 3, 2, 3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_be_const() {
|
||||
const RECT: Rect = Rect {
|
||||
@@ -1018,23 +956,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_size() {
|
||||
let size = Size {
|
||||
width: 3,
|
||||
height: 4,
|
||||
};
|
||||
assert_eq!(
|
||||
Rect::from(size),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 3,
|
||||
height: 4
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn centered_horizontally() {
|
||||
let rect = Rect::new(0, 0, 5, 5);
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
use core::ops::{Add, AddAssign, Neg, Sub, SubAssign};
|
||||
|
||||
use super::{Offset, Rect};
|
||||
|
||||
impl Neg for Offset {
|
||||
type Output = Self;
|
||||
|
||||
/// Negates the offset.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the negated value overflows (i.e. `x` or `y` is `i32::MIN`).
|
||||
fn neg(self) -> Self {
|
||||
Self {
|
||||
x: self.x.neg(),
|
||||
y: self.y.neg(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Add<Offset> for Rect {
|
||||
type Output = Self;
|
||||
|
||||
/// Moves the rect by an offset without changing its size.
|
||||
///
|
||||
/// If the offset would move the any of the rect's edges outside the bounds of `u16`, the
|
||||
/// rect's position is clamped to the nearest edge.
|
||||
fn add(self, offset: Offset) -> Self {
|
||||
let max_x = i32::from(u16::MAX - self.width);
|
||||
let max_y = i32::from(u16::MAX - self.height);
|
||||
let x = i32::from(self.x).saturating_add(offset.x).clamp(0, max_x) as u16;
|
||||
let y = i32::from(self.y).saturating_add(offset.y).clamp(0, max_y) as u16;
|
||||
Self { x, y, ..self }
|
||||
}
|
||||
}
|
||||
|
||||
impl Add<Rect> for Offset {
|
||||
type Output = Rect;
|
||||
|
||||
/// Moves the rect by an offset without changing its size.
|
||||
///
|
||||
/// If the offset would move the any of the rect's edges outside the bounds of `u16`, the
|
||||
/// rect's position is clamped to the nearest edge.
|
||||
fn add(self, rect: Rect) -> Rect {
|
||||
rect + self
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub<Offset> for Rect {
|
||||
type Output = Self;
|
||||
|
||||
/// Subtracts an offset from the rect without changing its size.
|
||||
///
|
||||
/// If the offset would move the any of the rect's edges outside the bounds of `u16`, the
|
||||
/// rect's position is clamped to the nearest
|
||||
fn sub(self, offset: Offset) -> Self {
|
||||
// Note this cannot be simplified to `self + -offset` because `Offset::MIN` would overflow
|
||||
let max_x = i32::from(u16::MAX - self.width);
|
||||
let max_y = i32::from(u16::MAX - self.height);
|
||||
let x = i32::from(self.x).saturating_sub(offset.x).clamp(0, max_x) as u16;
|
||||
let y = i32::from(self.y).saturating_sub(offset.y).clamp(0, max_y) as u16;
|
||||
Self { x, y, ..self }
|
||||
}
|
||||
}
|
||||
|
||||
impl AddAssign<Offset> for Rect {
|
||||
/// Moves the rect by an offset in place without changing its size.
|
||||
///
|
||||
/// If the offset would move the any of the rect's edges outside the bounds of `u16`, the
|
||||
/// rect's position is clamped to the nearest edge.
|
||||
fn add_assign(&mut self, offset: Offset) {
|
||||
*self = *self + offset;
|
||||
}
|
||||
}
|
||||
|
||||
impl SubAssign<Offset> for Rect {
|
||||
/// Moves the rect by an offset in place without changing its size.
|
||||
///
|
||||
/// If the offset would move the any of the rect's edges outside the bounds of `u16`, the
|
||||
/// rect's position is clamped to the nearest edge.
|
||||
fn sub_assign(&mut self, offset: Offset) {
|
||||
*self = *self - offset;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[rstest]
|
||||
#[case::zero(Rect::new(3, 4, 5, 6), Offset::ZERO, Rect::new(3, 4, 5, 6))]
|
||||
#[case::positive(Rect::new(3, 4, 5, 6), Offset::new(1, 2), Rect::new(4, 6, 5, 6))]
|
||||
#[case::negative(Rect::new(3, 4, 5, 6), Offset::new(-1, -2), Rect::new(2, 2, 5, 6))]
|
||||
#[case::saturate_negative(Rect::new(3, 4, 5, 6), Offset::MIN, Rect::new(0, 0, 5, 6))]
|
||||
#[case::saturate_positive(Rect::new(3, 4, 5, 6), Offset::MAX, Rect::new(u16::MAX- 5, u16::MAX - 6, 5, 6))]
|
||||
fn add_offset(#[case] rect: Rect, #[case] offset: Offset, #[case] expected: Rect) {
|
||||
assert_eq!(rect + offset, expected);
|
||||
assert_eq!(offset + rect, expected);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::zero(Rect::new(3, 4, 5, 6), Offset::ZERO, Rect::new(3, 4, 5, 6))]
|
||||
#[case::positive(Rect::new(3, 4, 5, 6), Offset::new(1, 2), Rect::new(2, 2, 5, 6))]
|
||||
#[case::negative(Rect::new(3, 4, 5, 6), Offset::new(-1, -2), Rect::new(4, 6, 5, 6))]
|
||||
#[case::saturate_negative(Rect::new(3, 4, 5, 6), Offset::MAX, Rect::new(0, 0, 5, 6))]
|
||||
#[case::saturate_positive(Rect::new(3, 4, 5, 6), -Offset::MAX, Rect::new(u16::MAX - 5, u16::MAX - 6, 5, 6))]
|
||||
fn sub_offset(#[case] rect: Rect, #[case] offset: Offset, #[case] expected: Rect) {
|
||||
assert_eq!(rect - offset, expected);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::zero(Rect::new(3, 4, 5, 6), Offset::ZERO, Rect::new(3, 4, 5, 6))]
|
||||
#[case::positive(Rect::new(3, 4, 5, 6), Offset::new(1, 2), Rect::new(4, 6, 5, 6))]
|
||||
#[case::negative(Rect::new(3, 4, 5, 6), Offset::new(-1, -2), Rect::new(2, 2, 5, 6))]
|
||||
#[case::saturate_negative(Rect::new(3, 4, 5, 6), Offset::MIN, Rect::new(0, 0, 5, 6))]
|
||||
#[case::saturate_positive(Rect::new(3, 4, 5, 6), Offset::MAX, Rect::new(u16::MAX - 5, u16::MAX - 6, 5, 6))]
|
||||
fn add_assign_offset(#[case] rect: Rect, #[case] offset: Offset, #[case] expected: Rect) {
|
||||
let mut rect = rect;
|
||||
rect += offset;
|
||||
assert_eq!(rect, expected);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::zero(Rect::new(3, 4, 5, 6), Offset::ZERO, Rect::new(3, 4, 5, 6))]
|
||||
#[case::positive(Rect::new(3, 4, 5, 6), Offset::new(1, 2), Rect::new(2, 2, 5, 6))]
|
||||
#[case::negative(Rect::new(3, 4, 5, 6), Offset::new(-1, -2), Rect::new(4, 6, 5, 6))]
|
||||
#[case::saturate_negative(Rect::new(3, 4, 5, 6), Offset::MAX, Rect::new(0, 0, 5, 6))]
|
||||
#[case::saturate_positive(Rect::new(3, 4, 5, 6), -Offset::MAX, Rect::new(u16::MAX - 5, u16::MAX - 6, 5, 6))]
|
||||
fn sub_assign_offset(#[case] rect: Rect, #[case] offset: Offset, #[case] expected: Rect) {
|
||||
let mut rect = rect;
|
||||
rect -= offset;
|
||||
assert_eq!(rect, expected);
|
||||
}
|
||||
}
|
||||
@@ -24,20 +24,14 @@ use crate::layout::Rect;
|
||||
/// - [`from(Rect)`](Self::from) - Create from [`Rect`] (uses width and height)
|
||||
/// - [`into((u16, u16))`] - Convert to `(u16, u16)` tuple
|
||||
///
|
||||
/// # Computation
|
||||
///
|
||||
/// - [`area`](Self::area) - Compute the total number of cells covered by the size
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui_core::layout::{Rect, Size};
|
||||
///
|
||||
/// let size = Size::new(80, 24);
|
||||
/// assert_eq!(size.area(), 1920);
|
||||
/// let size = Size::from((80, 24));
|
||||
/// let size = Size::from(Rect::new(0, 0, 80, 24));
|
||||
/// assert_eq!(size.area(), 1920);
|
||||
/// ```
|
||||
///
|
||||
/// For comprehensive layout documentation and examples, see the [`layout`](crate::layout) module.
|
||||
@@ -54,24 +48,10 @@ impl Size {
|
||||
/// A zero sized Size
|
||||
pub const ZERO: Self = Self::new(0, 0);
|
||||
|
||||
/// The minimum possible Size
|
||||
pub const MIN: Self = Self::ZERO;
|
||||
|
||||
/// The maximum possible Size
|
||||
pub const MAX: Self = Self::new(u16::MAX, u16::MAX);
|
||||
|
||||
/// Create a new `Size` struct
|
||||
pub const fn new(width: u16, height: u16) -> Self {
|
||||
Self { width, height }
|
||||
}
|
||||
|
||||
/// Compute the total area of the size as a `u32`.
|
||||
///
|
||||
/// The multiplication uses `u32` to avoid overflow when the width and height are at their
|
||||
/// `u16` maximum values.
|
||||
pub const fn area(self) -> u32 {
|
||||
self.width as u32 * self.height as u32
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(u16, u16)> for Size {
|
||||
@@ -80,12 +60,6 @@ impl From<(u16, u16)> for Size {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Size> for (u16, u16) {
|
||||
fn from(size: Size) -> Self {
|
||||
(size.width, size.height)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Rect> for Size {
|
||||
fn from(rect: Rect) -> Self {
|
||||
rect.as_size()
|
||||
@@ -118,14 +92,6 @@ mod tests {
|
||||
assert_eq!(size.height, 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_tuple() {
|
||||
let size = Size::from((10, 20));
|
||||
let (width, height) = size.into();
|
||||
assert_eq!(size.width, width);
|
||||
assert_eq!(size.height, height);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_rect() {
|
||||
let size = Size::from(Rect::new(0, 0, 10, 20));
|
||||
@@ -137,11 +103,4 @@ mod tests {
|
||||
fn display() {
|
||||
assert_eq!(Size::new(10, 20).to_string(), "10x20");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn area() {
|
||||
assert_eq!(Size::new(10, 20).area(), 200);
|
||||
assert_eq!(Size::new(0, 0).area(), 0);
|
||||
assert_eq!(Size::new(u16::MAX, u16::MAX).area(), 4_294_836_225_u32);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#![no_std]
|
||||
// show the feature flags in the generated documentation
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![doc(
|
||||
html_logo_url = "https://raw.githubusercontent.com/ratatui/ratatui/main/assets/logo.png",
|
||||
html_favicon_url = "https://raw.githubusercontent.com/ratatui/ratatui/main/assets/favicon.ico"
|
||||
|
||||
@@ -238,53 +238,18 @@ impl fmt::Debug for Modifier {
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Style {
|
||||
/// The foreground color.
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub fg: Option<Color>,
|
||||
/// The background color.
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub bg: Option<Color>,
|
||||
/// The underline color.
|
||||
#[cfg(feature = "underline-color")]
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub underline_color: Option<Color>,
|
||||
/// The modifiers to add.
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
serde(
|
||||
default,
|
||||
skip_serializing_if = "Modifier::is_empty",
|
||||
deserialize_with = "deserialize_modifier"
|
||||
)
|
||||
)]
|
||||
pub add_modifier: Modifier,
|
||||
/// The modifiers to remove.
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
serde(
|
||||
default,
|
||||
skip_serializing_if = "Modifier::is_empty",
|
||||
deserialize_with = "deserialize_modifier"
|
||||
)
|
||||
)]
|
||||
pub sub_modifier: Modifier,
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
/// Deserialize a [`Modifier`] while treating missing or `null` values as empty.
|
||||
///
|
||||
/// This helper is used with serde to coerce absent or `null` modifier fields to
|
||||
/// [`Modifier::empty`], allowing configuration files to omit these fields
|
||||
/// without triggering deserialization errors.
|
||||
fn deserialize_modifier<'de, D>(deserializer: D) -> Result<Modifier, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
use serde::Deserialize;
|
||||
|
||||
Option::<Modifier>::deserialize(deserializer)
|
||||
.map(|modifier| modifier.unwrap_or_else(Modifier::empty))
|
||||
}
|
||||
|
||||
/// A custom debug implementation that prints only the fields that are not the default, and unwraps
|
||||
/// the `Option`s.
|
||||
impl fmt::Debug for Style {
|
||||
@@ -433,22 +398,6 @@ impl Style {
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns `true` if the style has the given modifier set.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui_core::style::{Modifier, Style};
|
||||
///
|
||||
/// let style = Style::default().add_modifier(Modifier::BOLD | Modifier::ITALIC);
|
||||
/// assert!(style.has_modifier(Modifier::BOLD));
|
||||
/// assert!(style.has_modifier(Modifier::ITALIC));
|
||||
/// assert!(!style.has_modifier(Modifier::UNDERLINED));
|
||||
/// ```
|
||||
pub const fn has_modifier(self, modifier: Modifier) -> bool {
|
||||
self.add_modifier.contains(modifier) && !self.sub_modifier.contains(modifier)
|
||||
}
|
||||
|
||||
/// Results in a combined style that is equivalent to applying the two individual styles to
|
||||
/// a style one after the other.
|
||||
///
|
||||
@@ -828,28 +777,6 @@ mod tests {
|
||||
assert_eq!(ALL, ALL_SHORT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn has_modifier_checks() {
|
||||
// basic presence
|
||||
let style = Style::new().add_modifier(Modifier::BOLD | Modifier::ITALIC);
|
||||
assert!(style.has_modifier(Modifier::BOLD));
|
||||
assert!(style.has_modifier(Modifier::ITALIC));
|
||||
assert!(!style.has_modifier(Modifier::UNDERLINED));
|
||||
|
||||
// removal prevents the modifier from being reported as present
|
||||
let style = Style::new()
|
||||
.add_modifier(Modifier::BOLD | Modifier::ITALIC)
|
||||
.remove_modifier(Modifier::ITALIC);
|
||||
assert!(style.has_modifier(Modifier::BOLD));
|
||||
assert!(!style.has_modifier(Modifier::ITALIC));
|
||||
|
||||
// patching with a style that removes a modifier clears it
|
||||
let style = Style::new().add_modifier(Modifier::BOLD | Modifier::ITALIC);
|
||||
let patched = style.patch(Style::new().remove_modifier(Modifier::ITALIC));
|
||||
assert!(patched.has_modifier(Modifier::BOLD));
|
||||
assert!(!patched.has_modifier(Modifier::ITALIC));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(Style::new().black(), Color::Black)]
|
||||
#[case(Style::new().red(), Color::Red)]
|
||||
@@ -996,74 +923,4 @@ mod tests {
|
||||
.remove_modifier(Modifier::DIM)
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
#[test]
|
||||
fn serialize_then_deserialize() {
|
||||
let style = Style {
|
||||
fg: Some(Color::Rgb(255, 0, 255)),
|
||||
bg: Some(Color::White),
|
||||
#[cfg(feature = "underline-color")]
|
||||
underline_color: Some(Color::Indexed(3)),
|
||||
add_modifier: Modifier::UNDERLINED,
|
||||
sub_modifier: Modifier::CROSSED_OUT,
|
||||
};
|
||||
|
||||
let json_str = serde_json::to_string(&style).unwrap();
|
||||
let json_value: serde_json::Value = serde_json::from_str(&json_str).unwrap();
|
||||
|
||||
let mut expected_json = serde_json::json!({
|
||||
"fg": "#FF00FF",
|
||||
"bg": "White",
|
||||
"add_modifier": "UNDERLINED",
|
||||
"sub_modifier": "CROSSED_OUT"
|
||||
});
|
||||
|
||||
#[cfg(feature = "underline-color")]
|
||||
{
|
||||
expected_json
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.insert("underline_color".into(), "3".into());
|
||||
}
|
||||
|
||||
assert_eq!(json_value, expected_json);
|
||||
|
||||
let deserialized: Style = serde_json::from_str(&json_str).unwrap();
|
||||
assert_eq!(deserialized, style);
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
#[test]
|
||||
fn deserialize_defaults() {
|
||||
let style = Style {
|
||||
fg: None,
|
||||
bg: None,
|
||||
#[cfg(feature = "underline-color")]
|
||||
underline_color: None,
|
||||
add_modifier: Modifier::empty(),
|
||||
sub_modifier: Modifier::empty(),
|
||||
};
|
||||
|
||||
let json_str = serde_json::to_string(&style).unwrap();
|
||||
assert_eq!(json_str, "{}");
|
||||
|
||||
let deserialized: Style = serde_json::from_str(&json_str).unwrap();
|
||||
assert_eq!(deserialized, style);
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
#[test]
|
||||
fn deserialize_null_modifiers() {
|
||||
let json_value = serde_json::json!({
|
||||
"add_modifier": serde_json::Value::Null,
|
||||
"sub_modifier": serde_json::Value::Null
|
||||
});
|
||||
let json_str = serde_json::to_string(&json_value).unwrap();
|
||||
|
||||
let style: Style = serde_json::from_str(&json_str).unwrap();
|
||||
|
||||
assert!(style.add_modifier.is_empty());
|
||||
assert!(style.sub_modifier.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -655,8 +655,8 @@ 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 shouldn't panic
|
||||
"resets", // typo
|
||||
"#1🦀2", // len 7 but on char boundaries shouldnt panic
|
||||
"resett", // typo
|
||||
"lightblackk", // typo
|
||||
];
|
||||
|
||||
|
||||
@@ -10,6 +10,5 @@ pub mod half_block;
|
||||
pub mod line;
|
||||
pub mod marker;
|
||||
pub mod merge;
|
||||
pub mod pixel;
|
||||
pub mod scrollbar;
|
||||
pub mod shade;
|
||||
|
||||
@@ -1,21 +1,7 @@
|
||||
//! Braille symbols.
|
||||
//!
|
||||
//! Note that the symbols are not listed according to their unicode codepoint but according to the
|
||||
//! corresponding bit pattern in row-major order.
|
||||
|
||||
pub const BRAILLE: [char; 256] = [
|
||||
'⠀', '⠁', '⠈', '⠉', '⠂', '⠃', '⠊', '⠋', '⠐', '⠑', '⠘', '⠙', '⠒', '⠓', '⠚', '⠛', '⠄', '⠅', '⠌',
|
||||
'⠍', '⠆', '⠇', '⠎', '⠏', '⠔', '⠕', '⠜', '⠝', '⠖', '⠗', '⠞', '⠟', '⠠', '⠡', '⠨', '⠩', '⠢', '⠣',
|
||||
'⠪', '⠫', '⠰', '⠱', '⠸', '⠹', '⠲', '⠳', '⠺', '⠻', '⠤', '⠥', '⠬', '⠭', '⠦', '⠧', '⠮', '⠯', '⠴',
|
||||
'⠵', '⠼', '⠽', '⠶', '⠷', '⠾', '⠿', '⡀', '⡁', '⡈', '⡉', '⡂', '⡃', '⡊', '⡋', '⡐', '⡑', '⡘', '⡙',
|
||||
'⡒', '⡓', '⡚', '⡛', '⡄', '⡅', '⡌', '⡍', '⡆', '⡇', '⡎', '⡏', '⡔', '⡕', '⡜', '⡝', '⡖', '⡗', '⡞',
|
||||
'⡟', '⡠', '⡡', '⡨', '⡩', '⡢', '⡣', '⡪', '⡫', '⡰', '⡱', '⡸', '⡹', '⡲', '⡳', '⡺', '⡻', '⡤', '⡥',
|
||||
'⡬', '⡭', '⡦', '⡧', '⡮', '⡯', '⡴', '⡵', '⡼', '⡽', '⡶', '⡷', '⡾', '⡿', '⢀', '⢁', '⢈', '⢉', '⢂',
|
||||
'⢃', '⢊', '⢋', '⢐', '⢑', '⢘', '⢙', '⢒', '⢓', '⢚', '⢛', '⢄', '⢅', '⢌', '⢍', '⢆', '⢇', '⢎', '⢏',
|
||||
'⢔', '⢕', '⢜', '⢝', '⢖', '⢗', '⢞', '⢟', '⢠', '⢡', '⢨', '⢩', '⢢', '⢣', '⢪', '⢫', '⢰', '⢱', '⢸',
|
||||
'⢹', '⢲', '⢳', '⢺', '⢻', '⢤', '⢥', '⢬', '⢭', '⢦', '⢧', '⢮', '⢯', '⢴', '⢵', '⢼', '⢽', '⢶', '⢷',
|
||||
'⢾', '⢿', '⣀', '⣁', '⣈', '⣉', '⣂', '⣃', '⣊', '⣋', '⣐', '⣑', '⣘', '⣙', '⣒', '⣓', '⣚', '⣛', '⣄',
|
||||
'⣅', '⣌', '⣍', '⣆', '⣇', '⣎', '⣏', '⣔', '⣕', '⣜', '⣝', '⣖', '⣗', '⣞', '⣟', '⣠', '⣡', '⣨', '⣩',
|
||||
'⣢', '⣣', '⣪', '⣫', '⣰', '⣱', '⣸', '⣹', '⣲', '⣳', '⣺', '⣻', '⣤', '⣥', '⣬', '⣭', '⣦', '⣧', '⣮',
|
||||
'⣯', '⣴', '⣵', '⣼', '⣽', '⣶', '⣷', '⣾', '⣿',
|
||||
pub const BLANK: u16 = 0x2800;
|
||||
pub const DOTS: [[u16; 2]; 4] = [
|
||||
[0x0001, 0x0008],
|
||||
[0x0002, 0x0010],
|
||||
[0x0004, 0x0020],
|
||||
[0x0040, 0x0080],
|
||||
];
|
||||
|
||||
@@ -4,7 +4,6 @@ pub const DOT: &str = "•";
|
||||
|
||||
/// Marker to use when plotting data points
|
||||
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
#[non_exhaustive]
|
||||
pub enum Marker {
|
||||
/// One point per cell in shape of dot (`•`)
|
||||
#[default]
|
||||
@@ -26,33 +25,6 @@ pub enum Marker {
|
||||
/// 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,
|
||||
/// Use quadrant characters to represent data points.
|
||||
///
|
||||
/// Quadrant characters display densely packed and regularly spaced pseudo-pixels with a 2x2
|
||||
/// resolution per character, without visible bands between cells.
|
||||
Quadrant,
|
||||
/// Use sextant characters from the [Unicode Symbols for Legacy Computing
|
||||
/// Supplement](https://en.wikipedia.org/wiki/Symbols_for_Legacy_Computing_Supplement) to
|
||||
/// represent data points.
|
||||
///
|
||||
/// Sextant characters display densely packed and regularly spaced pseudo-pixels with a 2x3
|
||||
/// resolution per character, without visible bands between cells.
|
||||
///
|
||||
/// Note: the Symbols for Legacy Computing Supplement block is a relatively recent addition to
|
||||
/// unicode that is less broadly supported than Braille dots. If your terminal does not support
|
||||
/// this, you will see unicode replacement characters (`<60>`) instead of sextants (`🬌`, `🬲`, `🬑`).
|
||||
Sextant,
|
||||
/// Use octant characters from the [Unicode Symbols for Legacy Computing
|
||||
/// Supplement](https://en.wikipedia.org/wiki/Symbols_for_Legacy_Computing_Supplement) to
|
||||
/// represent data points.
|
||||
///
|
||||
/// Octant characters have the same 2x4 resolution as Braille characters but display densely
|
||||
/// packed and regularly spaced pseudo-pixels, without visible bands between cells.
|
||||
///
|
||||
/// Note: the Symbols for Legacy Computing Supplement block is a relatively recent addition to
|
||||
/// unicode that is less broadly supported than Braille dots. If your terminal does not support
|
||||
/// this, you will see unicode replacement characters (`<60>`) instead of octants (``, ``, ``).
|
||||
Octant,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -99,7 +99,7 @@ pub enum MergeStrategy {
|
||||
///
|
||||
/// The following diagram illustrates how this would apply to several overlapping blocks where
|
||||
/// the thick bordered blocks are rendered last, merging the previous symbols into a single
|
||||
/// composite character. All combinations of the plain and thick segments exist, so these
|
||||
/// composite character. All combindations of the plain and thick segments exist, so these
|
||||
/// symbols can be merged into a single character:
|
||||
///
|
||||
/// ```text
|
||||
@@ -264,7 +264,7 @@ pub enum MergeStrategy {
|
||||
/// assert_eq!(strategy.merge("┃", "═"), "╬");
|
||||
/// assert_eq!(strategy.merge("═", "┃"), "╋");
|
||||
///
|
||||
/// // combinations of double with plain that don't exist are merged based on the second symbol
|
||||
/// // combindations of double with plain that don't exist are merged based on the second symbol
|
||||
/// assert_eq!(strategy.merge("┐", "╔"), "╦");
|
||||
/// assert_eq!(strategy.merge("╔", "┐"), "┬");
|
||||
/// ```
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
//! Pseudo-pixel symbols: quadrant, sextant and octant characters.
|
||||
//!
|
||||
//! Note that the symbols are not listed according to their unicode codepoint but according to the
|
||||
//! corresponding bit pattern in row-major order.
|
||||
|
||||
pub const QUADRANTS: [char; 16] = [
|
||||
' ', '▘', '▝', '▀', '▖', '▌', '▞', '▛', '▗', '▚', '▐', '▜', '▄', '▙', '▟', '█',
|
||||
];
|
||||
pub const SEXTANTS: [char; 64] = [
|
||||
' ', '🬀', '🬁', '🬂', '🬃', '🬄', '🬅', '🬆', '🬇', '🬈', '🬉', '🬊', '🬋', '🬌', '🬍', '🬎', '🬏', '🬐', '🬑',
|
||||
'🬒', '🬓', '▌', '🬔', '🬕', '🬖', '🬗', '🬘', '🬙', '🬚', '🬛', '🬜', '🬝', '🬞', '🬟', '🬠', '🬡', '🬢', '🬣',
|
||||
'🬤', '🬥', '🬦', '🬧', '▐', '🬨', '🬩', '🬪', '🬫', '🬬', '🬭', '🬮', '🬯', '🬰', '🬱', '🬲', '🬳', '🬴', '🬵',
|
||||
'🬶', '🬷', '🬸', '🬹', '🬺', '🬻', '█',
|
||||
];
|
||||
pub const OCTANTS: [char; 256] = [
|
||||
' ', '', '', '🮂', '', '▘', '', '', '', '', '▝', '', '', '', '', '▀', '', '', '',
|
||||
'', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
|
||||
'', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
|
||||
'', '', '', '', '', '', '🮅', '', '', '', '', '', '', '', '', '', '', '', '',
|
||||
'', '', '', '', '▖', '', '', '', '', '▌', '', '', '', '', '▞', '', '', '', '',
|
||||
'▛', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
|
||||
'', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
|
||||
'', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
|
||||
'', '', '', '', '', '', '', '', '▗', '', '', '', '', '▚', '', '', '', '', '▐',
|
||||
'', '', '', '', '▜', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
|
||||
'', '', '▂', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
|
||||
'', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
|
||||
'', '', '', '', '', '', '', '', '', '', '', '', '▄', '', '', '', '', '▙', '',
|
||||
'', '', '', '▟', '', '▆', '', '', '█',
|
||||
];
|
||||
@@ -31,381 +31,10 @@
|
||||
//! [`Backend`]: crate::backend::Backend
|
||||
//! [`Buffer`]: crate::buffer::Buffer
|
||||
|
||||
mod backend;
|
||||
mod buffers;
|
||||
mod cursor;
|
||||
mod frame;
|
||||
mod init;
|
||||
mod inline;
|
||||
mod render;
|
||||
mod resize;
|
||||
mod terminal;
|
||||
mod viewport;
|
||||
|
||||
pub use frame::{CompletedFrame, Frame};
|
||||
pub use terminal::{Options as TerminalOptions, Terminal};
|
||||
pub use viewport::Viewport;
|
||||
|
||||
use crate::backend::Backend;
|
||||
use crate::buffer::Buffer;
|
||||
use crate::layout::{Position, Rect};
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// If you're building a fullscreen application with the `ratatui` crate's default backend
|
||||
/// ([Crossterm]), prefer [`ratatui::run`] (or [`ratatui::init`] + [`ratatui::restore`]) over
|
||||
/// constructing `Terminal` directly. These helpers enable common terminal modes (raw mode +
|
||||
/// alternate screen) and restore them on exit and on panic.
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// ratatui::run(|terminal| {
|
||||
/// let mut should_quit = false;
|
||||
/// while !should_quit {
|
||||
/// terminal.draw(|frame| {
|
||||
/// frame.render_widget("Hello, World!", frame.area());
|
||||
/// })?;
|
||||
///
|
||||
/// // Handle events, update application state, and set `should_quit = true` to exit.
|
||||
/// }
|
||||
/// Ok(())
|
||||
/// })?;
|
||||
/// ```
|
||||
///
|
||||
/// # Typical Usage
|
||||
///
|
||||
/// In a typical application, the flow is: set up a terminal, run an event loop, update state, and
|
||||
/// draw each frame.
|
||||
///
|
||||
/// 1. Choose a setup path for a `Terminal`. Most apps call [`ratatui::run`], which passes a
|
||||
/// preconfigured `Terminal` into your callback. If you need more control, use [`ratatui::init`]
|
||||
/// and [`ratatui::restore`], or construct a `Terminal` manually via [`Terminal::new`]
|
||||
/// (fullscreen) or [`Terminal::with_options`] (select a [`Viewport`]).
|
||||
/// 2. Enter your application's event loop and call [`Terminal::draw`] (or [`Terminal::try_draw`])
|
||||
/// to render the current UI state into a [`Frame`].
|
||||
/// 3. Handle input and application state updates between draw calls.
|
||||
/// 4. If the terminal is resized, call [`Terminal::draw`] again. Ratatui automatically resizes
|
||||
/// fullscreen and inline viewports during `draw`; fixed viewports require an explicit call to
|
||||
/// [`Terminal::resize`] if you want the region to change.
|
||||
///
|
||||
/// # Rendering Pipeline
|
||||
///
|
||||
/// A single call to [`Terminal::draw`] (or [`Terminal::try_draw`]) represents one render pass. In
|
||||
/// broad strokes, Ratatui:
|
||||
///
|
||||
/// 1. Checks whether the underlying terminal size changed (see [`Terminal::autoresize`]).
|
||||
/// 2. Creates a [`Frame`] backed by the current buffer (see [`Terminal::get_frame`]).
|
||||
/// 3. Runs your render callback to populate that buffer.
|
||||
/// 4. Diffs the current buffer against the previous buffer and writes the changes (see
|
||||
/// [`Terminal::flush`]).
|
||||
/// 5. Applies cursor visibility and position requested by the frame (see
|
||||
/// [`Frame::set_cursor_position`]).
|
||||
/// 6. Swaps the buffers to prepare for the next render pass (see [`Terminal::swap_buffers`]).
|
||||
/// 7. Flushes the backend (see [`Backend::flush`]).
|
||||
///
|
||||
/// Each render pass starts with an empty buffer for the current viewport. Your render callback
|
||||
/// should render everything that should be visible in [`Frame::area`], even if it is unchanged
|
||||
/// from the previous frame. Ratatui diffs the current and previous buffers and only writes the
|
||||
/// changes; anything you don't render is treated as empty and may clear previously drawn content.
|
||||
///
|
||||
/// If the viewport size changes between render passes (for example via [`Terminal::autoresize`] or
|
||||
/// an explicit [`Terminal::resize`]), Ratatui clears the viewport and resets the previous buffer so
|
||||
/// the next `draw` is treated as a full redraw.
|
||||
///
|
||||
/// Most applications should use [`Terminal::draw`] / [`Terminal::try_draw`]. For manual rendering
|
||||
/// (primarily for tests), you can build a frame with [`Terminal::get_frame`], write diffs with
|
||||
/// [`Terminal::flush`], then call [`Terminal::swap_buffers`]. If your backend buffers output, also
|
||||
/// call [`Backend::flush`].
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # mod ratatui {
|
||||
/// # pub use ratatui_core::backend;
|
||||
/// # pub use ratatui_core::terminal::Terminal;
|
||||
/// # }
|
||||
/// use ratatui::Terminal;
|
||||
/// use ratatui::backend::{Backend, TestBackend};
|
||||
///
|
||||
/// let backend = TestBackend::new(10, 10);
|
||||
/// let mut terminal = Terminal::new(backend)?;
|
||||
///
|
||||
/// // Manual render pass (roughly what `Terminal::draw` does internally).
|
||||
/// {
|
||||
/// let mut frame = terminal.get_frame();
|
||||
/// frame.render_widget("Hello World!", frame.area());
|
||||
/// }
|
||||
///
|
||||
/// terminal.flush()?;
|
||||
/// terminal.swap_buffers();
|
||||
/// terminal.backend_mut().flush()?;
|
||||
/// # Ok::<(), Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
///
|
||||
/// # Viewports
|
||||
///
|
||||
/// The viewport controls *where* Ratatui draws and therefore what [`Frame::area`] represents.
|
||||
/// Most applications use [`Viewport::Fullscreen`], but Ratatui also supports [`Viewport::Inline`]
|
||||
/// and [`Viewport::Fixed`].
|
||||
///
|
||||
/// Choose a viewport at initialization time with [`Terminal::with_options`] and
|
||||
/// [`TerminalOptions`].
|
||||
///
|
||||
/// In [`Viewport::Fullscreen`], the viewport is the entire terminal and `Frame::area` starts at
|
||||
/// (0, 0). Ratatui automatically resizes the internal buffers when the terminal size changes.
|
||||
///
|
||||
/// In [`Viewport::Fixed`], the viewport is a user-provided [`Rect`] in terminal coordinates.
|
||||
/// `Frame::area` is that exact rectangle (including its `x`/`y` offset). Fixed viewports are not
|
||||
/// automatically resized; if the region should change, call [`Terminal::resize`].
|
||||
///
|
||||
/// In [`Viewport::Inline`], Ratatui draws into a rectangle anchored to where the UI started. This
|
||||
/// mode is described in more detail in the "Inline Viewport" section below.
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// use ratatui::{layout::Rect, Terminal, TerminalOptions, Viewport};
|
||||
/// use ratatui::backend::CrosstermBackend;
|
||||
///
|
||||
/// // Fullscreen (most common):
|
||||
/// let fullscreen = Terminal::new(CrosstermBackend::new(std::io::stdout()))?;
|
||||
///
|
||||
/// // Fixed region (your app manages the coordinates):
|
||||
/// let viewport = Viewport::Fixed(Rect::new(0, 0, 30, 10));
|
||||
/// let fixed = Terminal::with_options(
|
||||
/// CrosstermBackend::new(std::io::stdout()),
|
||||
/// TerminalOptions { viewport },
|
||||
/// )?;
|
||||
/// ```
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// # Inline Viewport
|
||||
///
|
||||
/// Inline mode is designed for applications that want to embed a UI into a larger CLI flow. In
|
||||
/// [`Viewport::Inline`], Ratatui anchors the viewport to the backend cursor row at initialization
|
||||
/// time and always starts drawing at column 0.
|
||||
///
|
||||
/// To reserve vertical space for the requested height, Ratatui may append lines. When the cursor is
|
||||
/// near the bottom edge, terminals scroll; Ratatui accounts for that scrolling by shifting the
|
||||
/// computed viewport origin upward so the viewport stays fully visible.
|
||||
///
|
||||
/// While running in inline mode, [`Terminal::insert_before`] can be used to print output above the
|
||||
/// viewport without disturbing the UI.
|
||||
/// When Ratatui is built with the `scrolling-regions` feature, `insert_before` can do this without
|
||||
/// clearing and redrawing the viewport.
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// use ratatui::{TerminalOptions, Viewport};
|
||||
///
|
||||
/// println!("Some output above the UI");
|
||||
///
|
||||
/// let options = TerminalOptions {
|
||||
/// viewport: Viewport::Inline(10),
|
||||
/// };
|
||||
/// let mut terminal = ratatui::try_init_with_options(options)?;
|
||||
///
|
||||
/// terminal.insert_before(1, |buf| {
|
||||
/// // Render a single line of output into `buf` before the UI.
|
||||
/// // (For example: logs, status updates, or command output.)
|
||||
/// })?;
|
||||
/// ```
|
||||
///
|
||||
/// # More Information
|
||||
///
|
||||
/// - Choosing a viewport: [`Terminal::with_options`], [`TerminalOptions`], and [`Viewport`]
|
||||
/// - The rendering pipeline: [`Terminal::draw`] and [`Terminal::try_draw`]
|
||||
/// - Resize handling: [`Terminal::autoresize`] and [`Terminal::resize`]
|
||||
/// - Manual rendering and testing: [`Terminal::get_frame`], [`Terminal::flush`], and
|
||||
/// [`Terminal::swap_buffers`]
|
||||
/// - Printing above an inline UI: [`Terminal::insert_before`]
|
||||
///
|
||||
/// # Initialization
|
||||
///
|
||||
/// Most interactive TUIs need process-wide terminal setup (for example: raw mode and an alternate
|
||||
/// screen) and matching teardown on exit and on panic. In Ratatui, that setup lives in the
|
||||
/// `ratatui` crate; `Terminal` itself focuses on rendering and does not implicitly change those
|
||||
/// modes.
|
||||
///
|
||||
/// If you're using the `ratatui` crate with its default backend ([Crossterm]), there are three
|
||||
/// common entry points:
|
||||
///
|
||||
/// - [`ratatui::run`]: recommended for most applications. Provides a [`ratatui::DefaultTerminal`],
|
||||
/// runs your closure, and restores terminal state on exit and on panic.
|
||||
/// - [`ratatui::init`] + [`ratatui::restore`]: like `run`, but you control the event loop and
|
||||
/// decide when to restore.
|
||||
/// - [`Terminal::new`] / [`Terminal::with_options`]: manual construction (for example: custom
|
||||
/// backends such as [Termion] / [Termwiz], inline UIs, or fixed viewports). You are responsible
|
||||
/// for terminal mode setup and teardown.
|
||||
///
|
||||
/// [`ratatui::run`] was introduced in Ratatui 0.30, so older tutorials may use `init`/`restore` or
|
||||
/// manual construction.
|
||||
///
|
||||
/// Some applications install a custom panic hook to log a crash report, print a friendlier error,
|
||||
/// or integrate with error reporting. If you do, install it before calling [`ratatui::init`] /
|
||||
/// [`ratatui::run`]. Ratatui wraps the current hook so it can restore terminal state first (for
|
||||
/// example: leaving the alternate screen and disabling raw mode) and then delegate to your hook.
|
||||
///
|
||||
/// Crossterm is cross-platform and is what most Ratatui applications use by default. Ratatui also
|
||||
/// supports other backends such as [Termion] and [Termwiz], and third-party backends can integrate
|
||||
/// by implementing [`Backend`].
|
||||
///
|
||||
/// # How it works
|
||||
///
|
||||
/// `Terminal` ties together a [`Backend`], a [`Viewport`], and a double-buffered diffing renderer.
|
||||
/// The high-level flow is described in the "Rendering Pipeline" section above; this section focuses
|
||||
/// on how that pipeline is implemented.
|
||||
///
|
||||
/// `Terminal` is generic over a [`Backend`] implementation and does not depend on a particular
|
||||
/// terminal library. It relies on the backend to:
|
||||
///
|
||||
/// - report the current screen size (used by [`Terminal::autoresize`])
|
||||
/// - draw cell updates (used by [`Terminal::flush`])
|
||||
/// - clear regions (used by [`Terminal::clear`] and [`Terminal::resize`])
|
||||
/// - move and show/hide the cursor (used by [`Terminal::try_draw`])
|
||||
/// - optionally append lines (used by inline viewports and by [`Terminal::insert_before`])
|
||||
///
|
||||
/// ## Buffers and diffing
|
||||
///
|
||||
/// The `Terminal` maintains two [`Buffer`]s sized to the current viewport. During a render pass,
|
||||
/// widgets draw into the "current" buffer via the [`Frame`] passed to your callback. At the end of
|
||||
/// the pass, [`Terminal::flush`] diffs the current buffer against the previous buffer and sends
|
||||
/// only the changed cells to the backend.
|
||||
///
|
||||
/// After flushing, [`Terminal::swap_buffers`] flips which buffer is considered "current" and resets
|
||||
/// the next buffer. This is why each render pass starts from an empty buffer: your callback is
|
||||
/// expected to fully redraw the viewport every time.
|
||||
///
|
||||
/// The [`CompletedFrame`] returned from [`Terminal::draw`] / [`Terminal::try_draw`] provides a
|
||||
/// reference to the buffer that was just rendered, which can be useful for assertions in tests.
|
||||
///
|
||||
/// ## Viewport state and resizing
|
||||
///
|
||||
/// The active [`Viewport`] controls how the viewport area is computed:
|
||||
///
|
||||
/// - Fullscreen: `Frame::area` covers the full backend size.
|
||||
/// - Fixed: `Frame::area` is the exact rectangle you provided in terminal coordinates.
|
||||
/// - Inline: `Frame::area` is a rectangle anchored to the backend cursor row.
|
||||
///
|
||||
/// For fullscreen and inline viewports, [`Terminal::autoresize`] checks the backend size during
|
||||
/// every render pass and calls [`Terminal::resize`] when it changes. Resizing updates the internal
|
||||
/// buffer sizes and clears the affected region; it also resets the previous buffer so the next draw
|
||||
/// is treated as a full redraw.
|
||||
///
|
||||
/// ## Cursor tracking
|
||||
///
|
||||
/// The cursor position requested by [`Frame::set_cursor_position`] is applied after
|
||||
/// [`Terminal::flush`] so the cursor ends up on top of the rendered UI. `Terminal` also tracks a
|
||||
/// "last known cursor position" as a best-effort record of where it last wrote, and uses that
|
||||
/// information when recomputing inline viewports on resize.
|
||||
///
|
||||
/// ## Inline-specific behavior
|
||||
///
|
||||
/// Inline viewports reserve vertical space by calling [`Backend::append_lines`]. If the cursor is
|
||||
/// close enough to the bottom edge, terminals scroll as lines are appended. Ratatui accounts for
|
||||
/// that scrolling by shifting the computed viewport origin upward so the viewport remains fully
|
||||
/// visible. On resize, Ratatui recomputes the inline origin while trying to keep the cursor at the
|
||||
/// same relative row inside the viewport.
|
||||
///
|
||||
/// When Ratatui is built with the `scrolling-regions` feature, [`Terminal::insert_before`] uses
|
||||
/// terminal scrolling regions to insert content above an inline viewport without clearing and
|
||||
/// redrawing it.
|
||||
///
|
||||
/// [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
|
||||
/// [`Backend::flush`]: crate::backend::Backend::flush
|
||||
/// [`Buffer`]: crate::buffer::Buffer
|
||||
/// [`ratatui::DefaultTerminal`]: https://docs.rs/ratatui/latest/ratatui/type.DefaultTerminal.html
|
||||
/// [`ratatui::init`]: https://docs.rs/ratatui/latest/ratatui/fn.init.html
|
||||
/// [`ratatui::restore`]: https://docs.rs/ratatui/latest/ratatui/fn.restore.html
|
||||
/// [`ratatui::run`]: https://docs.rs/ratatui/latest/ratatui/fn.run.html
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Terminal<B>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
/// The backend used to write updates to the terminal.
|
||||
///
|
||||
/// Most application code does not need to interact with the backend directly; see
|
||||
/// [`Terminal::draw`]. Accessing the backend can be useful for backend-specific testing and
|
||||
/// inspection (see [`Terminal::backend`]).
|
||||
backend: B,
|
||||
/// Double-buffered render state.
|
||||
///
|
||||
/// [`Terminal::flush`] diffs `buffers[current]` against the other buffer to compute a minimal
|
||||
/// set of updates to send to the backend.
|
||||
buffers: [Buffer; 2],
|
||||
/// Index of the "current" buffer in [`Terminal::buffers`].
|
||||
///
|
||||
/// This toggles between 0 and 1 and is updated by [`Terminal::swap_buffers`].
|
||||
current: usize,
|
||||
/// Whether Ratatui believes it has hidden the cursor.
|
||||
///
|
||||
/// This is tracked so [`Drop`] can attempt to restore cursor visibility.
|
||||
hidden_cursor: bool,
|
||||
/// The configured [`Viewport`] mode.
|
||||
///
|
||||
/// This determines how the initial viewport area is computed during construction, whether
|
||||
/// [`Terminal::autoresize`] runs, how [`Terminal::clear`] behaves, and whether operations like
|
||||
/// [`Terminal::insert_before`] have any effect.
|
||||
viewport: Viewport,
|
||||
/// The current viewport rectangle in terminal coordinates.
|
||||
///
|
||||
/// This is the area returned by [`Frame::area`] and the size of the internal buffers. It is
|
||||
/// set during construction and updated by [`Terminal::resize`]. In inline mode, calls to
|
||||
/// [`Terminal::insert_before`] can also move the viewport vertically.
|
||||
viewport_area: Rect,
|
||||
/// Last known renderable "screen" area.
|
||||
///
|
||||
/// For fullscreen and inline viewports this tracks the backend-reported terminal size. For
|
||||
/// fixed viewports, this tracks the user-provided fixed area.
|
||||
///
|
||||
/// This is used by [`Terminal::autoresize`] and is reported via [`CompletedFrame::area`].
|
||||
last_known_area: Rect,
|
||||
/// Last known cursor position in terminal coordinates.
|
||||
///
|
||||
/// This is updated when:
|
||||
///
|
||||
/// - [`Terminal::set_cursor_position`] is called directly.
|
||||
/// - [`Frame::set_cursor_position`] is used during [`Terminal::draw`].
|
||||
/// - [`Terminal::flush`] observes a diff update (used as a proxy for the "last written" cell).
|
||||
///
|
||||
/// Inline viewports use this during [`Terminal::resize`] to preserve the cursor's relative
|
||||
/// position within the viewport.
|
||||
last_known_cursor_pos: Position,
|
||||
/// Number of frames rendered so far.
|
||||
///
|
||||
/// This increments after each successful [`Terminal::draw`] / [`Terminal::try_draw`] and wraps
|
||||
/// at `usize::MAX`.
|
||||
frame_count: usize,
|
||||
}
|
||||
|
||||
/// Options to pass to [`Terminal::with_options`]
|
||||
///
|
||||
/// Most applications can use [`Terminal::new`]. Use `TerminalOptions` when you need to configure a
|
||||
/// non-default [`Viewport`] at initialization time (see [`Terminal`] for an overview).
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct TerminalOptions {
|
||||
/// Viewport used to draw to the terminal.
|
||||
///
|
||||
/// See [`Terminal`] for a higher-level overview, and [`Viewport`] for the per-variant
|
||||
/// definition.
|
||||
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 {
|
||||
#[allow(unused_variables)]
|
||||
if let Err(err) = self.show_cursor() {
|
||||
#[cfg(feature = "std")]
|
||||
std::eprintln!("Failed to show the cursor: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
use crate::backend::Backend;
|
||||
use crate::layout::Size;
|
||||
use crate::terminal::Terminal;
|
||||
|
||||
impl<B: Backend> Terminal<B> {
|
||||
/// Returns a shared reference to the backend.
|
||||
///
|
||||
/// This is primarily useful for backend-specific inspection in tests (e.g. reading
|
||||
/// [`TestBackend`]'s buffer). Most applications should interact with the terminal via
|
||||
/// [`Terminal::draw`] rather than calling backend methods directly.
|
||||
///
|
||||
/// [`TestBackend`]: crate::backend::TestBackend
|
||||
pub const fn backend(&self) -> &B {
|
||||
&self.backend
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the backend.
|
||||
///
|
||||
/// This is an advanced escape hatch. Mutating the backend directly can desynchronize Ratatui's
|
||||
/// internal buffers from what's on-screen; if you do this, you may need to call
|
||||
/// [`Terminal::clear`] to force a full redraw.
|
||||
pub const fn backend_mut(&mut self) -> &mut B {
|
||||
&mut self.backend
|
||||
}
|
||||
|
||||
/// Queries the real size of the backend.
|
||||
///
|
||||
/// This returns the size of the underlying terminal. The current renderable area depends on
|
||||
/// the configured [`Viewport`]; use [`Frame::area`] inside [`Terminal::draw`] if you want the
|
||||
/// area you should render into.
|
||||
///
|
||||
/// [`Frame::area`]: crate::terminal::Frame::area
|
||||
/// [`Terminal::draw`]: crate::terminal::Terminal::draw
|
||||
/// [`Viewport`]: crate::terminal::Viewport
|
||||
pub fn size(&self) -> Result<Size, B::Error> {
|
||||
self.backend.size()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::backend::TestBackend;
|
||||
use crate::layout::{Position, Size};
|
||||
use crate::terminal::Terminal;
|
||||
|
||||
#[test]
|
||||
fn backend_returns_shared_reference() {
|
||||
let backend = TestBackend::new(3, 2);
|
||||
let terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
assert_eq!(terminal.backend().cursor_position(), Position::ORIGIN);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backend_mut_allows_mutating_backend_state() {
|
||||
let backend = TestBackend::new(3, 2);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
terminal.backend_mut().resize(4, 3);
|
||||
|
||||
assert_eq!(terminal.size().unwrap(), Size::new(4, 3));
|
||||
terminal
|
||||
.backend()
|
||||
.assert_buffer_lines([" ", " ", " "]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn size_queries_underlying_backend_size() {
|
||||
let mut backend = TestBackend::new(3, 2);
|
||||
backend.resize(4, 3);
|
||||
let terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
assert_eq!(terminal.size().unwrap(), Size::new(4, 3));
|
||||
}
|
||||
}
|
||||
@@ -1,380 +0,0 @@
|
||||
use crate::backend::{Backend, ClearType};
|
||||
use crate::buffer::{Buffer, Cell};
|
||||
use crate::layout::{Position, Rect};
|
||||
use crate::terminal::{Frame, Terminal, Viewport};
|
||||
|
||||
impl<B: Backend> Terminal<B> {
|
||||
/// Returns a [`Frame`] for manual rendering.
|
||||
///
|
||||
/// Most applications should render via [`Terminal::draw`] / [`Terminal::try_draw`]. This method
|
||||
/// exposes the frame construction step used by [`Terminal::try_draw`] so tests and advanced
|
||||
/// callers can render without running the full draw pipeline.
|
||||
///
|
||||
/// Unlike `draw` / `try_draw`, this does not call [`Terminal::autoresize`], does not write
|
||||
/// updates to the backend, and does not apply any cursor changes. After rendering, you
|
||||
/// typically call [`Terminal::flush`], [`Terminal::swap_buffers`], and [`Backend::flush`].
|
||||
///
|
||||
/// The returned `Frame` mutably borrows the current buffer, so it must be dropped before you
|
||||
/// can call methods like [`Terminal::flush`]. The example below uses a scope to make that
|
||||
/// explicit.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # mod ratatui {
|
||||
/// # pub use ratatui_core::backend;
|
||||
/// # pub use ratatui_core::terminal::Terminal;
|
||||
/// # }
|
||||
/// use ratatui::Terminal;
|
||||
/// use ratatui::backend::{Backend, TestBackend};
|
||||
///
|
||||
/// let backend = TestBackend::new(30, 5);
|
||||
/// let mut terminal = Terminal::new(backend)?;
|
||||
/// {
|
||||
/// let mut frame = terminal.get_frame();
|
||||
/// frame.render_widget("Hello", frame.area());
|
||||
/// }
|
||||
/// // When not using `draw`, present the buffer manually:
|
||||
/// terminal.flush()?;
|
||||
/// terminal.swap_buffers();
|
||||
/// terminal.backend_mut().flush()?;
|
||||
/// # Ok::<(), Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
///
|
||||
/// [`Backend::flush`]: crate::backend::Backend::flush
|
||||
pub const 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.
|
||||
///
|
||||
/// This is the buffer that the next [`Frame`] will render into (see [`Terminal::get_frame`]).
|
||||
/// Most applications should render inside [`Terminal::draw`] and access the buffer via
|
||||
/// [`Frame::buffer_mut`] instead.
|
||||
pub const fn current_buffer_mut(&mut self) -> &mut Buffer {
|
||||
&mut self.buffers[self.current]
|
||||
}
|
||||
|
||||
/// Writes the current buffer to the backend using a diff against the previous buffer.
|
||||
///
|
||||
/// This is one of the building blocks used by [`Terminal::draw`] / [`Terminal::try_draw`]. It
|
||||
/// does not swap buffers or flush the backend; see [`Terminal::swap_buffers`] and
|
||||
/// [`Backend::flush`].
|
||||
///
|
||||
/// Implementation note: when there are updates, Ratatui records the position of the last
|
||||
/// updated cell as the "last known cursor position". Inline viewports use this to preserve the
|
||||
/// cursor's relative position within the viewport across resizes.
|
||||
///
|
||||
/// [`Backend::flush`]: crate::backend::Backend::flush
|
||||
pub fn flush(&mut self) -> Result<(), B::Error> {
|
||||
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())
|
||||
}
|
||||
|
||||
/// Clears the inactive buffer and swaps it with the current buffer.
|
||||
///
|
||||
/// This is part of the standard rendering flow (see [`Terminal::try_draw`]). If you render
|
||||
/// manually using [`Terminal::get_frame`] and [`Terminal::flush`], call this afterward so the
|
||||
/// next flush can compute diffs against the correct "previous" buffer.
|
||||
pub fn swap_buffers(&mut self) {
|
||||
self.buffers[1 - self.current].reset();
|
||||
self.current = 1 - self.current;
|
||||
}
|
||||
|
||||
/// Clear the terminal and force a full redraw on the next draw call.
|
||||
///
|
||||
/// What gets cleared depends on the active [`Viewport`]:
|
||||
///
|
||||
/// - [`Viewport::Fullscreen`]: clears the entire terminal.
|
||||
/// - [`Viewport::Fixed`]: clears only the viewport region.
|
||||
/// - [`Viewport::Inline`]: clears after the viewport's origin, leaving any content above the
|
||||
/// viewport untouched.
|
||||
///
|
||||
/// Current behavior: for [`Viewport::Inline`], clearing runs from the viewport origin through
|
||||
/// the end of the visible display area, not just the viewport's rectangle. This is an
|
||||
/// implementation detail rather than a contract; do not rely on it.
|
||||
///
|
||||
/// This preserves the cursor position.
|
||||
///
|
||||
/// This also resets the "previous" buffer so the next [`Terminal::flush`] redraws the full
|
||||
/// viewport. [`Terminal::resize`] calls this internally.
|
||||
///
|
||||
/// Implementation note: this uses [`ClearType::AfterCursor`] starting at the viewport origin.
|
||||
pub fn clear(&mut self) -> Result<(), B::Error> {
|
||||
let original_cursor = self.backend.get_cursor_position()?;
|
||||
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(_) => {
|
||||
let area = self.viewport_area;
|
||||
self.clear_fixed_viewport(area)?;
|
||||
}
|
||||
}
|
||||
self.backend.set_cursor_position(original_cursor)?;
|
||||
// Reset the back buffer to make sure the next update will redraw everything.
|
||||
self.buffers[1 - self.current].reset();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Clears a fixed viewport using terminal clear commands when possible.
|
||||
///
|
||||
/// Terminal clear commands can be faster than per-cell updates.
|
||||
fn clear_fixed_viewport(&mut self, area: Rect) -> Result<(), B::Error> {
|
||||
if area.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let size = self.backend.size()?;
|
||||
let is_full_width = area.x == 0 && area.width == size.width;
|
||||
let ends_at_bottom = area.bottom() == size.height;
|
||||
if is_full_width && ends_at_bottom {
|
||||
self.backend.set_cursor_position(area.as_position())?;
|
||||
self.backend.clear_region(ClearType::AfterCursor)?;
|
||||
} else if is_full_width {
|
||||
self.clear_full_width_rows(area)?;
|
||||
} else {
|
||||
self.clear_region_cells(area)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Clears full-width rows using line clear commands.
|
||||
///
|
||||
/// This avoids per-cell writes when the viewport spans the full width.
|
||||
fn clear_full_width_rows(&mut self, area: Rect) -> Result<(), B::Error> {
|
||||
for y in area.top()..area.bottom() {
|
||||
self.backend.set_cursor_position(Position { x: 0, y })?;
|
||||
self.backend.clear_region(ClearType::CurrentLine)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Clears a non-full-width region by writing empty cells directly.
|
||||
///
|
||||
/// This is used when line-based clears would affect cells outside the viewport.
|
||||
fn clear_region_cells(&mut self, area: Rect) -> Result<(), B::Error> {
|
||||
let clear_cell = Cell::default();
|
||||
let updates = area.positions().map(|pos| (pos.x, pos.y, &clear_cell));
|
||||
self.backend.draw(updates)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::backend::{Backend, TestBackend};
|
||||
use crate::buffer::{Buffer, Cell};
|
||||
use crate::layout::{Position, Rect};
|
||||
use crate::terminal::{Terminal, TerminalOptions, Viewport};
|
||||
|
||||
#[test]
|
||||
fn get_frame_uses_current_viewport_and_frame_count() {
|
||||
let backend = TestBackend::new(5, 3);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
let frame = terminal.get_frame();
|
||||
assert_eq!(frame.count, 0);
|
||||
assert_eq!(frame.area().width, 5);
|
||||
assert_eq!(frame.area().height, 3);
|
||||
assert_eq!(frame.buffer.area, frame.area());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flush_writes_updates_and_tracks_last_updated_cell() {
|
||||
let backend = TestBackend::new(3, 2);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
{
|
||||
let frame = terminal.get_frame();
|
||||
frame.buffer[(1, 0)].set_symbol("x");
|
||||
}
|
||||
|
||||
terminal.flush().unwrap();
|
||||
terminal.backend().assert_buffer_lines([" x ", " "]);
|
||||
assert_eq!(terminal.last_known_cursor_pos, Position { x: 1, y: 0 });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flush_with_no_updates_does_not_change_last_known_cursor_pos() {
|
||||
let backend = TestBackend::new(3, 2);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
terminal.set_cursor_position((2, 1)).unwrap();
|
||||
|
||||
terminal.flush().unwrap();
|
||||
|
||||
assert_eq!(terminal.last_known_cursor_pos, Position { x: 2, y: 1 });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swap_buffers_resets_new_current_buffer() {
|
||||
let backend = TestBackend::new(3, 2);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
terminal.buffers[1][(0, 0)].set_symbol("x");
|
||||
terminal.swap_buffers();
|
||||
|
||||
assert_eq!(terminal.current, 1);
|
||||
assert_eq!(
|
||||
terminal.buffers[terminal.current],
|
||||
Buffer::empty(terminal.viewport_area)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_fullscreen_clears_backend_and_resets_back_buffer() {
|
||||
let backend = TestBackend::new(3, 2);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
{
|
||||
let frame = terminal.get_frame();
|
||||
frame.buffer[(0, 0)] = Cell::new("x");
|
||||
}
|
||||
terminal.flush().unwrap();
|
||||
terminal.backend().assert_buffer_lines(["x ", " "]);
|
||||
|
||||
terminal.buffers[1][(2, 1)] = Cell::new("y");
|
||||
terminal.clear().unwrap();
|
||||
|
||||
terminal.backend().assert_buffer_lines([" ", " "]);
|
||||
assert_eq!(
|
||||
terminal.buffers[1 - terminal.current],
|
||||
Buffer::empty(terminal.viewport_area)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_inline_clears_after_viewport_origin_and_resets_back_buffer() {
|
||||
// Inline clear is implemented as:
|
||||
// 1) move the backend cursor to the viewport origin
|
||||
// 2) call ClearType::AfterCursor once
|
||||
let mut backend = TestBackend::with_lines([
|
||||
"before 1 ",
|
||||
"before 2 ",
|
||||
"viewport 1",
|
||||
"viewport 2",
|
||||
"after 1 ",
|
||||
"after 2 ",
|
||||
]);
|
||||
backend
|
||||
.set_cursor_position(Position { x: 2, y: 2 })
|
||||
.unwrap();
|
||||
let options = TerminalOptions {
|
||||
viewport: Viewport::Inline(2),
|
||||
};
|
||||
let mut terminal = Terminal::with_options(backend, options).unwrap();
|
||||
terminal
|
||||
.backend_mut()
|
||||
.set_cursor_position(Position { x: 2, y: 2 })
|
||||
.unwrap();
|
||||
|
||||
terminal.buffers[1][(2, 2)] = Cell::new("x");
|
||||
terminal.clear().unwrap();
|
||||
|
||||
// Inline viewport is anchored to the cursor row (y = 2) with height 2. Clear runs from
|
||||
// the viewport origin through the end of the display, including the rows after it.
|
||||
terminal.backend().assert_buffer_lines([
|
||||
"before 1 ",
|
||||
"before 2 ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
]);
|
||||
assert_eq!(
|
||||
terminal.buffers[1 - terminal.current],
|
||||
Buffer::empty(terminal.viewport_area)
|
||||
);
|
||||
assert_eq!(
|
||||
terminal.backend().cursor_position(),
|
||||
Position { x: 2, y: 2 }
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_fixed_clears_viewport_rows_and_resets_back_buffer() {
|
||||
// For full-width fixed viewports that reach the terminal bottom, clear uses
|
||||
// ClearType::AfterCursor starting at the viewport origin.
|
||||
let mut backend = TestBackend::with_lines(["before 1 ", "viewport 1", "viewport 2"]);
|
||||
backend.set_cursor_position((2, 0)).unwrap();
|
||||
let options = TerminalOptions {
|
||||
viewport: Viewport::Fixed(Rect::new(0, 1, 10, 2)),
|
||||
};
|
||||
let mut terminal = Terminal::with_options(backend, options).unwrap();
|
||||
|
||||
terminal.clear().unwrap();
|
||||
|
||||
terminal
|
||||
.backend()
|
||||
.assert_buffer_lines(["before 1 ", " ", " "]);
|
||||
assert_eq!(
|
||||
terminal.buffers[1 - terminal.current],
|
||||
Buffer::empty(terminal.viewport_area)
|
||||
);
|
||||
assert_eq!(
|
||||
terminal.backend().cursor_position(),
|
||||
Position { x: 2, y: 0 }
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_fixed_full_width_not_at_bottom() {
|
||||
let mut backend =
|
||||
TestBackend::with_lines(["before 1 ", "viewport 1", "viewport 2", "after 1 "]);
|
||||
backend.set_cursor_position((1, 0)).unwrap();
|
||||
let options = TerminalOptions {
|
||||
viewport: Viewport::Fixed(Rect::new(0, 1, 10, 2)),
|
||||
};
|
||||
let mut terminal = Terminal::with_options(backend, options).unwrap();
|
||||
|
||||
terminal.clear().unwrap();
|
||||
|
||||
terminal.backend().assert_buffer_lines([
|
||||
"before 1 ",
|
||||
" ",
|
||||
" ",
|
||||
"after 1 ",
|
||||
]);
|
||||
assert_eq!(
|
||||
terminal.backend().cursor_position(),
|
||||
Position { x: 1, y: 0 }
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_fixed_respects_non_full_width_viewport() {
|
||||
let mut backend =
|
||||
TestBackend::with_lines(["before 1 ", "viewport 1", "viewport 2", "after 1 "]);
|
||||
backend.set_cursor_position((3, 0)).unwrap();
|
||||
let options = TerminalOptions {
|
||||
viewport: Viewport::Fixed(Rect::new(1, 1, 3, 2)),
|
||||
};
|
||||
let mut terminal = Terminal::with_options(backend, options).unwrap();
|
||||
|
||||
terminal.clear().unwrap();
|
||||
|
||||
terminal.backend().assert_buffer_lines([
|
||||
"before 1 ",
|
||||
"v port 1",
|
||||
"v port 2",
|
||||
"after 1 ",
|
||||
]);
|
||||
assert_eq!(
|
||||
terminal.backend().cursor_position(),
|
||||
Position { x: 3, y: 0 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
use crate::backend::Backend;
|
||||
use crate::layout::Position;
|
||||
use crate::terminal::Terminal;
|
||||
|
||||
impl<B: Backend> Terminal<B> {
|
||||
/// Hides the cursor.
|
||||
///
|
||||
/// When using [`Terminal::draw`], prefer controlling the cursor with
|
||||
/// [`Frame::set_cursor_position`]. Mixing the APIs can lead to surprising results.
|
||||
///
|
||||
/// [`Frame::set_cursor_position`]: crate::terminal::Frame::set_cursor_position
|
||||
/// [`Terminal::draw`]: crate::terminal::Terminal::draw
|
||||
pub fn hide_cursor(&mut self) -> Result<(), B::Error> {
|
||||
self.backend.hide_cursor()?;
|
||||
self.hidden_cursor = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Shows the cursor.
|
||||
///
|
||||
/// When using [`Terminal::draw`], prefer controlling the cursor with
|
||||
/// [`Frame::set_cursor_position`]. Mixing the APIs can lead to surprising results.
|
||||
///
|
||||
/// [`Frame::set_cursor_position`]: crate::terminal::Frame::set_cursor_position
|
||||
/// [`Terminal::draw`]: crate::terminal::Terminal::draw
|
||||
pub fn show_cursor(&mut self) -> Result<(), B::Error> {
|
||||
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 = "use `get_cursor_position()` instead which returns `Result<Position>`"]
|
||||
pub fn get_cursor(&mut self) -> Result<(u16, u16), B::Error> {
|
||||
let Position { x, y } = self.get_cursor_position()?;
|
||||
Ok((x, y))
|
||||
}
|
||||
|
||||
/// Sets the cursor position.
|
||||
#[deprecated = "use `set_cursor_position((x, y))` instead which takes `impl Into<Position>`"]
|
||||
pub fn set_cursor(&mut self, x: u16, y: u16) -> Result<(), B::Error> {
|
||||
self.set_cursor_position(Position { x, y })
|
||||
}
|
||||
|
||||
/// Gets the current cursor position.
|
||||
///
|
||||
/// This queries the backend for the current cursor position.
|
||||
///
|
||||
/// When using [`Terminal::draw`], prefer controlling the cursor with
|
||||
/// [`Frame::set_cursor_position`]. For direct control, see [`Terminal::set_cursor_position`].
|
||||
///
|
||||
/// [`Frame::set_cursor_position`]: crate::terminal::Frame::set_cursor_position
|
||||
/// [`Terminal::draw`]: crate::terminal::Terminal::draw
|
||||
pub fn get_cursor_position(&mut self) -> Result<Position, B::Error> {
|
||||
self.backend.get_cursor_position()
|
||||
}
|
||||
|
||||
/// Sets the cursor position.
|
||||
///
|
||||
/// This updates the backend cursor and Ratatui's internal cursor tracking. Inline viewports
|
||||
/// use that tracking when recomputing the viewport on resize.
|
||||
///
|
||||
/// When using [`Terminal::draw`], consider using [`Frame::set_cursor_position`] instead so the
|
||||
/// cursor is updated as part of the normal rendering flow.
|
||||
///
|
||||
/// [`Frame::set_cursor_position`]: crate::terminal::Frame::set_cursor_position
|
||||
/// [`Terminal::draw`]: crate::terminal::Terminal::draw
|
||||
pub fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> Result<(), B::Error> {
|
||||
let position = position.into();
|
||||
self.backend.set_cursor_position(position)?;
|
||||
self.last_known_cursor_pos = position;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::backend::{Backend, TestBackend};
|
||||
use crate::layout::Position;
|
||||
use crate::terminal::Terminal;
|
||||
|
||||
#[test]
|
||||
fn hide_cursor_updates_terminal_state() {
|
||||
let backend = TestBackend::new(10, 5);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
terminal.hide_cursor().unwrap();
|
||||
|
||||
assert!(terminal.hidden_cursor);
|
||||
assert!(!terminal.backend().cursor_visible());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn show_cursor_updates_terminal_state() {
|
||||
let backend = TestBackend::new(10, 5);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
terminal.hide_cursor().unwrap();
|
||||
terminal.show_cursor().unwrap();
|
||||
|
||||
assert!(!terminal.hidden_cursor);
|
||||
assert!(terminal.backend().cursor_visible());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_cursor_position_updates_backend_and_tracking() {
|
||||
let backend = TestBackend::new(10, 5);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
terminal.set_cursor_position((3, 4)).unwrap();
|
||||
|
||||
assert_eq!(terminal.last_known_cursor_pos, Position { x: 3, y: 4 });
|
||||
terminal
|
||||
.backend_mut()
|
||||
.assert_cursor_position(Position { x: 3, y: 4 });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_cursor_position_queries_backend() {
|
||||
let backend = TestBackend::new(10, 5);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
terminal
|
||||
.backend_mut()
|
||||
.set_cursor_position(Position { x: 7, y: 2 })
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
terminal.get_cursor_position().unwrap(),
|
||||
Position { x: 7, y: 2 }
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(deprecated)]
|
||||
fn deprecated_cursor_wrappers_delegate_to_position_apis() {
|
||||
let backend = TestBackend::new(10, 5);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
terminal.set_cursor(4, 1).unwrap();
|
||||
|
||||
assert_eq!(terminal.get_cursor().unwrap(), (4, 1));
|
||||
assert_eq!(terminal.last_known_cursor_pos, Position { x: 4, y: 1 });
|
||||
terminal
|
||||
.backend_mut()
|
||||
.assert_cursor_position(Position { x: 4, y: 1 });
|
||||
}
|
||||
}
|
||||
@@ -1,236 +0,0 @@
|
||||
use crate::backend::Backend;
|
||||
use crate::buffer::Buffer;
|
||||
use crate::layout::Position;
|
||||
use crate::terminal::inline::compute_inline_size;
|
||||
use crate::terminal::{Terminal, TerminalOptions, Viewport};
|
||||
|
||||
impl<B: Backend> Terminal<B> {
|
||||
/// Creates a new [`Terminal`] with the given [`Backend`] with a full screen viewport.
|
||||
///
|
||||
/// This is a convenience for [`Terminal::with_options`] with [`Viewport::Fullscreen`].
|
||||
///
|
||||
/// After creating a terminal, call [`Terminal::draw`] (or [`Terminal::try_draw`]) in a loop to
|
||||
/// render your UI.
|
||||
///
|
||||
/// Note that unlike [`ratatui::init`], this does not install a panic hook, so it is
|
||||
/// recommended to do that manually when using this function, otherwise any panic messages will
|
||||
/// be printed to the alternate screen and the terminal may be left in an unusable state.
|
||||
///
|
||||
/// See [how to set up panic hooks](https://ratatui.rs/recipes/apps/panic-hooks/) and
|
||||
/// [`better-panic` example](https://ratatui.rs/recipes/apps/better-panic/) for more
|
||||
/// information.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # #![allow(unexpected_cfgs)]
|
||||
/// # #[cfg(feature = "crossterm")]
|
||||
/// # {
|
||||
/// use std::io::stdout;
|
||||
///
|
||||
/// use ratatui::Terminal;
|
||||
/// use ratatui::backend::CrosstermBackend;
|
||||
///
|
||||
/// let backend = CrosstermBackend::new(stdout());
|
||||
/// let _terminal = Terminal::new(backend)?;
|
||||
///
|
||||
/// // Optionally set up a panic hook to restore the terminal on panic.
|
||||
/// let old_hook = std::panic::take_hook();
|
||||
/// std::panic::set_hook(Box::new(move |info| {
|
||||
/// ratatui::restore();
|
||||
/// old_hook(info);
|
||||
/// }));
|
||||
/// # }
|
||||
/// # #[cfg(not(feature = "crossterm"))]
|
||||
/// # {
|
||||
/// # use ratatui_core::{backend::TestBackend, terminal::Terminal};
|
||||
/// # let backend = TestBackend::new(10, 10);
|
||||
/// # let _terminal = Terminal::new(backend)?;
|
||||
/// # }
|
||||
/// # Ok::<(), Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
///
|
||||
/// [`ratatui::init`]: https://docs.rs/ratatui/latest/ratatui/fn.init.html
|
||||
pub fn new(backend: B) -> Result<Self, B::Error> {
|
||||
Self::with_options(
|
||||
backend,
|
||||
TerminalOptions {
|
||||
viewport: Viewport::Fullscreen,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a new [`Terminal`] with the given [`Backend`] and [`TerminalOptions`].
|
||||
///
|
||||
/// The viewport determines what area is exposed to widgets via [`Frame::area`]. See
|
||||
/// [`Viewport`] for an overview of the available modes.
|
||||
///
|
||||
/// [`Frame::area`]: crate::terminal::Frame::area
|
||||
///
|
||||
/// After creating a terminal, call [`Terminal::draw`] (or [`Terminal::try_draw`]) in a loop to
|
||||
/// render your UI.
|
||||
///
|
||||
/// Resize behavior depends on the selected viewport:
|
||||
///
|
||||
/// - [`Viewport::Fullscreen`] and [`Viewport::Inline`] are automatically resized during
|
||||
/// [`Terminal::draw`] (via [`Terminal::autoresize`]).
|
||||
/// - [`Viewport::Fixed`] is not automatically resized; call [`Terminal::resize`] if the region
|
||||
/// should change.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # #![allow(unexpected_cfgs)]
|
||||
/// # #[cfg(feature = "crossterm")]
|
||||
/// # {
|
||||
/// use std::io::stdout;
|
||||
///
|
||||
/// use ratatui::backend::CrosstermBackend;
|
||||
/// use ratatui::layout::Rect;
|
||||
/// use ratatui::{Terminal, TerminalOptions, Viewport};
|
||||
///
|
||||
/// let backend = CrosstermBackend::new(stdout());
|
||||
/// let viewport = Viewport::Fixed(Rect::new(0, 0, 10, 10));
|
||||
/// let _terminal = Terminal::with_options(backend, TerminalOptions { viewport })?;
|
||||
/// # }
|
||||
/// # #[cfg(not(feature = "crossterm"))]
|
||||
/// # {
|
||||
/// # use ratatui_core::{
|
||||
/// # backend::TestBackend,
|
||||
/// # layout::Rect,
|
||||
/// # terminal::{Terminal, TerminalOptions, Viewport},
|
||||
/// # };
|
||||
/// # let backend = TestBackend::new(10, 10);
|
||||
/// # let viewport = Viewport::Fixed(Rect::new(0, 0, 10, 10));
|
||||
/// # let _terminal = Terminal::with_options(backend, TerminalOptions { viewport })?;
|
||||
/// # }
|
||||
/// # Ok::<(), Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
///
|
||||
/// When the viewport is [`Viewport::Inline`], Ratatui anchors the viewport to the current
|
||||
/// cursor row at initialization time (always starting at column 0). Ratatui may scroll the
|
||||
/// terminal to make enough room for the requested height so the viewport stays fully visible.
|
||||
pub fn with_options(mut backend: B, options: TerminalOptions) -> Result<Self, B::Error> {
|
||||
let area = match options.viewport {
|
||||
Viewport::Fullscreen | Viewport::Inline(_) => backend.size()?.into(),
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::backend::{Backend, TestBackend};
|
||||
use crate::layout::{Position, Rect};
|
||||
use crate::terminal::{Terminal, TerminalOptions, Viewport};
|
||||
|
||||
#[test]
|
||||
fn new_fullscreen_initializes_state() {
|
||||
let backend = TestBackend::new(10, 5);
|
||||
let terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
assert_eq!(terminal.viewport, Viewport::Fullscreen);
|
||||
assert_eq!(terminal.viewport_area, Rect::new(0, 0, 10, 5));
|
||||
assert_eq!(terminal.last_known_area, Rect::new(0, 0, 10, 5));
|
||||
assert_eq!(terminal.last_known_cursor_pos, Position::ORIGIN);
|
||||
assert_eq!(terminal.current, 0);
|
||||
assert!(!terminal.hidden_cursor);
|
||||
assert_eq!(terminal.frame_count, 0);
|
||||
assert_eq!(terminal.buffers[0].area, terminal.viewport_area);
|
||||
assert_eq!(terminal.buffers[1].area, terminal.viewport_area);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_options_fixed_uses_fixed_area() {
|
||||
let backend = TestBackend::new(10, 10);
|
||||
let viewport = Viewport::Fixed(Rect::new(2, 3, 5, 4));
|
||||
let terminal = Terminal::with_options(
|
||||
backend,
|
||||
TerminalOptions {
|
||||
viewport: viewport.clone(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(terminal.viewport, viewport);
|
||||
assert_eq!(terminal.viewport_area, Rect::new(2, 3, 5, 4));
|
||||
assert_eq!(terminal.last_known_area, Rect::new(2, 3, 5, 4));
|
||||
assert_eq!(terminal.last_known_cursor_pos, Position { x: 2, y: 3 });
|
||||
assert_eq!(terminal.buffers[0].area, terminal.viewport_area);
|
||||
assert_eq!(terminal.buffers[1].area, terminal.viewport_area);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_options_inline_anchors_to_cursor_when_space_available() {
|
||||
let mut backend = TestBackend::new(10, 10);
|
||||
backend
|
||||
.set_cursor_position(Position { x: 0, y: 3 })
|
||||
.unwrap();
|
||||
|
||||
let terminal = Terminal::with_options(
|
||||
backend,
|
||||
TerminalOptions {
|
||||
viewport: Viewport::Inline(4),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(terminal.viewport_area, Rect::new(0, 3, 10, 4));
|
||||
assert_eq!(terminal.last_known_cursor_pos, Position { x: 0, y: 3 });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_options_inline_shifts_up_when_near_bottom() {
|
||||
let mut backend = TestBackend::new(10, 10);
|
||||
backend
|
||||
.set_cursor_position(Position { x: 0, y: 8 })
|
||||
.unwrap();
|
||||
|
||||
let terminal = Terminal::with_options(
|
||||
backend,
|
||||
TerminalOptions {
|
||||
viewport: Viewport::Inline(4),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(terminal.viewport_area, Rect::new(0, 6, 10, 4));
|
||||
assert_eq!(terminal.last_known_cursor_pos, Position { x: 0, y: 8 });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_options_inline_clamps_height_to_terminal() {
|
||||
let mut backend = TestBackend::new(10, 3);
|
||||
backend
|
||||
.set_cursor_position(Position { x: 0, y: 0 })
|
||||
.unwrap();
|
||||
|
||||
let terminal = Terminal::with_options(
|
||||
backend,
|
||||
TerminalOptions {
|
||||
viewport: Viewport::Inline(10),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(terminal.viewport_area, Rect::new(0, 0, 10, 3));
|
||||
}
|
||||
}
|
||||
@@ -1,927 +0,0 @@
|
||||
use crate::backend::Backend;
|
||||
use crate::buffer::{Buffer, Cell};
|
||||
use crate::layout::{Position, Rect, Size};
|
||||
use crate::terminal::{Terminal, Viewport};
|
||||
|
||||
impl<B: Backend> Terminal<B> {
|
||||
/// Insert some content before the current inline viewport. This has no effect when the
|
||||
/// viewport is not inline.
|
||||
///
|
||||
/// This is intended for inline UIs that want to print output (e.g. logs or status messages)
|
||||
/// above the UI without breaking it. See [`Viewport::Inline`] for how inline viewports are
|
||||
/// anchored.
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// When Ratatui is built with the `scrolling-regions` feature, this can be done without
|
||||
/// clearing and redrawing the viewport. Without `scrolling-regions`, Ratatui falls back to a
|
||||
/// more portable approach and clears the viewport so the next [`Terminal::draw`] repaints it.
|
||||
///
|
||||
/// 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:
|
||||
/// ```text
|
||||
/// +---------------------+
|
||||
/// | pre-existing line 1 |
|
||||
/// | pre-existing line 2 |
|
||||
/// +---------------------+
|
||||
/// | viewport |
|
||||
/// +---------------------+
|
||||
/// | |
|
||||
/// | |
|
||||
/// +---------------------+
|
||||
/// ```
|
||||
///
|
||||
/// After inserting 2 lines:
|
||||
/// ```text
|
||||
/// +---------------------+
|
||||
/// | pre-existing line 1 |
|
||||
/// | pre-existing line 2 |
|
||||
/// | inserted line 1 |
|
||||
/// | inserted line 2 |
|
||||
/// +---------------------+
|
||||
/// | viewport |
|
||||
/// +---------------------+
|
||||
/// +---------------------+
|
||||
/// ```
|
||||
///
|
||||
/// After inserting 2 more lines:
|
||||
/// ```text
|
||||
/// +---------------------+
|
||||
/// | 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,no_run
|
||||
/// # mod ratatui {
|
||||
/// # pub use ratatui_core::backend;
|
||||
/// # pub use ratatui_core::layout;
|
||||
/// # pub use ratatui_core::style;
|
||||
/// # pub use ratatui_core::terminal::{Terminal, TerminalOptions, Viewport};
|
||||
/// # pub use ratatui_core::text;
|
||||
/// # pub use ratatui_core::widgets;
|
||||
/// # }
|
||||
/// use ratatui::backend::{Backend, TestBackend};
|
||||
/// use ratatui::layout::Position;
|
||||
/// use ratatui::style::{Color, Style};
|
||||
/// use ratatui::text::{Line, Span};
|
||||
/// use ratatui::widgets::Widget;
|
||||
/// use ratatui::{Terminal, TerminalOptions, Viewport};
|
||||
///
|
||||
/// let mut backend = TestBackend::new(10, 10);
|
||||
/// // Simulate existing output above the inline UI.
|
||||
/// backend.set_cursor_position(Position::new(0, 3))?;
|
||||
/// let mut terminal = Terminal::with_options(
|
||||
/// backend,
|
||||
/// TerminalOptions {
|
||||
/// viewport: Viewport::Inline(4),
|
||||
/// },
|
||||
/// )?;
|
||||
///
|
||||
/// terminal.insert_before(1, |buf| {
|
||||
/// 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);
|
||||
/// })?;
|
||||
/// # Ok::<(), Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
pub fn insert_before<F>(&mut self, height: u16, draw_fn: F) -> Result<(), B::Error>
|
||||
where
|
||||
F: FnOnce(&mut Buffer),
|
||||
{
|
||||
match self.viewport {
|
||||
#[cfg(feature = "scrolling-regions")]
|
||||
Viewport::Inline(_) => self.insert_before_scrolling_regions(height, draw_fn),
|
||||
#[cfg(not(feature = "scrolling-regions"))]
|
||||
Viewport::Inline(_) => self.insert_before_no_scrolling_regions(height, draw_fn),
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Implement `Self::insert_before` using standard backend capabilities.
|
||||
///
|
||||
/// This is the fallback implementation when the `scrolling-regions` feature is disabled. It
|
||||
/// renders the inserted lines into a temporary [`Buffer`], then draws them directly to the
|
||||
/// backend in chunks, scrolling the terminal as needed.
|
||||
///
|
||||
/// See [`Terminal::insert_before`] for the public API contract.
|
||||
#[cfg(not(feature = "scrolling-regions"))]
|
||||
fn insert_before_no_scrolling_regions(
|
||||
&mut self,
|
||||
height: u16,
|
||||
draw_fn: impl FnOnce(&mut Buffer),
|
||||
) -> Result<(), B::Error> {
|
||||
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(())
|
||||
}
|
||||
|
||||
/// Implement `Self::insert_before` using scrolling regions.
|
||||
///
|
||||
/// If a terminal supports scrolling regions, it means that we can define a subset of rows of
|
||||
/// the screen, and then tell the terminal to scroll up or down just within that region. The
|
||||
/// rows outside of the region are not affected.
|
||||
///
|
||||
/// This function utilizes this feature to avoid having to redraw the viewport. This is done
|
||||
/// either by splitting the screen at the top of the viewport, and then creating a gap by
|
||||
/// either scrolling the viewport down, or scrolling the area above it up. The lines to insert
|
||||
/// are then drawn into the gap created.
|
||||
#[cfg(feature = "scrolling-regions")]
|
||||
fn insert_before_scrolling_regions(
|
||||
&mut self,
|
||||
mut height: u16,
|
||||
draw_fn: impl FnOnce(&mut Buffer),
|
||||
) -> Result<(), B::Error> {
|
||||
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();
|
||||
|
||||
// Handle the special case where the viewport takes up the whole screen.
|
||||
if self.viewport_area.height == self.last_known_area.height {
|
||||
// "Borrow" the top line of the viewport. Draw over it, then immediately scroll it into
|
||||
// scrollback. Do this repeatedly until the whole buffer has been put into scrollback.
|
||||
let mut first = true;
|
||||
while !buffer.is_empty() {
|
||||
buffer = if first {
|
||||
self.draw_lines(0, 1, buffer)?
|
||||
} else {
|
||||
self.draw_lines_over_cleared(0, 1, buffer)?
|
||||
};
|
||||
first = false;
|
||||
self.backend.scroll_region_up(0..1, 1)?;
|
||||
}
|
||||
|
||||
// Redraw the top line of the viewport.
|
||||
let width = self.viewport_area.width as usize;
|
||||
let top_line = self.buffers[1 - self.current].content[0..width].to_vec();
|
||||
self.draw_lines_over_cleared(0, 1, &top_line)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Handle the case where the viewport isn't yet at the bottom of the screen.
|
||||
{
|
||||
let viewport_top = self.viewport_area.top();
|
||||
let viewport_bottom = self.viewport_area.bottom();
|
||||
let screen_bottom = self.last_known_area.bottom();
|
||||
if viewport_bottom < screen_bottom {
|
||||
let to_draw = height.min(screen_bottom - viewport_bottom);
|
||||
self.backend
|
||||
.scroll_region_down(viewport_top..viewport_bottom + to_draw, to_draw)?;
|
||||
buffer = self.draw_lines_over_cleared(viewport_top, to_draw, buffer)?;
|
||||
self.set_viewport_area(Rect {
|
||||
y: viewport_top + to_draw,
|
||||
..self.viewport_area
|
||||
});
|
||||
height -= to_draw;
|
||||
}
|
||||
}
|
||||
|
||||
let viewport_top = self.viewport_area.top();
|
||||
while height > 0 {
|
||||
let to_draw = height.min(viewport_top);
|
||||
self.backend.scroll_region_up(0..viewport_top, to_draw)?;
|
||||
buffer = self.draw_lines_over_cleared(viewport_top - to_draw, to_draw, buffer)?;
|
||||
height -= to_draw;
|
||||
}
|
||||
|
||||
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.
|
||||
///
|
||||
/// This is a small internal helper used by [`Terminal::insert_before`]. It writes cells
|
||||
/// directly to the backend in terminal coordinates (not viewport coordinates).
|
||||
fn draw_lines<'a>(
|
||||
&mut self,
|
||||
y_offset: u16,
|
||||
lines_to_draw: u16,
|
||||
cells: &'a [Cell],
|
||||
) -> Result<&'a [Cell], B::Error> {
|
||||
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)
|
||||
}
|
||||
|
||||
/// Draw lines at the given vertical offset, assuming that the lines they are replacing on the
|
||||
/// screen are cleared. The slice of cells must contain enough cells for the requested lines. A
|
||||
/// slice of the unused cells are returned.
|
||||
///
|
||||
/// This is used by the `scrolling-regions` implementation of [`Terminal::insert_before`] to
|
||||
/// avoid relying on a full-screen clear while updating only part of the terminal.
|
||||
#[cfg(feature = "scrolling-regions")]
|
||||
fn draw_lines_over_cleared<'a>(
|
||||
&mut self,
|
||||
y_offset: u16,
|
||||
lines_to_draw: u16,
|
||||
cells: &'a [Cell],
|
||||
) -> Result<&'a [Cell], B::Error> {
|
||||
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 area = Rect::new(0, y_offset, width as u16, y_offset + lines_to_draw);
|
||||
let old = Buffer::empty(area);
|
||||
let new = Buffer {
|
||||
area,
|
||||
content: to_draw.to_vec(),
|
||||
};
|
||||
self.backend.draw(old.diff(&new).into_iter())?;
|
||||
self.backend.flush()?;
|
||||
}
|
||||
Ok(remainder)
|
||||
}
|
||||
|
||||
/// Scroll the whole screen up by the given number of lines.
|
||||
///
|
||||
/// This is used by [`Terminal::insert_before`] when the `scrolling-regions` feature is
|
||||
/// disabled.
|
||||
/// It scrolls by moving the cursor to the last row and calling [`Backend::append_lines`].
|
||||
#[cfg(not(feature = "scrolling-regions"))]
|
||||
fn scroll_up(&mut self, lines_to_scroll: u16) -> Result<(), B::Error> {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute the on-screen area for an inline viewport.
|
||||
///
|
||||
/// This helper is used by [`Terminal::with_options`] (initialization) and [`Terminal::resize`]
|
||||
/// (after a terminal resize) to translate `Viewport::Inline(height)` into a concrete [`Rect`].
|
||||
///
|
||||
/// This returns the computed viewport area and the cursor position observed at the start of the
|
||||
/// call.
|
||||
///
|
||||
/// Inline viewports always start at column 0, span the full terminal width, and are anchored to the
|
||||
/// backend cursor row at the time of the call. The requested height is clamped to the current
|
||||
/// terminal height.
|
||||
///
|
||||
/// Ratatui reserves vertical space for the requested height by calling [`Backend::append_lines`].
|
||||
/// If the cursor is close enough to the bottom that appending would run past the last row,
|
||||
/// terminals scroll; in that case we shift the computed `y` upward by the number of rows scrolled
|
||||
/// so the viewport remains fully visible.
|
||||
///
|
||||
/// `offset_in_previous_viewport` is used by [`Terminal::resize`] to keep the cursor at the same
|
||||
/// relative row within the viewport across resizes.
|
||||
///
|
||||
/// Related viewport code lives in:
|
||||
///
|
||||
/// - [`Terminal::with_options`] (selects the viewport and computes the initial area)
|
||||
/// - [`Terminal::autoresize`] (detects backend size changes during [`Terminal::draw`])
|
||||
/// - [`Terminal::resize`] (recomputes the viewport and clears before the next draw)
|
||||
pub(crate) fn compute_inline_size<B: Backend>(
|
||||
backend: &mut B,
|
||||
height: u16,
|
||||
size: Size,
|
||||
offset_in_previous_viewport: u16,
|
||||
) -> Result<(Rect, Position), B::Error> {
|
||||
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,
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::backend::{Backend, TestBackend};
|
||||
use crate::layout::{Position, Rect, Size};
|
||||
use crate::style::Style;
|
||||
use crate::terminal::inline::compute_inline_size;
|
||||
use crate::terminal::{Terminal, TerminalOptions, Viewport};
|
||||
|
||||
#[test]
|
||||
fn compute_inline_size_uses_cursor_offset_when_space_available() {
|
||||
// Diagram (terminal height = 10, requested viewport height = 4):
|
||||
//
|
||||
// Cursor at y=6, previous cursor offset within viewport = 1.
|
||||
//
|
||||
// Before (conceptually):
|
||||
// 0
|
||||
// 1
|
||||
// 2
|
||||
// 3
|
||||
// 4
|
||||
// 5 <- viewport top (expected)
|
||||
// 6 <- cursor row (observed_pos.y)
|
||||
// 7
|
||||
// 8
|
||||
// 9
|
||||
//
|
||||
// After: viewport top y = 5 (6 - 1), height = 4 => rows 5..9 (exclusive).
|
||||
let mut backend = TestBackend::new(10, 10);
|
||||
backend
|
||||
.set_cursor_position(Position { x: 0, y: 6 })
|
||||
.unwrap();
|
||||
|
||||
let (area, observed_pos) =
|
||||
compute_inline_size(&mut backend, 4, Size::new(10, 10), 1).unwrap();
|
||||
|
||||
assert_eq!(observed_pos, Position { x: 0, y: 6 });
|
||||
assert_eq!(area, Rect::new(0, 5, 10, 4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compute_inline_size_saturates_when_offset_exceeds_cursor_row() {
|
||||
// Diagram (terminal height = 10, requested viewport height = 4):
|
||||
//
|
||||
// Cursor at y=0, previous cursor offset within viewport = 5 (nonsensical but possible if
|
||||
// callers pass a stale/oversized offset).
|
||||
//
|
||||
// We saturate so the computed viewport top cannot go negative:
|
||||
// top = cursor_y.saturating_sub(offset) = 0.saturating_sub(5) = 0
|
||||
//
|
||||
// Expected viewport area:
|
||||
// y=0..4 (fully pinned to the top)
|
||||
let mut backend = TestBackend::new(10, 10);
|
||||
backend
|
||||
.set_cursor_position(Position { x: 0, y: 0 })
|
||||
.unwrap();
|
||||
|
||||
let (area, _observed_pos) =
|
||||
compute_inline_size(&mut backend, 4, Size::new(10, 10), 5).unwrap();
|
||||
|
||||
assert_eq!(area, Rect::new(0, 0, 10, 4));
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "scrolling-regions"))]
|
||||
mod no_scrolling_regions {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn insert_before_is_noop_for_non_inline_viewports() {
|
||||
// Diagram:
|
||||
//
|
||||
// Viewport is fullscreen (not inline), so insert_before() is a no-op.
|
||||
//
|
||||
// Screen before:
|
||||
// x..
|
||||
// ...
|
||||
//
|
||||
// Screen after:
|
||||
// x..
|
||||
// ...
|
||||
let mut terminal = Terminal::new(TestBackend::new(3, 2)).unwrap();
|
||||
{
|
||||
let frame = terminal.get_frame();
|
||||
frame.buffer[(0, 0)].set_symbol("x");
|
||||
}
|
||||
terminal.flush().unwrap();
|
||||
|
||||
let viewport_area = terminal.viewport_area;
|
||||
terminal
|
||||
.insert_before(1, |buf| {
|
||||
buf.set_string(0, 0, "zzz", Style::default());
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(terminal.viewport_area, viewport_area);
|
||||
terminal.backend().assert_buffer_lines(["x ", " "]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_before_pushes_viewport_down_when_space_available() {
|
||||
// Diagram (screen height = 10, viewport height = 4, cursor row = 3):
|
||||
//
|
||||
// Before:
|
||||
// 0: 0000000000
|
||||
// 1: 1111111111
|
||||
// 2: 2222222222
|
||||
// 3: [viewport top] 3333333333
|
||||
// 4: 4444444444
|
||||
// 5: 5555555555
|
||||
// 6: 6666666666
|
||||
// 7: 7777777777
|
||||
// 8: 8888888888
|
||||
// 9: 9999999999
|
||||
//
|
||||
// After inserting 1 line above an inline viewport (no scrolling regions):
|
||||
// - A line is drawn at the old viewport top (y=3)
|
||||
// - The viewport moves down by 1 row (new top y=4)
|
||||
// - The viewport is cleared so it will be redrawn on the next draw()
|
||||
let mut backend = TestBackend::with_lines([
|
||||
"0000000000",
|
||||
"1111111111",
|
||||
"2222222222",
|
||||
"3333333333",
|
||||
"4444444444",
|
||||
"5555555555",
|
||||
"6666666666",
|
||||
"7777777777",
|
||||
"8888888888",
|
||||
"9999999999",
|
||||
]);
|
||||
backend
|
||||
.set_cursor_position(Position { x: 0, y: 3 })
|
||||
.unwrap();
|
||||
let mut terminal = Terminal::with_options(
|
||||
backend,
|
||||
TerminalOptions {
|
||||
viewport: Viewport::Inline(4),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
terminal
|
||||
.insert_before(1, |buf| {
|
||||
buf.set_string(0, 0, "INSERTLINE", Style::default());
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(terminal.viewport_area, Rect::new(0, 4, 10, 4));
|
||||
terminal.backend().assert_buffer_lines([
|
||||
"0000000000",
|
||||
"1111111111",
|
||||
"2222222222",
|
||||
"INSERTLINE",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_before_scrolls_when_viewport_is_at_bottom() {
|
||||
// Diagram (screen height = 10, viewport height = 4, cursor row = 6):
|
||||
//
|
||||
// Before:
|
||||
// 0: 0000000000
|
||||
// 1: 1111111111
|
||||
// 2: 2222222222
|
||||
// 3: 3333333333
|
||||
// 4: 4444444444
|
||||
// 5: 5555555555
|
||||
// 6: [viewport top] 6666666666
|
||||
// 7: 7777777777
|
||||
// 8: 8888888888
|
||||
// 9: 9999999999
|
||||
//
|
||||
// After inserting 2 lines:
|
||||
// - The area above the viewport scrolls up to make room
|
||||
// - Inserted lines appear immediately above the viewport
|
||||
// - The viewport is cleared so it will be redrawn on the next draw()
|
||||
let mut backend = TestBackend::with_lines([
|
||||
"0000000000",
|
||||
"1111111111",
|
||||
"2222222222",
|
||||
"3333333333",
|
||||
"4444444444",
|
||||
"5555555555",
|
||||
"6666666666",
|
||||
"7777777777",
|
||||
"8888888888",
|
||||
"9999999999",
|
||||
]);
|
||||
backend
|
||||
.set_cursor_position(Position { x: 0, y: 6 })
|
||||
.unwrap();
|
||||
let mut terminal = Terminal::with_options(
|
||||
backend,
|
||||
TerminalOptions {
|
||||
viewport: Viewport::Inline(4),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
terminal
|
||||
.insert_before(2, |buf| {
|
||||
buf.set_string(0, 0, "INSERTED1", Style::default());
|
||||
buf.set_string(0, 1, "INSERTED2", Style::default());
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(terminal.viewport_area, Rect::new(0, 6, 10, 4));
|
||||
terminal.backend().assert_buffer_lines([
|
||||
"2222222222",
|
||||
"3333333333",
|
||||
"4444444444",
|
||||
"5555555555",
|
||||
"INSERTED1 ",
|
||||
"INSERTED2 ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_before_then_draw_repaints_cleared_viewport() {
|
||||
// Diagram (screen height = 10, viewport height = 4, cursor row = 6):
|
||||
//
|
||||
// 1) Draw a frame into the inline viewport at the bottom:
|
||||
// 6..9: AAAAAAAAAA
|
||||
//
|
||||
// 2) Insert 2 lines above the viewport:
|
||||
// - Inserts appear at rows 4..5
|
||||
// - Viewport is cleared (so it is blank on-screen until the next draw)
|
||||
//
|
||||
// 3) Draw again:
|
||||
// 6..9: BBBBBBBBBB
|
||||
//
|
||||
// Expected final screen:
|
||||
// 4: INSERTED00
|
||||
// 5: INSERTED01
|
||||
// 6..9: BBBBBBBBBB
|
||||
let mut backend = TestBackend::new(10, 10);
|
||||
backend
|
||||
.set_cursor_position(Position { x: 0, y: 6 })
|
||||
.unwrap();
|
||||
let mut terminal = Terminal::with_options(
|
||||
backend,
|
||||
TerminalOptions {
|
||||
viewport: Viewport::Inline(4),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
let area = frame.area();
|
||||
for y in area.top()..area.bottom() {
|
||||
frame
|
||||
.buffer
|
||||
.set_string(area.x, y, "AAAAAAAAAA", Style::default());
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
terminal
|
||||
.insert_before(2, |buf| {
|
||||
buf.set_string(0, 0, "INSERTED00", Style::default());
|
||||
buf.set_string(0, 1, "INSERTED01", Style::default());
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
let area = frame.area();
|
||||
for y in area.top()..area.bottom() {
|
||||
frame
|
||||
.buffer
|
||||
.set_string(area.x, y, "BBBBBBBBBB", Style::default());
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
terminal.backend().assert_buffer_lines([
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
"INSERTED00",
|
||||
"INSERTED01",
|
||||
"BBBBBBBBBB",
|
||||
"BBBBBBBBBB",
|
||||
"BBBBBBBBBB",
|
||||
"BBBBBBBBBB",
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "scrolling-regions")]
|
||||
mod scrolling_regions {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn insert_before_moves_viewport_down_without_clearing() {
|
||||
// Diagram (screen height = 10, viewport height = 4, cursor row = 3):
|
||||
//
|
||||
// With scrolling regions enabled, we can create a gap and draw the inserted line
|
||||
// without clearing the viewport content.
|
||||
//
|
||||
// Before:
|
||||
// 2: 2222222222
|
||||
// 3: [viewport top] 3333333333
|
||||
// 4: 4444444444
|
||||
//
|
||||
// After:
|
||||
// 3: INSERTLINE
|
||||
// 4: 3333333333 (viewport content preserved)
|
||||
let mut backend = TestBackend::with_lines([
|
||||
"0000000000",
|
||||
"1111111111",
|
||||
"2222222222",
|
||||
"3333333333",
|
||||
"4444444444",
|
||||
"5555555555",
|
||||
"6666666666",
|
||||
"7777777777",
|
||||
"8888888888",
|
||||
"9999999999",
|
||||
]);
|
||||
backend
|
||||
.set_cursor_position(Position { x: 0, y: 3 })
|
||||
.unwrap();
|
||||
let mut terminal = Terminal::with_options(
|
||||
backend,
|
||||
TerminalOptions {
|
||||
viewport: Viewport::Inline(4),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
terminal
|
||||
.insert_before(1, |buf| {
|
||||
buf.set_string(0, 0, "INSERTLINE", Style::default());
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(terminal.viewport_area, Rect::new(0, 4, 10, 4));
|
||||
terminal.backend().assert_buffer_lines([
|
||||
"0000000000",
|
||||
"1111111111",
|
||||
"2222222222",
|
||||
"INSERTLINE",
|
||||
"3333333333",
|
||||
"4444444444",
|
||||
"5555555555",
|
||||
"6666666666",
|
||||
"8888888888",
|
||||
"9999999999",
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_before_when_viewport_is_at_bottom_preserves_viewport() {
|
||||
// Diagram (screen height = 10, viewport height = 4, viewport top = 6):
|
||||
//
|
||||
// With scrolling regions enabled and the viewport already at the bottom:
|
||||
// - The region above the viewport (rows 0..6) scrolls up to make room.
|
||||
// - Inserted lines are drawn into the cleared space immediately above the viewport.
|
||||
// - The viewport itself is not cleared and stays on-screen.
|
||||
//
|
||||
// Before (after drawing V into the viewport):
|
||||
// 0: 0000000000
|
||||
// 1: 1111111111
|
||||
// 2: 2222222222
|
||||
// 3: 3333333333
|
||||
// 4: 4444444444
|
||||
// 5: 5555555555
|
||||
// 6..9: VVVVVVVVVV
|
||||
//
|
||||
// After inserting 2 lines:
|
||||
// 0..3: previous 2..5
|
||||
// 4: AAAAAAAAAA
|
||||
// 5: BBBBBBBBBB
|
||||
// 6..9: VVVVVVVVVV
|
||||
//
|
||||
// The scrolled-off lines are appended to scrollback (previous 0 and 1).
|
||||
let mut backend = TestBackend::with_lines([
|
||||
"0000000000",
|
||||
"1111111111",
|
||||
"2222222222",
|
||||
"3333333333",
|
||||
"4444444444",
|
||||
"5555555555",
|
||||
"6666666666",
|
||||
"7777777777",
|
||||
"8888888888",
|
||||
"9999999999",
|
||||
]);
|
||||
backend
|
||||
.set_cursor_position(Position { x: 0, y: 6 })
|
||||
.unwrap();
|
||||
let mut terminal = Terminal::with_options(
|
||||
backend,
|
||||
TerminalOptions {
|
||||
viewport: Viewport::Inline(4),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
let area = frame.area();
|
||||
for y in area.top()..area.bottom() {
|
||||
frame
|
||||
.buffer
|
||||
.set_string(area.x, y, "VVVVVVVVVV", Style::default());
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
terminal
|
||||
.insert_before(2, |buf| {
|
||||
buf.set_string(0, 0, "AAAAAAAAAA", Style::default());
|
||||
buf.set_string(0, 1, "BBBBBBBBBB", Style::default());
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
terminal.backend().assert_buffer_lines([
|
||||
"2222222222",
|
||||
"3333333333",
|
||||
"4444444444",
|
||||
"5555555555",
|
||||
"AAAAAAAAAA",
|
||||
"BBBBBBBBBB",
|
||||
"VVVVVVVVVV",
|
||||
"VVVVVVVVVV",
|
||||
"VVVVVVVVVV",
|
||||
"VVVVVVVVVV",
|
||||
]);
|
||||
terminal
|
||||
.backend()
|
||||
.assert_scrollback_lines(["0000000000", "1111111111"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_before_when_viewport_is_fullscreen_appends_to_scrollback() {
|
||||
// Diagram (screen height = 4, viewport height = 4):
|
||||
//
|
||||
// When the viewport takes the whole screen, there is no visible "area above" it.
|
||||
// The scrolling-regions implementation handles this by repeatedly:
|
||||
// - drawing one line over the top row
|
||||
// - immediately scrolling that row into scrollback
|
||||
//
|
||||
// The viewport content stays on-screen; inserted lines end up in scrollback.
|
||||
let mut backend = TestBackend::new(10, 4);
|
||||
backend
|
||||
.set_cursor_position(Position { x: 0, y: 0 })
|
||||
.unwrap();
|
||||
let mut terminal = Terminal::with_options(
|
||||
backend,
|
||||
TerminalOptions {
|
||||
viewport: Viewport::Inline(4),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
let area = frame.area();
|
||||
frame
|
||||
.buffer
|
||||
.set_string(area.x, area.y, "VIEWLINE00", Style::default());
|
||||
frame
|
||||
.buffer
|
||||
.set_string(area.x, area.y + 1, "VIEWLINE01", Style::default());
|
||||
frame
|
||||
.buffer
|
||||
.set_string(area.x, area.y + 2, "VIEWLINE02", Style::default());
|
||||
frame
|
||||
.buffer
|
||||
.set_string(area.x, area.y + 3, "VIEWLINE03", Style::default());
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
terminal
|
||||
.insert_before(2, |buf| {
|
||||
buf.set_string(0, 0, "INSERTED00", Style::default());
|
||||
buf.set_string(0, 1, "INSERTED01", Style::default());
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
terminal.backend().assert_buffer_lines([
|
||||
"VIEWLINE00",
|
||||
"VIEWLINE01",
|
||||
"VIEWLINE02",
|
||||
"VIEWLINE03",
|
||||
]);
|
||||
terminal
|
||||
.backend()
|
||||
.assert_scrollback_lines(["INSERTED00", "INSERTED01"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,737 +0,0 @@
|
||||
use crate::backend::Backend;
|
||||
use crate::terminal::{CompletedFrame, Frame, Terminal};
|
||||
|
||||
impl<B: Backend> Terminal<B> {
|
||||
/// Draws a single frame to the terminal.
|
||||
///
|
||||
/// Returns a [`CompletedFrame`] if successful, otherwise a backend error (`B::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
|
||||
///
|
||||
/// The [`Frame`] passed to the render callback represents the currently configured
|
||||
/// [`Viewport`] (see [`Frame::area`] and [`Terminal::with_options`]).
|
||||
///
|
||||
/// Build layout relative to the [`Rect`] returned by [`Frame::area`] rather than assuming the
|
||||
/// origin is `(0, 0)`, so the same rendering code works for fixed and inline viewports.
|
||||
///
|
||||
/// [`Frame::area`]: crate::terminal::Frame::area
|
||||
/// [`Rect`]: crate::layout::Rect
|
||||
/// [`Viewport`]: crate::terminal::Viewport
|
||||
///
|
||||
/// This method will:
|
||||
///
|
||||
/// - call [`Terminal::autoresize`] if necessary
|
||||
/// - call the render callback, passing it a [`Frame`] reference to render to
|
||||
/// - call [`Terminal::flush`] to write changes to the backend
|
||||
/// - show/hide the cursor based on [`Frame::set_cursor_position`]
|
||||
/// - call [`Terminal::swap_buffers`] to prepare for the next render pass
|
||||
/// - call [`Backend::flush`]
|
||||
/// - return a [`CompletedFrame`] with the current buffer and the area used for rendering
|
||||
///
|
||||
/// The [`CompletedFrame`] returned by this method can be useful for debugging or testing
|
||||
/// purposes, but it is often not used in regular applications.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # mod ratatui {
|
||||
/// # pub use ratatui_core::backend;
|
||||
/// # pub use ratatui_core::layout;
|
||||
/// # pub use ratatui_core::terminal::{Frame, Terminal};
|
||||
/// # }
|
||||
/// use ratatui::backend::TestBackend;
|
||||
/// use ratatui::layout::Position;
|
||||
/// use ratatui::{Frame, Terminal};
|
||||
///
|
||||
/// let backend = TestBackend::new(10, 10);
|
||||
/// let mut terminal = Terminal::new(backend)?;
|
||||
///
|
||||
/// // With a closure.
|
||||
/// terminal.draw(|frame| {
|
||||
/// let area = frame.area();
|
||||
/// frame.render_widget("Hello World!", area);
|
||||
/// frame.set_cursor_position(Position { x: 0, y: 0 });
|
||||
/// })?;
|
||||
///
|
||||
/// // Or with a function.
|
||||
/// terminal.draw(render)?;
|
||||
///
|
||||
/// fn render(frame: &mut Frame<'_>) {
|
||||
/// frame.render_widget("Hello World!", frame.area());
|
||||
/// }
|
||||
/// # Ok::<(), Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
///
|
||||
/// [`Backend::flush`]: crate::backend::Backend::flush
|
||||
pub fn draw<F>(&mut self, render_callback: F) -> Result<CompletedFrame<'_>, B::Error>
|
||||
where
|
||||
F: FnOnce(&mut Frame),
|
||||
{
|
||||
self.try_draw(|frame| {
|
||||
render_callback(frame);
|
||||
Ok::<(), B::Error>(())
|
||||
})
|
||||
}
|
||||
|
||||
/// Tries to draw a single frame to the terminal.
|
||||
///
|
||||
/// Returns [`Result::Ok`] containing a [`CompletedFrame`] if successful, otherwise
|
||||
/// [`Result::Err`] containing the backend error (`B::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
|
||||
///
|
||||
/// The [`Frame`] passed to the render callback represents the currently configured
|
||||
/// [`Viewport`] (see [`Frame::area`] and [`Terminal::with_options`]).
|
||||
///
|
||||
/// Build layout relative to the [`Rect`] returned by [`Frame::area`] rather than assuming the
|
||||
/// origin is `(0, 0)`, so the same rendering code works for fixed and inline viewports.
|
||||
///
|
||||
/// [`Frame::area`]: crate::terminal::Frame::area
|
||||
/// [`Rect`]: crate::layout::Rect
|
||||
/// [`Viewport`]: crate::terminal::Viewport
|
||||
///
|
||||
/// This method will:
|
||||
///
|
||||
/// - call [`Terminal::autoresize`] if necessary
|
||||
/// - call the render callback, passing it a [`Frame`] reference to render to
|
||||
/// - call [`Terminal::flush`] to write changes to the backend
|
||||
/// - show/hide the cursor based on [`Frame::set_cursor_position`]
|
||||
/// - call [`Terminal::swap_buffers`] to prepare for the next render pass
|
||||
/// - call [`Backend::flush`]
|
||||
/// - return a [`CompletedFrame`] with the current buffer and the area used for rendering
|
||||
///
|
||||
/// The render callback passed to `try_draw` can return any [`Result`] with an error type that
|
||||
/// can be converted into `B::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` 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 applications.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # #![allow(unexpected_cfgs)]
|
||||
/// # #[cfg(feature = "crossterm")]
|
||||
/// # {
|
||||
/// use std::io;
|
||||
///
|
||||
/// use ratatui::backend::CrosstermBackend;
|
||||
/// use ratatui::layout::Position;
|
||||
/// use ratatui::{Frame, Terminal};
|
||||
///
|
||||
/// let backend = CrosstermBackend::new(std::io::stdout());
|
||||
/// let mut terminal = Terminal::new(backend)?;
|
||||
///
|
||||
/// // With a closure that returns `Result`.
|
||||
/// terminal.try_draw(|frame| -> io::Result<()> {
|
||||
/// let _value: u8 = "42".parse().map_err(io::Error::other)?;
|
||||
/// let area = frame.area();
|
||||
/// frame.render_widget("Hello World!", area);
|
||||
/// frame.set_cursor_position(Position { x: 0, y: 0 });
|
||||
/// Ok(())
|
||||
/// })?;
|
||||
///
|
||||
/// // Or with a function.
|
||||
/// terminal.try_draw(render)?;
|
||||
///
|
||||
/// fn render(frame: &mut Frame<'_>) -> io::Result<()> {
|
||||
/// frame.render_widget("Hello World!", frame.area());
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// # }
|
||||
/// # #[cfg(not(feature = "crossterm"))]
|
||||
/// # {
|
||||
/// # use ratatui_core::{backend::TestBackend, terminal::Terminal};
|
||||
/// # let backend = TestBackend::new(10, 10);
|
||||
/// # let mut terminal = Terminal::new(backend)?;
|
||||
/// # terminal
|
||||
/// # .try_draw(|frame| {
|
||||
/// # frame.render_widget("Hello World!", frame.area());
|
||||
/// # Ok::<(), core::convert::Infallible>(())
|
||||
/// # })
|
||||
/// # ?;
|
||||
/// # }
|
||||
/// # Ok::<(), Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
///
|
||||
/// [`Backend::flush`]: crate::backend::Backend::flush
|
||||
pub fn try_draw<F, E>(&mut self, render_callback: F) -> Result<CompletedFrame<'_>, B::Error>
|
||||
where
|
||||
F: FnOnce(&mut Frame) -> Result<(), E>,
|
||||
E: Into<B::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;
|
||||
|
||||
// Apply the buffer diff to the backend (this is the terminal's "flush" step, distinct
|
||||
// from `Backend::flush` below which flushes the backend's output).
|
||||
self.flush()?;
|
||||
|
||||
match cursor_position {
|
||||
None => self.hide_cursor()?,
|
||||
Some(position) => {
|
||||
self.show_cursor()?;
|
||||
self.set_cursor_position(position)?;
|
||||
}
|
||||
}
|
||||
|
||||
self.swap_buffers();
|
||||
|
||||
// Flush any buffered backend output.
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use core::fmt;
|
||||
|
||||
use crate::backend::{Backend, ClearType, TestBackend, WindowSize};
|
||||
use crate::buffer::{Buffer, Cell};
|
||||
use crate::layout::{Position, Rect};
|
||||
use crate::terminal::{Terminal, TerminalOptions, Viewport};
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
struct TestError(&'static str);
|
||||
|
||||
impl fmt::Display for TestError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl core::error::Error for TestError {}
|
||||
|
||||
/// A thin wrapper around [`TestBackend`] with a fallible error type.
|
||||
///
|
||||
/// [`TestBackend`] uses [`core::convert::Infallible`] as its associated `Backend::Error`, which
|
||||
/// is ideal for most tests but makes it impossible to write a `try_draw` callback that returns
|
||||
/// an error (because `E: Into<B::Error>` would require converting a real error into
|
||||
/// `Infallible`). This wrapper keeps the same observable backend behavior (buffer + cursor)
|
||||
/// while allowing tests to exercise `Terminal::try_draw`'s error path.
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
struct FallibleTestBackend {
|
||||
inner: TestBackend,
|
||||
}
|
||||
|
||||
impl FallibleTestBackend {
|
||||
fn new(inner: TestBackend) -> Self {
|
||||
Self { inner }
|
||||
}
|
||||
}
|
||||
|
||||
impl Backend for FallibleTestBackend {
|
||||
type Error = TestError;
|
||||
|
||||
fn draw<'a, I>(&mut self, content: I) -> Result<(), Self::Error>
|
||||
where
|
||||
I: Iterator<Item = (u16, u16, &'a crate::buffer::Cell)>,
|
||||
{
|
||||
self.inner.draw(content).map_err(|err| match err {})
|
||||
}
|
||||
|
||||
fn append_lines(&mut self, n: u16) -> Result<(), Self::Error> {
|
||||
self.inner.append_lines(n).map_err(|err| match err {})
|
||||
}
|
||||
|
||||
fn hide_cursor(&mut self) -> Result<(), Self::Error> {
|
||||
self.inner.hide_cursor().map_err(|err| match err {})
|
||||
}
|
||||
|
||||
fn show_cursor(&mut self) -> Result<(), Self::Error> {
|
||||
self.inner.show_cursor().map_err(|err| match err {})
|
||||
}
|
||||
|
||||
fn get_cursor_position(&mut self) -> Result<Position, Self::Error> {
|
||||
self.inner.get_cursor_position().map_err(|err| match err {})
|
||||
}
|
||||
|
||||
fn set_cursor_position<P: Into<Position>>(
|
||||
&mut self,
|
||||
position: P,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.inner
|
||||
.set_cursor_position(position)
|
||||
.map_err(|err| match err {})
|
||||
}
|
||||
|
||||
fn clear(&mut self) -> Result<(), Self::Error> {
|
||||
self.inner.clear().map_err(|err| match err {})
|
||||
}
|
||||
|
||||
fn clear_region(&mut self, clear_type: ClearType) -> Result<(), Self::Error> {
|
||||
self.inner
|
||||
.clear_region(clear_type)
|
||||
.map_err(|err| match err {})
|
||||
}
|
||||
|
||||
fn size(&self) -> Result<crate::layout::Size, Self::Error> {
|
||||
self.inner.size().map_err(|err| match err {})
|
||||
}
|
||||
|
||||
fn window_size(&mut self) -> Result<WindowSize, Self::Error> {
|
||||
self.inner.window_size().map_err(|err| match err {})
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> Result<(), Self::Error> {
|
||||
self.inner.flush().map_err(|err| match err {})
|
||||
}
|
||||
|
||||
#[cfg(feature = "scrolling-regions")]
|
||||
fn scroll_region_up(
|
||||
&mut self,
|
||||
region: core::ops::Range<u16>,
|
||||
line_count: u16,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.inner
|
||||
.scroll_region_up(region, line_count)
|
||||
.map_err(|err| match err {})
|
||||
}
|
||||
|
||||
#[cfg(feature = "scrolling-regions")]
|
||||
fn scroll_region_down(
|
||||
&mut self,
|
||||
region: core::ops::Range<u16>,
|
||||
line_count: u16,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.inner
|
||||
.scroll_region_down(region, line_count)
|
||||
.map_err(|err| match err {})
|
||||
}
|
||||
}
|
||||
|
||||
/// `draw` hides the cursor when the frame does not request a cursor position.
|
||||
///
|
||||
/// This asserts the end-to-end effect on the backend (buffer contents + cursor state) as well
|
||||
/// as internal frame counting.
|
||||
#[test]
|
||||
fn draw_hides_cursor_when_frame_cursor_is_not_set() {
|
||||
let backend = TestBackend::new(3, 2);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
terminal.show_cursor().unwrap();
|
||||
|
||||
let completed = terminal
|
||||
.draw(|frame| {
|
||||
// Ensure the frame produces updates so `Terminal::flush` writes to the backend.
|
||||
frame.buffer_mut()[(0, 0)] = Cell::new("x");
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(completed.count, 0, "first draw returns count 0");
|
||||
assert_eq!(
|
||||
completed.area,
|
||||
Rect::new(0, 0, 3, 2),
|
||||
"completed area matches terminal size in fullscreen mode"
|
||||
);
|
||||
assert_eq!(
|
||||
completed.buffer,
|
||||
&Buffer::with_lines(["x ", " "]),
|
||||
"completed buffer contains the rendered content"
|
||||
);
|
||||
|
||||
assert!(terminal.hidden_cursor);
|
||||
assert!(!terminal.backend().cursor_visible());
|
||||
assert_eq!(
|
||||
terminal.frame_count, 1,
|
||||
"successful draw increments frame_count"
|
||||
);
|
||||
}
|
||||
|
||||
/// `draw` applies the cursor requested by `Frame::set_cursor_position`.
|
||||
///
|
||||
/// The cursor is updated after rendering has been flushed, so it appears on top of the drawn
|
||||
/// UI.
|
||||
#[test]
|
||||
fn draw_shows_and_positions_cursor_when_frame_cursor_is_set() {
|
||||
let backend = TestBackend::new(3, 2);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
terminal.hide_cursor().unwrap();
|
||||
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
// The cursor is applied after the frame is flushed.
|
||||
frame.set_cursor_position(Position { x: 2, y: 1 });
|
||||
frame.buffer_mut()[(1, 0)] = Cell::new("y");
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert!(!terminal.hidden_cursor);
|
||||
assert!(terminal.backend().cursor_visible());
|
||||
assert_eq!(
|
||||
terminal.backend().cursor_position(),
|
||||
Position { x: 2, y: 1 },
|
||||
"backend cursor is positioned after flushing"
|
||||
);
|
||||
assert_eq!(
|
||||
terminal.last_known_cursor_pos,
|
||||
Position { x: 2, y: 1 },
|
||||
"terminal cursor tracking matches the final cursor position"
|
||||
);
|
||||
}
|
||||
|
||||
/// When the render callback returns an error, `try_draw` does not update the terminal.
|
||||
///
|
||||
/// This is a characterization of the "no partial updates" behavior: backend contents and
|
||||
/// cursor state are unchanged and `frame_count` does not advance.
|
||||
#[test]
|
||||
fn try_draw_propagates_render_errors_without_updating_backend() {
|
||||
let backend = FallibleTestBackend::new(TestBackend::with_lines(["aaa", "bbb"]));
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
terminal.show_cursor().unwrap();
|
||||
|
||||
let was_hidden = terminal.hidden_cursor;
|
||||
let cursor_visible = terminal.backend().inner.cursor_visible();
|
||||
let cursor_position = terminal.backend().inner.cursor_position();
|
||||
|
||||
let result = terminal.try_draw(|_frame| Err::<(), _>(TestError("render failed")));
|
||||
|
||||
assert_eq!(
|
||||
result.unwrap_err(),
|
||||
TestError("render failed"),
|
||||
"try_draw returns the render callback error"
|
||||
);
|
||||
|
||||
assert_eq!(terminal.frame_count, 0, "frame_count is unchanged on error");
|
||||
assert_eq!(
|
||||
terminal.backend().inner.buffer(),
|
||||
&Buffer::with_lines(["aaa", "bbb"]),
|
||||
"backend buffer is unchanged on error"
|
||||
);
|
||||
assert_eq!(
|
||||
terminal.hidden_cursor, was_hidden,
|
||||
"terminal cursor state is unchanged on error"
|
||||
);
|
||||
assert_eq!(
|
||||
terminal.backend().inner.cursor_visible(),
|
||||
cursor_visible,
|
||||
"backend cursor visibility is unchanged on error"
|
||||
);
|
||||
assert_eq!(
|
||||
terminal.backend().inner.cursor_position(),
|
||||
cursor_position,
|
||||
"backend cursor position is unchanged on error"
|
||||
);
|
||||
}
|
||||
|
||||
/// `draw` autoresizes fullscreen terminals and clears before rendering.
|
||||
///
|
||||
/// This simulates the backend resizing between draw calls; `draw` runs `autoresize()` first
|
||||
/// (which calls `resize()` and clears) so the frame renders into a fresh, correctly-sized
|
||||
/// region.
|
||||
#[test]
|
||||
fn draw_clears_on_fullscreen_resize_before_rendering() {
|
||||
let backend = TestBackend::with_lines(["xxx", "yyy"]);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
terminal.backend_mut().resize(4, 3);
|
||||
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
// Render a marker to show we rendered after the clear.
|
||||
frame.buffer_mut()[(0, 0)] = Cell::new("x");
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
terminal.viewport_area,
|
||||
Rect::new(0, 0, 4, 3),
|
||||
"viewport area tracks the resized terminal size"
|
||||
);
|
||||
assert_eq!(
|
||||
terminal.last_known_area,
|
||||
Rect::new(0, 0, 4, 3),
|
||||
"last_known_area tracks the resized terminal size"
|
||||
);
|
||||
terminal
|
||||
.backend()
|
||||
.assert_buffer_lines(["x ", " ", " "]);
|
||||
}
|
||||
|
||||
/// In fixed viewports, `Frame::area` is an absolute terminal rectangle.
|
||||
///
|
||||
/// This asserts that rendering at `frame.area().x/y` updates the backend at that absolute
|
||||
/// position.
|
||||
#[test]
|
||||
fn draw_uses_fixed_viewport_coordinates() {
|
||||
let backend = TestBackend::new(5, 3);
|
||||
let mut terminal = Terminal::with_options(
|
||||
backend,
|
||||
TerminalOptions {
|
||||
viewport: Viewport::Fixed(Rect::new(2, 1, 2, 1)),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
assert_eq!(
|
||||
frame.area(),
|
||||
Rect::new(2, 1, 2, 1),
|
||||
"frame area matches the configured fixed viewport"
|
||||
);
|
||||
let area = frame.area();
|
||||
frame.buffer_mut()[(area.x, area.y)] = Cell::new("z");
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
terminal
|
||||
.backend()
|
||||
.assert_buffer_lines([" ", " z ", " "]);
|
||||
}
|
||||
|
||||
/// Inline viewports render into a sub-rectangle, but `CompletedFrame::area` reports terminal
|
||||
/// size.
|
||||
///
|
||||
/// This asserts that the `CompletedFrame` returned from `draw` reports the full terminal
|
||||
/// size while its buffer is sized to the inline viewport, and that rendering uses the inline
|
||||
/// viewport's absolute origin.
|
||||
#[test]
|
||||
fn draw_inline_completed_frame_reports_terminal_size() {
|
||||
let mut inner = TestBackend::new(6, 5);
|
||||
inner.set_cursor_position((0, 2)).unwrap();
|
||||
let mut terminal = Terminal::with_options(
|
||||
inner,
|
||||
TerminalOptions {
|
||||
viewport: Viewport::Inline(3),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let viewport_area = terminal.viewport_area;
|
||||
{
|
||||
// `CompletedFrame` borrows the terminal, so backend assertions happen after it drops.
|
||||
let completed = terminal
|
||||
.draw(|frame| {
|
||||
assert_eq!(
|
||||
frame.area(),
|
||||
viewport_area,
|
||||
"inline frame area matches the computed viewport"
|
||||
);
|
||||
frame.buffer_mut()[(viewport_area.x, viewport_area.y)] = Cell::new("i");
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
completed.area,
|
||||
Rect::new(0, 0, 6, 5),
|
||||
"completed area reports the full terminal size"
|
||||
);
|
||||
assert_eq!(
|
||||
completed.buffer.area, viewport_area,
|
||||
"completed buffer is sized to the inline viewport"
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
terminal.backend().buffer()[(viewport_area.x, viewport_area.y)].symbol(),
|
||||
"i"
|
||||
);
|
||||
}
|
||||
|
||||
/// Inline viewports are autoresized during `draw`.
|
||||
///
|
||||
/// This asserts that when the backend reports a different terminal size, `draw` recomputes the
|
||||
/// inline viewport rectangle and renders into the new viewport area.
|
||||
#[test]
|
||||
fn draw_inline_autoresize_recomputes_viewport_on_grow() {
|
||||
let mut backend = TestBackend::new(6, 5);
|
||||
backend
|
||||
.set_cursor_position(Position { x: 0, y: 2 })
|
||||
.unwrap();
|
||||
let mut terminal = Terminal::with_options(
|
||||
backend,
|
||||
TerminalOptions {
|
||||
viewport: Viewport::Inline(3),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
let area = frame.area();
|
||||
frame.set_cursor_position(Position {
|
||||
x: area.x,
|
||||
y: area.y.saturating_add(1),
|
||||
});
|
||||
frame.buffer_mut()[(area.x, area.y)] = Cell::new("a");
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
terminal.backend_mut().resize(8, 7);
|
||||
let new_area = Rect::new(0, 0, 8, 7);
|
||||
|
||||
let previous_viewport = terminal.viewport_area;
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
let area = frame.area();
|
||||
frame.buffer_mut()[(area.x, area.y)] = Cell::new("g");
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
terminal.last_known_area, new_area,
|
||||
"inline last_known_area tracks the resized terminal size"
|
||||
);
|
||||
assert_eq!(
|
||||
terminal.viewport_area.width, 8,
|
||||
"inline viewport width tracks the resized terminal width"
|
||||
);
|
||||
assert_eq!(
|
||||
terminal.viewport_area.height, 3,
|
||||
"inline viewport height is capped by the configured inline height"
|
||||
);
|
||||
assert_eq!(
|
||||
terminal.viewport_area.y, previous_viewport.y,
|
||||
"inline viewport stays anchored relative to the cursor across a grow"
|
||||
);
|
||||
assert_eq!(
|
||||
terminal.backend().buffer()[(terminal.viewport_area.x, terminal.viewport_area.y)]
|
||||
.symbol(),
|
||||
"g",
|
||||
"render output lands at the recomputed viewport origin"
|
||||
);
|
||||
}
|
||||
|
||||
/// Inline viewports are autoresized during `draw`.
|
||||
///
|
||||
/// This asserts that shrinking the backend terminal size causes `draw` to recompute the inline
|
||||
/// viewport origin so it stays visible, and that rendering uses the new viewport origin.
|
||||
#[test]
|
||||
fn draw_inline_autoresize_recomputes_viewport_on_shrink() {
|
||||
let mut backend = TestBackend::new(6, 6);
|
||||
backend
|
||||
.set_cursor_position(Position { x: 0, y: 4 })
|
||||
.unwrap();
|
||||
let mut terminal = Terminal::with_options(
|
||||
backend,
|
||||
TerminalOptions {
|
||||
viewport: Viewport::Inline(4),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
let area = frame.area();
|
||||
frame.set_cursor_position(Position {
|
||||
x: area.x,
|
||||
y: area.y.saturating_add(2),
|
||||
});
|
||||
frame.buffer_mut()[(area.x, area.y)] = Cell::new("a");
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
terminal.backend_mut().resize(6, 5);
|
||||
let new_area = Rect::new(0, 0, 6, 5);
|
||||
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
let area = frame.area();
|
||||
frame.buffer_mut()[(area.x, area.y)] = Cell::new("s");
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
terminal.last_known_area, new_area,
|
||||
"inline last_known_area tracks the resized terminal size"
|
||||
);
|
||||
assert_eq!(
|
||||
terminal.viewport_area,
|
||||
Rect::new(0, 1, 6, 4),
|
||||
"inline viewport is recomputed to stay visible after a shrink"
|
||||
);
|
||||
assert_eq!(
|
||||
terminal.backend().buffer()[(terminal.viewport_area.x, terminal.viewport_area.y)]
|
||||
.symbol(),
|
||||
"s",
|
||||
"render output lands at the recomputed viewport origin"
|
||||
);
|
||||
}
|
||||
|
||||
/// `CompletedFrame` is only valid until the next draw call.
|
||||
///
|
||||
/// This asserts that each `draw` returns the buffer for the frame that was just rendered
|
||||
/// and that the count increments after each successful draw.
|
||||
#[test]
|
||||
fn draw_returns_completed_frame_for_current_render_pass() {
|
||||
let backend = TestBackend::new(3, 2);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
{
|
||||
// `CompletedFrame` borrows the terminal, and is only valid until the next draw call.
|
||||
let first = terminal
|
||||
.draw(|frame| {
|
||||
frame.buffer_mut()[(0, 0)] = Cell::new("a");
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(first.count, 0, "first CompletedFrame has count 0");
|
||||
assert_eq!(
|
||||
first.buffer,
|
||||
&Buffer::with_lines(["a ", " "]),
|
||||
"first frame's buffer contains the first render output"
|
||||
);
|
||||
}
|
||||
|
||||
let second = terminal
|
||||
.draw(|frame| {
|
||||
frame.buffer_mut()[(0, 0)] = Cell::new("b");
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(second.count, 1, "second CompletedFrame has count 1");
|
||||
assert_eq!(
|
||||
second.buffer,
|
||||
&Buffer::with_lines(["b ", " "]),
|
||||
"second frame's buffer contains the second render output"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,255 +0,0 @@
|
||||
use crate::backend::Backend;
|
||||
use crate::layout::Rect;
|
||||
use crate::terminal::inline::compute_inline_size;
|
||||
use crate::terminal::{Terminal, Viewport};
|
||||
|
||||
impl<B: Backend> Terminal<B> {
|
||||
/// Updates the Terminal so that internal buffers match the requested area.
|
||||
///
|
||||
/// This updates the buffer size used for rendering and triggers a full clear so the next
|
||||
/// [`Terminal::draw`] paints into a consistent area.
|
||||
///
|
||||
/// When the viewport is [`Viewport::Inline`], the `area` argument is treated as the new
|
||||
/// terminal size and the viewport origin is recomputed relative to the current cursor position.
|
||||
/// Ratatui attempts to keep the cursor at the same relative row within the viewport across
|
||||
/// resizes.
|
||||
///
|
||||
/// See also: [`Terminal::autoresize`] (automatic resizing during [`Terminal::draw`]).
|
||||
pub fn resize(&mut self, area: Rect) -> Result<(), B::Error> {
|
||||
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(())
|
||||
}
|
||||
|
||||
/// Queries the backend for size and resizes if it doesn't match the previous size.
|
||||
///
|
||||
/// This is called automatically during [`Terminal::draw`] for fullscreen and inline viewports.
|
||||
/// Fixed viewports are not automatically resized.
|
||||
///
|
||||
/// If the size changed, this calls [`Terminal::resize`] (which clears the screen).
|
||||
pub fn autoresize(&mut self) -> Result<(), B::Error> {
|
||||
// fixed viewports do not get autoresized
|
||||
if matches!(self.viewport, Viewport::Fullscreen | Viewport::Inline(_)) {
|
||||
let area = self.size()?.into();
|
||||
if area != self.last_known_area {
|
||||
self.resize(area)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Resize internal buffers and update the current viewport area.
|
||||
///
|
||||
/// This is an internal helper used by [`Terminal::with_options`] and [`Terminal::resize`].
|
||||
pub(crate) 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;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::backend::{Backend, TestBackend};
|
||||
use crate::buffer::Buffer;
|
||||
use crate::layout::{Position, Rect};
|
||||
use crate::terminal::{Terminal, TerminalOptions, Viewport};
|
||||
|
||||
#[test]
|
||||
fn resize_fullscreen_updates_viewport_and_buffer_areas() {
|
||||
let backend = TestBackend::new(3, 2);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
terminal.backend_mut().resize(4, 3);
|
||||
let new_area = Rect::new(0, 0, 4, 3);
|
||||
terminal.resize(new_area).unwrap();
|
||||
|
||||
assert_eq!(terminal.viewport_area, new_area);
|
||||
assert_eq!(terminal.last_known_area, new_area);
|
||||
assert_eq!(terminal.buffers[terminal.current].area, new_area);
|
||||
assert_eq!(terminal.buffers[1 - terminal.current].area, new_area);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resize_fullscreen_triggers_clear_and_resets_back_buffer() {
|
||||
// This test is specifically about the side effects of `resize`:
|
||||
// - it calls `clear` to force a full redraw
|
||||
// - it resets the "previous" buffer
|
||||
let backend = TestBackend::new(3, 2);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
// Put visible content on the backend so we can tell whether a clear happened.
|
||||
{
|
||||
let frame = terminal.get_frame();
|
||||
frame.buffer[(0, 0)].set_symbol("x");
|
||||
}
|
||||
terminal.flush().unwrap();
|
||||
terminal.backend().assert_buffer_lines(["x ", " "]);
|
||||
|
||||
terminal.backend_mut().resize(4, 3);
|
||||
let new_area = Rect::new(0, 0, 4, 3);
|
||||
terminal.resize(new_area).unwrap();
|
||||
|
||||
terminal
|
||||
.backend()
|
||||
.assert_buffer_lines([" ", " ", " "]);
|
||||
assert_eq!(
|
||||
terminal.buffers[1 - terminal.current],
|
||||
Buffer::empty(new_area)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn autoresize_fullscreen_uses_backend_size_when_changed() {
|
||||
let backend = TestBackend::new(3, 2);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
{
|
||||
let frame = terminal.get_frame();
|
||||
frame.buffer[(0, 0)].set_symbol("x");
|
||||
}
|
||||
terminal.flush().unwrap();
|
||||
|
||||
terminal.backend_mut().resize(4, 3);
|
||||
terminal.autoresize().unwrap();
|
||||
|
||||
assert_eq!(terminal.viewport_area, Rect::new(0, 0, 4, 3));
|
||||
assert_eq!(terminal.last_known_area, Rect::new(0, 0, 4, 3));
|
||||
terminal
|
||||
.backend()
|
||||
.assert_buffer_lines([" ", " ", " "]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn autoresize_fixed_does_not_change_viewport() {
|
||||
let backend = TestBackend::with_lines(["xxx", "yyy"]);
|
||||
let mut terminal = Terminal::with_options(
|
||||
backend,
|
||||
TerminalOptions {
|
||||
viewport: Viewport::Fixed(Rect::new(1, 0, 2, 2)),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
terminal.autoresize().unwrap();
|
||||
|
||||
assert_eq!(terminal.viewport_area, Rect::new(1, 0, 2, 2));
|
||||
assert_eq!(terminal.last_known_area, Rect::new(1, 0, 2, 2));
|
||||
terminal.backend().assert_buffer_lines(["xxx", "yyy"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resize_fixed_changes_viewport_area_and_buffer_sizes() {
|
||||
let backend = TestBackend::new(5, 3);
|
||||
let mut terminal = Terminal::with_options(
|
||||
backend,
|
||||
TerminalOptions {
|
||||
viewport: Viewport::Fixed(Rect::new(1, 1, 2, 1)),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
terminal.resize(Rect::new(0, 0, 3, 2)).unwrap();
|
||||
|
||||
assert_eq!(terminal.viewport_area, Rect::new(0, 0, 3, 2));
|
||||
assert_eq!(terminal.last_known_area, Rect::new(0, 0, 3, 2));
|
||||
assert_eq!(
|
||||
terminal.buffers[terminal.current].area,
|
||||
terminal.viewport_area
|
||||
);
|
||||
assert_eq!(
|
||||
terminal.buffers[1 - terminal.current].area,
|
||||
terminal.viewport_area
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resize_inline_recomputes_origin_using_previous_cursor_offset() {
|
||||
let mut backend = TestBackend::new(10, 10);
|
||||
backend
|
||||
.set_cursor_position(Position { x: 0, y: 4 })
|
||||
.unwrap();
|
||||
let mut terminal = Terminal::with_options(
|
||||
backend,
|
||||
TerminalOptions {
|
||||
viewport: Viewport::Inline(4),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(terminal.viewport_area, Rect::new(0, 4, 10, 4));
|
||||
|
||||
// Characterization test:
|
||||
// This test simulates a terminal resize (increasing the terminal height) while an inline
|
||||
// viewport is active. The key behavior being exercised is that the viewport remains
|
||||
// anchored to the backend cursor row and preserves the cursor's relative offset within the
|
||||
// previous viewport.
|
||||
//
|
||||
// For inline viewports, `Terminal::resize(area)` interprets `area` as the *new terminal
|
||||
// size*, then recomputes the viewport origin based on:
|
||||
// - the backend cursor position at the time of the call
|
||||
// - the cursor offset within the *previous* viewport (`last_known_cursor_pos -
|
||||
// viewport_top`)
|
||||
//
|
||||
// This means `resize(Rect { .. })` can update `viewport_area.y` even when the passed-in
|
||||
// `area.y` is 0, because `viewport_area` is anchored to the cursor row, not the terminal
|
||||
// origin.
|
||||
terminal.last_known_cursor_pos = Position { x: 0, y: 5 };
|
||||
terminal
|
||||
.backend_mut()
|
||||
.set_cursor_position(Position { x: 0, y: 6 })
|
||||
.unwrap();
|
||||
|
||||
terminal.backend_mut().resize(10, 12);
|
||||
let new_terminal_area = Rect::new(0, 0, 10, 12);
|
||||
terminal.resize(new_terminal_area).unwrap();
|
||||
|
||||
// Previous viewport top was y=4, and last_known_cursor_pos was y=5, so the cursor offset
|
||||
// within the viewport is 1 row. At the time of resize the backend cursor is at y=6, so the
|
||||
// new viewport top becomes 6 - 1 = 5.
|
||||
assert_eq!(terminal.viewport_area, Rect::new(0, 5, 10, 4));
|
||||
assert_eq!(terminal.last_known_area, new_terminal_area);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resize_inline_clamps_height_to_terminal_height() {
|
||||
// Characterization test:
|
||||
// This test simulates a terminal resize that *reduces* the terminal height. Inline
|
||||
// viewports clamp their height to the new terminal size so the viewport remains fully
|
||||
// visible.
|
||||
let mut backend = TestBackend::new(10, 10);
|
||||
backend
|
||||
.set_cursor_position(Position { x: 0, y: 0 })
|
||||
.unwrap();
|
||||
let mut terminal = Terminal::with_options(
|
||||
backend,
|
||||
TerminalOptions {
|
||||
viewport: Viewport::Inline(10),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
terminal.backend_mut().resize(10, 3);
|
||||
terminal.resize(Rect::new(0, 0, 10, 3)).unwrap();
|
||||
|
||||
assert_eq!(terminal.viewport_area, Rect::new(0, 0, 10, 3));
|
||||
}
|
||||
}
|
||||
879
ratatui-core/src/terminal/terminal.rs
Normal file
879
ratatui-core/src/terminal/terminal.rs
Normal file
@@ -0,0 +1,879 @@
|
||||
use crate::backend::{Backend, ClearType};
|
||||
use crate::buffer::{Buffer, Cell};
|
||||
use crate::layout::{Position, Rect, Size};
|
||||
use crate::terminal::{CompletedFrame, Frame, 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.
|
||||
///
|
||||
/// # Initialization
|
||||
///
|
||||
/// For most applications, consider using the convenience functions `ratatui::run()`,
|
||||
/// `ratatui::init()`, and `ratatui::restore()` (available since version 0.28.1) along with the
|
||||
/// `DefaultTerminal` type alias instead of constructing `Terminal` instances manually. These
|
||||
/// functions handle the common setup and teardown tasks automatically. Manual construction
|
||||
/// using `Terminal::new()` or `Terminal::with_options()` is still supported for applications
|
||||
/// that need fine-grained control over initialization.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ## Using convenience functions (recommended for most applications)
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// // Modern approach using convenience functions
|
||||
/// ratatui::run(|terminal| {
|
||||
/// terminal.draw(|frame| {
|
||||
/// let area = frame.area();
|
||||
/// frame.render_widget(Paragraph::new("Hello World!"), area);
|
||||
/// })?;
|
||||
/// Ok(())
|
||||
/// })?;
|
||||
/// ```
|
||||
///
|
||||
/// ## Manual construction (for fine-grained control)
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// use std::io::stdout;
|
||||
///
|
||||
/// use ratatui::{backend::CrosstermBackend, widgets::Paragraph, Terminal};
|
||||
///
|
||||
/// 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 {
|
||||
#[allow(unused_variables)]
|
||||
if let Err(err) = self.show_cursor() {
|
||||
#[cfg(feature = "std")]
|
||||
std::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,ignore
|
||||
/// use std::io::stdout;
|
||||
///
|
||||
/// use ratatui::{backend::CrosstermBackend, Terminal};
|
||||
///
|
||||
/// let backend = CrosstermBackend::new(stdout());
|
||||
/// let terminal = Terminal::new(backend)?;
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
pub fn new(backend: B) -> Result<Self, B::Error> {
|
||||
Self::with_options(
|
||||
backend,
|
||||
TerminalOptions {
|
||||
viewport: Viewport::Fullscreen,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a new [`Terminal`] with the given [`Backend`] and [`TerminalOptions`].
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// use std::io::stdout;
|
||||
///
|
||||
/// use ratatui::{backend::CrosstermBackend, layout::Rect, Terminal, TerminalOptions, Viewport};
|
||||
///
|
||||
/// 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) -> Result<Self, B::Error> {
|
||||
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 const 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 const 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 const 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) -> Result<(), B::Error> {
|
||||
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) -> Result<(), B::Error> {
|
||||
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) -> Result<(), B::Error> {
|
||||
// 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 applications.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// # let backend = ratatui::backend::TestBackend::new(10, 10);
|
||||
/// # let mut terminal = ratatui::Terminal::new(backend)?;
|
||||
/// use ratatui::{layout::Position, 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) -> Result<CompletedFrame<'_>, B::Error>
|
||||
where
|
||||
F: FnOnce(&mut Frame),
|
||||
{
|
||||
self.try_draw(|frame| {
|
||||
render_callback(frame);
|
||||
Ok::<(), B::Error>(())
|
||||
})
|
||||
}
|
||||
|
||||
/// 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 applications.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// ```ignore
|
||||
/// # 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) -> Result<CompletedFrame<'_>, B::Error>
|
||||
where
|
||||
F: FnOnce(&mut Frame) -> Result<(), E>,
|
||||
E: Into<B::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) -> Result<(), B::Error> {
|
||||
self.backend.hide_cursor()?;
|
||||
self.hidden_cursor = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Shows the cursor.
|
||||
pub fn show_cursor(&mut self) -> Result<(), B::Error> {
|
||||
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 = "use `get_cursor_position()` instead which returns `Result<Position>`"]
|
||||
pub fn get_cursor(&mut self) -> Result<(u16, u16), B::Error> {
|
||||
let Position { x, y } = self.get_cursor_position()?;
|
||||
Ok((x, y))
|
||||
}
|
||||
|
||||
/// Sets the cursor position.
|
||||
#[deprecated = "use `set_cursor_position((x, y))` instead which takes `impl Into<Position>`"]
|
||||
pub fn set_cursor(&mut self, x: u16, y: u16) -> Result<(), B::Error> {
|
||||
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) -> Result<Position, B::Error> {
|
||||
self.backend.get_cursor_position()
|
||||
}
|
||||
|
||||
/// Sets the cursor position.
|
||||
pub fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> Result<(), B::Error> {
|
||||
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) -> Result<(), B::Error> {
|
||||
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(_) => {
|
||||
let area = self.viewport_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) -> Result<Size, B::Error> {
|
||||
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,ignore
|
||||
/// use ratatui::{
|
||||
/// backend::TestBackend,
|
||||
/// style::{Color, Style},
|
||||
/// text::{Line, Span},
|
||||
/// widgets::{Paragraph, Widget},
|
||||
/// Terminal,
|
||||
/// };
|
||||
/// # 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) -> Result<(), B::Error>
|
||||
where
|
||||
F: FnOnce(&mut Buffer),
|
||||
{
|
||||
match self.viewport {
|
||||
#[cfg(feature = "scrolling-regions")]
|
||||
Viewport::Inline(_) => self.insert_before_scrolling_regions(height, draw_fn),
|
||||
#[cfg(not(feature = "scrolling-regions"))]
|
||||
Viewport::Inline(_) => self.insert_before_no_scrolling_regions(height, draw_fn),
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Implement `Self::insert_before` using standard backend capabilities.
|
||||
#[cfg(not(feature = "scrolling-regions"))]
|
||||
fn insert_before_no_scrolling_regions(
|
||||
&mut self,
|
||||
height: u16,
|
||||
draw_fn: impl FnOnce(&mut Buffer),
|
||||
) -> Result<(), B::Error> {
|
||||
// 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(())
|
||||
}
|
||||
|
||||
/// Implement `Self::insert_before` using scrolling regions.
|
||||
///
|
||||
/// If a terminal supports scrolling regions, it means that we can define a subset of rows of
|
||||
/// the screen, and then tell the terminal to scroll up or down just within that region. The
|
||||
/// rows outside of the region are not affected.
|
||||
///
|
||||
/// This function utilizes this feature to avoid having to redraw the viewport. This is done
|
||||
/// either by splitting the screen at the top of the viewport, and then creating a gap by
|
||||
/// either scrolling the viewport down, or scrolling the area above it up. The lines to insert
|
||||
/// are then drawn into the gap created.
|
||||
#[cfg(feature = "scrolling-regions")]
|
||||
fn insert_before_scrolling_regions(
|
||||
&mut self,
|
||||
mut height: u16,
|
||||
draw_fn: impl FnOnce(&mut Buffer),
|
||||
) -> Result<(), B::Error> {
|
||||
// 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();
|
||||
|
||||
// Handle the special case where the viewport takes up the whole screen.
|
||||
if self.viewport_area.height == self.last_known_area.height {
|
||||
// "Borrow" the top line of the viewport. Draw over it, then immediately scroll it into
|
||||
// scrollback. Do this repeatedly until the whole buffer has been put into scrollback.
|
||||
let mut first = true;
|
||||
while !buffer.is_empty() {
|
||||
buffer = if first {
|
||||
self.draw_lines(0, 1, buffer)?
|
||||
} else {
|
||||
self.draw_lines_over_cleared(0, 1, buffer)?
|
||||
};
|
||||
first = false;
|
||||
self.backend.scroll_region_up(0..1, 1)?;
|
||||
}
|
||||
|
||||
// Redraw the top line of the viewport.
|
||||
let width = self.viewport_area.width as usize;
|
||||
let top_line = self.buffers[1 - self.current].content[0..width].to_vec();
|
||||
self.draw_lines_over_cleared(0, 1, &top_line)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Handle the case where the viewport isn't yet at the bottom of the screen.
|
||||
{
|
||||
let viewport_top = self.viewport_area.top();
|
||||
let viewport_bottom = self.viewport_area.bottom();
|
||||
let screen_bottom = self.last_known_area.bottom();
|
||||
if viewport_bottom < screen_bottom {
|
||||
let to_draw = height.min(screen_bottom - viewport_bottom);
|
||||
self.backend
|
||||
.scroll_region_down(viewport_top..viewport_bottom + to_draw, to_draw)?;
|
||||
buffer = self.draw_lines_over_cleared(viewport_top, to_draw, buffer)?;
|
||||
self.set_viewport_area(Rect {
|
||||
y: viewport_top + to_draw,
|
||||
..self.viewport_area
|
||||
});
|
||||
height -= to_draw;
|
||||
}
|
||||
}
|
||||
|
||||
let viewport_top = self.viewport_area.top();
|
||||
while height > 0 {
|
||||
let to_draw = height.min(viewport_top);
|
||||
self.backend.scroll_region_up(0..viewport_top, to_draw)?;
|
||||
buffer = self.draw_lines_over_cleared(viewport_top - to_draw, to_draw, buffer)?;
|
||||
height -= to_draw;
|
||||
}
|
||||
|
||||
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],
|
||||
) -> Result<&'a [Cell], B::Error> {
|
||||
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)
|
||||
}
|
||||
|
||||
/// Draw lines at the given vertical offset, assuming that the lines they are replacing on the
|
||||
/// screen are cleared. The slice of cells must contain enough cells for the requested lines. A
|
||||
/// slice of the unused cells are returned.
|
||||
#[cfg(feature = "scrolling-regions")]
|
||||
fn draw_lines_over_cleared<'a>(
|
||||
&mut self,
|
||||
y_offset: u16,
|
||||
lines_to_draw: u16,
|
||||
cells: &'a [Cell],
|
||||
) -> Result<&'a [Cell], B::Error> {
|
||||
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 area = Rect::new(0, y_offset, width as u16, y_offset + lines_to_draw);
|
||||
let old = Buffer::empty(area);
|
||||
let new = Buffer {
|
||||
area,
|
||||
content: to_draw.to_vec(),
|
||||
};
|
||||
self.backend.draw(old.diff(&new).into_iter())?;
|
||||
self.backend.flush()?;
|
||||
}
|
||||
Ok(remainder)
|
||||
}
|
||||
|
||||
/// Scroll the whole screen up by the given number of lines.
|
||||
#[cfg(not(feature = "scrolling-regions"))]
|
||||
fn scroll_up(&mut self, lines_to_scroll: u16) -> Result<(), B::Error> {
|
||||
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,
|
||||
) -> Result<(Rect, Position), B::Error> {
|
||||
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,
|
||||
))
|
||||
}
|
||||
@@ -2,67 +2,31 @@ use core::fmt;
|
||||
|
||||
use crate::layout::Rect;
|
||||
|
||||
/// The area of the terminal that Ratatui draws into.
|
||||
/// Represents the viewport of the terminal. The viewport is the area of the terminal that is
|
||||
/// currently visible to the user. It can be either fullscreen, inline or fixed.
|
||||
///
|
||||
/// A [`Viewport`] controls where widgets render and what [`Frame::area`] returns.
|
||||
/// When the viewport is fullscreen, the whole terminal is used to draw the application.
|
||||
///
|
||||
/// For a higher-level overview of viewports in the context of an application (including
|
||||
/// examples), see [`Terminal`].
|
||||
/// When the viewport is inline, it is drawn inline with the rest of the terminal. The height of
|
||||
/// the viewport is fixed, but the width is the same as the terminal width.
|
||||
///
|
||||
/// Most applications use [`Viewport::Fullscreen`]. Use [`Viewport::Inline`] when you want to embed
|
||||
/// a UI into a larger CLI flow (for example: print some text, then start an interactive UI below
|
||||
/// it). Use [`Viewport::Fixed`] when you want Ratatui to render into a specific region of the
|
||||
/// terminal.
|
||||
/// When the viewport is fixed, it is drawn in a fixed area of the terminal. The area is specified
|
||||
/// by a [`Rect`].
|
||||
///
|
||||
/// In fullscreen mode, the viewport starts at (0, 0). In inline and fixed mode, the viewport may
|
||||
/// have a non-zero `x`/`y` origin; prefer using `Frame::area()` as your root layout rectangle.
|
||||
/// See [`Terminal::with_options`] for more information.
|
||||
///
|
||||
/// See [`Terminal::with_options`] for how to select a viewport, and [`Terminal::resize`] /
|
||||
/// [`Terminal::autoresize`] for resize behavior.
|
||||
///
|
||||
/// [`Frame::area`]: crate::terminal::Frame::area
|
||||
/// [`Terminal`]: crate::terminal::Terminal
|
||||
/// [`Terminal::with_options`]: crate::terminal::Terminal::with_options
|
||||
/// [`Terminal::resize`]: crate::terminal::Terminal::resize
|
||||
/// [`Terminal::autoresize`]: crate::terminal::Terminal::autoresize
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum Viewport {
|
||||
/// Draw into the entire terminal.
|
||||
///
|
||||
/// This is the default viewport used by [`Terminal::new`].
|
||||
///
|
||||
/// When the terminal size changes, Ratatui automatically resizes internal buffers during
|
||||
/// [`Terminal::draw`].
|
||||
///
|
||||
/// `Frame::area()` always starts at (0, 0).
|
||||
///
|
||||
/// [`Terminal::new`]: crate::terminal::Terminal::new
|
||||
/// [`Terminal::draw`]: crate::terminal::Terminal::draw
|
||||
/// The viewport is fullscreen
|
||||
#[default]
|
||||
Fullscreen,
|
||||
/// Draw the application inline with the rest of the terminal output.
|
||||
/// The viewport is inline with the rest of the terminal.
|
||||
///
|
||||
/// The viewport spans the full terminal width and its top-left corner is anchored to column 0
|
||||
/// of the current cursor row when the terminal is created (and when it is resized). Ratatui
|
||||
/// reserves space for the requested height; if the cursor is near the bottom of the screen,
|
||||
/// this may scroll the terminal so the viewport remains fully visible.
|
||||
///
|
||||
/// The height is specified in rows and is clamped to the current terminal height.
|
||||
/// The viewport's height is fixed and specified in number of lines. The width is the same as
|
||||
/// the terminal's width. The viewport is drawn below the cursor position.
|
||||
Inline(u16),
|
||||
/// Draw into a fixed region of the terminal.
|
||||
///
|
||||
/// This can be useful when Ratatui is responsible for only part of the screen (for example, a
|
||||
/// status panel beside another renderer), or when you want to manage the overall layout
|
||||
/// yourself.
|
||||
///
|
||||
/// Fixed viewports are not automatically resized. If the region should change (for example, on
|
||||
/// terminal resize), call [`Terminal::resize`] yourself.
|
||||
///
|
||||
/// The area is specified as a [`Rect`] in terminal coordinates.
|
||||
///
|
||||
/// `Frame::area()` returns this rectangle as-is (including its `x`/`y` offset).
|
||||
///
|
||||
/// [`Terminal::resize`]: crate::terminal::Terminal::resize
|
||||
/// The viewport is drawn in a fixed area of the terminal. The area is specified by a [`Rect`].
|
||||
Fixed(Rect),
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ use alloc::vec::Vec;
|
||||
use core::fmt;
|
||||
|
||||
use unicode_truncate::UnicodeTruncateStr;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::buffer::Buffer;
|
||||
use crate::layout::{Alignment, Rect};
|
||||
@@ -436,9 +435,8 @@ impl<'a> Line<'a> {
|
||||
/// let line = Line::from(vec!["Hello".blue(), " world!".green()]);
|
||||
/// assert_eq!(12, line.width());
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn width(&self) -> usize {
|
||||
UnicodeWidthStr::width(self)
|
||||
self.spans.iter().map(Span::width).sum()
|
||||
}
|
||||
|
||||
/// Returns an iterator over the graphemes held by this line.
|
||||
@@ -564,16 +562,6 @@ impl<'a> Line<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl UnicodeWidthStr for Line<'_> {
|
||||
fn width(&self) -> usize {
|
||||
self.spans.iter().map(UnicodeWidthStr::width).sum()
|
||||
}
|
||||
|
||||
fn width_cjk(&self) -> usize {
|
||||
self.spans.iter().map(UnicodeWidthStr::width_cjk).sum()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for Line<'a> {
|
||||
type Item = Span<'a>;
|
||||
type IntoIter = alloc::vec::IntoIter<Span<'a>>;
|
||||
@@ -803,7 +791,7 @@ 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` shouldn't be implemented directly: [`Display`] should be implemented instead, and
|
||||
/// such, `ToLine` shouln't be implemented directly: [`Display`] should be implemented instead, and
|
||||
/// you get the `ToLine` implementation for free.
|
||||
///
|
||||
/// [`Display`]: std::fmt::Display
|
||||
|
||||
@@ -269,7 +269,7 @@ impl<'a> Span<'a> {
|
||||
|
||||
/// Returns the unicode width of the content held by this span.
|
||||
pub fn width(&self) -> usize {
|
||||
UnicodeWidthStr::width(self)
|
||||
self.content.width()
|
||||
}
|
||||
|
||||
/// Returns an iterator over the graphemes held by this span.
|
||||
@@ -376,16 +376,6 @@ impl<'a> Span<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl UnicodeWidthStr for Span<'_> {
|
||||
fn width(&self) -> usize {
|
||||
self.content.width()
|
||||
}
|
||||
|
||||
fn width_cjk(&self) -> usize {
|
||||
self.content.width_cjk()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> From<T> for Span<'a>
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
@@ -474,7 +464,7 @@ impl Widget for &Span<'_> {
|
||||
/// 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` shouldn't be implemented directly: [`Display`] should be implemented instead, and
|
||||
/// such, `ToSpan` shouln't be implemented directly: [`Display`] should be implemented instead, and
|
||||
/// you get the `ToSpan` implementation for free.
|
||||
///
|
||||
/// [`Display`]: std::fmt::Display
|
||||
|
||||
@@ -5,8 +5,6 @@ use alloc::vec;
|
||||
use alloc::vec::Vec;
|
||||
use core::fmt;
|
||||
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::buffer::Buffer;
|
||||
use crate::layout::{Alignment, Rect};
|
||||
use crate::style::{Style, Styled};
|
||||
@@ -286,7 +284,7 @@ impl<'a> Text<'a> {
|
||||
/// assert_eq!(15, text.width());
|
||||
/// ```
|
||||
pub fn width(&self) -> usize {
|
||||
UnicodeWidthStr::width(self)
|
||||
self.iter().map(Line::width).max().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Returns the height.
|
||||
@@ -561,25 +559,6 @@ impl<'a> Text<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl UnicodeWidthStr for Text<'_> {
|
||||
/// Returns the max width of all the lines.
|
||||
fn width(&self) -> usize {
|
||||
self.lines
|
||||
.iter()
|
||||
.map(UnicodeWidthStr::width)
|
||||
.max()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn width_cjk(&self) -> usize {
|
||||
self.lines
|
||||
.iter()
|
||||
.map(UnicodeWidthStr::width_cjk)
|
||||
.max()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for Text<'a> {
|
||||
type Item = Line<'a>;
|
||||
type IntoIter = alloc::vec::IntoIter<Self::Item>;
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
//! Integration tests for Rect operations visualized with buffers.
|
||||
|
||||
use ratatui_core::buffer::Buffer;
|
||||
use ratatui_core::layout::{Margin, Offset, Rect};
|
||||
use ratatui_core::widgets::Widget;
|
||||
|
||||
/// A minimal widget that fills its entire area with the given symbol.
|
||||
struct Filled<'a> {
|
||||
symbol: &'a str,
|
||||
}
|
||||
|
||||
impl Widget for Filled<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
for y in area.top()..area.bottom() {
|
||||
for x in area.left()..area.right() {
|
||||
if let Some(cell) = buf.cell_mut((x, y)) {
|
||||
cell.set_symbol(self.symbol);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inner() {
|
||||
let base = Rect::new(2, 2, 10, 6);
|
||||
let inner = base.inner(Margin::new(2, 1));
|
||||
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 10));
|
||||
Filled { symbol: "█" }.render(base, &mut buf);
|
||||
Filled { symbol: "░" }.render(inner, &mut buf);
|
||||
|
||||
let expected = Buffer::with_lines([
|
||||
" ",
|
||||
" ",
|
||||
" ██████████ ",
|
||||
" ██░░░░░░██ ",
|
||||
" ██░░░░░░██ ",
|
||||
" ██░░░░░░██ ",
|
||||
" ██░░░░░░██ ",
|
||||
" ██████████ ",
|
||||
" ",
|
||||
" ",
|
||||
]);
|
||||
assert_eq!(buf, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outer() {
|
||||
let base = Rect::new(4, 3, 6, 4);
|
||||
let outer = base.outer(Margin::new(2, 1));
|
||||
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 10));
|
||||
Filled { symbol: "░" }.render(outer, &mut buf);
|
||||
Filled { symbol: "█" }.render(base, &mut buf);
|
||||
|
||||
let expected = Buffer::with_lines([
|
||||
" ",
|
||||
" ",
|
||||
" ░░░░░░░░░░ ",
|
||||
" ░░██████░░ ",
|
||||
" ░░██████░░ ",
|
||||
" ░░██████░░ ",
|
||||
" ░░██████░░ ",
|
||||
" ░░░░░░░░░░ ",
|
||||
" ",
|
||||
" ",
|
||||
]);
|
||||
assert_eq!(buf, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offset() {
|
||||
let base = Rect::new(2, 2, 5, 3);
|
||||
let moved = base + Offset::new(4, 2);
|
||||
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 10));
|
||||
Filled { symbol: "░" }.render(base, &mut buf);
|
||||
Filled { symbol: "█" }.render(moved, &mut buf);
|
||||
|
||||
let expected = Buffer::with_lines([
|
||||
" ",
|
||||
" ",
|
||||
" ░░░░░ ",
|
||||
" ░░░░░ ",
|
||||
" ░░░░█████ ",
|
||||
" █████ ",
|
||||
" █████ ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
]);
|
||||
assert_eq!(buf, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersection() {
|
||||
let a = Rect::new(2, 2, 6, 4);
|
||||
let b = Rect::new(5, 3, 6, 4);
|
||||
let inter = a.intersection(b);
|
||||
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 10));
|
||||
Filled { symbol: "░" }.render(a, &mut buf);
|
||||
Filled { symbol: "▒" }.render(b, &mut buf);
|
||||
Filled { symbol: "█" }.render(inter, &mut buf);
|
||||
|
||||
let expected = Buffer::with_lines([
|
||||
" ",
|
||||
" ",
|
||||
" ░░░░░░ ",
|
||||
" ░░░███▒▒▒ ",
|
||||
" ░░░███▒▒▒ ",
|
||||
" ░░░███▒▒▒ ",
|
||||
" ▒▒▒▒▒▒ ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
]);
|
||||
assert_eq!(buf, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clamp() {
|
||||
let area = Rect::new(2, 2, 10, 6);
|
||||
let rect = Rect::new(8, 5, 8, 4);
|
||||
let clamped = rect.clamp(area);
|
||||
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 12));
|
||||
Filled { symbol: "█" }.render(area, &mut buf);
|
||||
Filled { symbol: "▒" }.render(rect, &mut buf);
|
||||
Filled { symbol: "░" }.render(clamped, &mut buf);
|
||||
|
||||
let expected = Buffer::with_lines([
|
||||
" ",
|
||||
" ",
|
||||
" ██████████ ",
|
||||
" ██████████ ",
|
||||
" ██░░░░░░░░ ",
|
||||
" ██░░░░░░░░▒▒▒▒ ",
|
||||
" ██░░░░░░░░▒▒▒▒ ",
|
||||
" ██░░░░░░░░▒▒▒▒ ",
|
||||
" ▒▒▒▒▒▒▒▒ ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
]);
|
||||
assert_eq!(buf, expected);
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "ratatui-crossterm"
|
||||
version = "0.1.0"
|
||||
version = "0.1.0-alpha.6"
|
||||
description = "Crossterm backend for the Ratatui Terminal UI library."
|
||||
documentation = "https://docs.rs/ratatui-crossterm"
|
||||
documentation = "https://docs.rs/ratatui-crossterm/"
|
||||
readme = "README.md"
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
@@ -14,31 +14,11 @@ exclude.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
features = ["document-features"]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[features]
|
||||
default = ["underline-color", "crossterm_0_29"]
|
||||
|
||||
#! One of the following versions of crossterm must be enabled. By default, the latest version is
|
||||
#! enabled. This will always default to the most recent version of crossterm, but you can override
|
||||
#! this by specifying the version you want to use in your Cargo.toml file. The purpose of these
|
||||
#! features is to allow widget libraries to depend on specific versions of crossterm in a way that
|
||||
#! doesn't cause version conflicts. A compiler error will be raised if you don't enable at least one
|
||||
#! of the following features.
|
||||
|
||||
## Enables crossterm 0.28.x
|
||||
crossterm_0_28 = ["dep:crossterm_0_28"]
|
||||
## Enables crossterm 0.29.x
|
||||
crossterm_0_29 = ["dep:crossterm_0_29"]
|
||||
|
||||
#! The following features are optional and can be enabled to add additional functionality to the
|
||||
#! library. These features are not required for the library to function, but they may be useful in
|
||||
#! certain situations.
|
||||
default = ["underline-color"]
|
||||
|
||||
## Enables serde for crossterm dependency
|
||||
serde = ["crossterm_0_28?/serde", "crossterm_0_29?/serde"]
|
||||
serde = ["crossterm/serde"]
|
||||
|
||||
## enables the backend code that sets the underline color.
|
||||
## Underline color is not supported on Windows 7.
|
||||
@@ -57,14 +37,13 @@ unstable-backend-writer = []
|
||||
|
||||
|
||||
[dependencies]
|
||||
cfg-if = "1.0.1"
|
||||
crossterm_0_28 = { version = "0.28", package = "crossterm", optional = true }
|
||||
crossterm_0_29 = { version = "0.29", package = "crossterm", optional = true }
|
||||
crossterm.workspace = true
|
||||
document-features = { workspace = true, optional = true }
|
||||
instability.workspace = true
|
||||
ratatui-core.workspace = true
|
||||
ratatui-core = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
ratatui = { path = "../ratatui", features = ["crossterm"] }
|
||||
rstest.workspace = true
|
||||
|
||||
[lints]
|
||||
|
||||
@@ -2,41 +2,10 @@
|
||||
|
||||
<!-- cargo-rdme start -->
|
||||
|
||||
This crate provides [`CrosstermBackend`], an implementation of the [`Backend`] trait for the
|
||||
[Ratatui] library. It uses the [Crossterm] library for all terminal manipulation.
|
||||
This module provides the [`CrosstermBackend`] implementation for the [`Backend`] trait. It uses
|
||||
the [Crossterm] crate to interact with the terminal.
|
||||
|
||||
### Crossterm Version and Re-export
|
||||
|
||||
`ratatui-crossterm` requires you to specify a version of the [Crossterm] library to be used.
|
||||
This is managed via feature flags. The highest enabled feature flag of the available
|
||||
`crossterm_0_xx` features (e.g., `crossterm_0_28`, `crossterm_0_29`) takes precedence. These
|
||||
features determine which version of Crossterm is compiled and used by the backend. Feature
|
||||
unification may mean that any crate in your dependency graph that chooses to depend on a
|
||||
specific version of Crossterm may be affected by the feature flags you enable.
|
||||
|
||||
Ratatui will support at least the two most recent versions of Crossterm (though we may increase
|
||||
this if crossterm release cadence increases). We will remove support for older versions in major
|
||||
(0.x) releases of `ratatui-crossterm`, and we may add support for newer versions in minor
|
||||
(0.x.y) releases.
|
||||
|
||||
To promote interoperability within the [Ratatui] ecosystem, the selected Crossterm crate is
|
||||
re-exported as `ratatui_crossterm::crossterm`. This re-export is essential for authors of widget
|
||||
libraries or any applications that need to perform direct Crossterm operations while ensuring
|
||||
compatibility with the version used by `ratatui-crossterm`. By using
|
||||
`ratatui_crossterm::crossterm` for such operations, developers can avoid version conflicts and
|
||||
ensure that all parts of their application use a consistent set of Crossterm types and
|
||||
functions.
|
||||
|
||||
For example, if your application's `Cargo.toml` enables the `crossterm_0_29` feature for
|
||||
`ratatui-crossterm`, then any code using `ratatui_crossterm::crossterm` will refer to the 0.29
|
||||
version of Crossterm.
|
||||
|
||||
For more information on how to use the backend, see the documentation for the
|
||||
[`CrosstermBackend`] struct.
|
||||
|
||||
[Ratatui]: https://ratatui.rs
|
||||
[Crossterm]: https://crates.io/crates/crossterm
|
||||
[`Backend`]: ratatui_core::backend::Backend
|
||||
|
||||
## Crate Organization
|
||||
|
||||
|
||||
@@ -1,45 +1,15 @@
|
||||
// show the feature flags in the generated documentation
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![doc(
|
||||
html_logo_url = "https://raw.githubusercontent.com/ratatui/ratatui/main/assets/logo.png",
|
||||
html_favicon_url = "https://raw.githubusercontent.com/ratatui/ratatui/main/assets/favicon.ico"
|
||||
)]
|
||||
#![warn(missing_docs)]
|
||||
//! This crate provides [`CrosstermBackend`], an implementation of the [`Backend`] trait for the
|
||||
//! [Ratatui] library. It uses the [Crossterm] library for all terminal manipulation.
|
||||
//! This module provides the [`CrosstermBackend`] implementation for the [`Backend`] trait. It uses
|
||||
//! the [Crossterm] crate to interact with the terminal.
|
||||
//!
|
||||
//! ## Crossterm Version and Re-export
|
||||
//!
|
||||
//! `ratatui-crossterm` requires you to specify a version of the [Crossterm] library to be used.
|
||||
//! This is managed via feature flags. The highest enabled feature flag of the available
|
||||
//! `crossterm_0_xx` features (e.g., `crossterm_0_28`, `crossterm_0_29`) takes precedence. These
|
||||
//! features determine which version of Crossterm is compiled and used by the backend. Feature
|
||||
//! unification may mean that any crate in your dependency graph that chooses to depend on a
|
||||
//! specific version of Crossterm may be affected by the feature flags you enable.
|
||||
//!
|
||||
//! Ratatui will support at least the two most recent versions of Crossterm (though we may increase
|
||||
//! this if crossterm release cadence increases). We will remove support for older versions in major
|
||||
//! (0.x) releases of `ratatui-crossterm`, and we may add support for newer versions in minor
|
||||
//! (0.x.y) releases.
|
||||
//!
|
||||
//! To promote interoperability within the [Ratatui] ecosystem, the selected Crossterm crate is
|
||||
//! re-exported as `ratatui_crossterm::crossterm`. This re-export is essential for authors of widget
|
||||
//! libraries or any applications that need to perform direct Crossterm operations while ensuring
|
||||
//! compatibility with the version used by `ratatui-crossterm`. By using
|
||||
//! `ratatui_crossterm::crossterm` for such operations, developers can avoid version conflicts and
|
||||
//! ensure that all parts of their application use a consistent set of Crossterm types and
|
||||
//! functions.
|
||||
//!
|
||||
//! For example, if your application's `Cargo.toml` enables the `crossterm_0_29` feature for
|
||||
//! `ratatui-crossterm`, then any code using `ratatui_crossterm::crossterm` will refer to the 0.29
|
||||
//! version of Crossterm.
|
||||
//!
|
||||
//! For more information on how to use the backend, see the documentation for the
|
||||
//! [`CrosstermBackend`] struct.
|
||||
//!
|
||||
//! [Ratatui]: https://ratatui.rs
|
||||
//! [Crossterm]: https://crates.io/crates/crossterm
|
||||
//! [`Backend`]: ratatui_core::backend::Backend
|
||||
//!
|
||||
//! # Crate Organization
|
||||
//!
|
||||
@@ -66,6 +36,7 @@
|
||||
|
||||
use std::io::{self, Write};
|
||||
|
||||
pub use crossterm;
|
||||
use crossterm::cursor::{Hide, MoveTo, Show};
|
||||
#[cfg(feature = "underline-color")]
|
||||
use crossterm::style::SetUnderlineColor;
|
||||
@@ -76,19 +47,6 @@ use crossterm::style::{
|
||||
};
|
||||
use crossterm::terminal::{self, Clear};
|
||||
use crossterm::{execute, queue};
|
||||
cfg_if::cfg_if! {
|
||||
// Re-export the selected Crossterm crate making sure to choose the latest version. We do this
|
||||
// to make it possible to easily enable all features when compiling `ratatui-crossterm`.
|
||||
if #[cfg(feature = "crossterm_0_29")] {
|
||||
pub use crossterm_0_29 as crossterm;
|
||||
} else if #[cfg(feature = "crossterm_0_28")] {
|
||||
pub use crossterm_0_28 as crossterm;
|
||||
} else {
|
||||
compile_error!(
|
||||
"At least one crossterm feature must be enabled. See the crate docs for more information."
|
||||
);
|
||||
}
|
||||
}
|
||||
use ratatui_core::backend::{Backend, ClearType, WindowSize};
|
||||
use ratatui_core::buffer::Cell;
|
||||
use ratatui_core::layout::{Position, Size};
|
||||
@@ -112,7 +70,7 @@ use ratatui_core::style::{Color, Modifier, Style};
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// ```rust,no_run
|
||||
/// use std::io::{stderr, stdout};
|
||||
///
|
||||
/// use crossterm::ExecutableCommand;
|
||||
@@ -168,7 +126,7 @@ where
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// ```rust,no_run
|
||||
/// use std::io::stdout;
|
||||
///
|
||||
/// use ratatui::backend::CrosstermBackend;
|
||||
@@ -423,79 +381,6 @@ impl IntoCrossterm<CrosstermColor> for Color {
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoCrossterm<ContentStyle> for Style {
|
||||
fn into_crossterm(self) -> ContentStyle {
|
||||
let mut attributes = CrosstermAttributes::default();
|
||||
|
||||
// Add modifiers
|
||||
if self.add_modifier.contains(Modifier::BOLD) {
|
||||
attributes.set(CrosstermAttribute::Bold);
|
||||
}
|
||||
if self.add_modifier.contains(Modifier::DIM) {
|
||||
attributes.set(CrosstermAttribute::Dim);
|
||||
}
|
||||
if self.add_modifier.contains(Modifier::ITALIC) {
|
||||
attributes.set(CrosstermAttribute::Italic);
|
||||
}
|
||||
if self.add_modifier.contains(Modifier::UNDERLINED) {
|
||||
attributes.set(CrosstermAttribute::Underlined);
|
||||
}
|
||||
if self.add_modifier.contains(Modifier::SLOW_BLINK) {
|
||||
attributes.set(CrosstermAttribute::SlowBlink);
|
||||
}
|
||||
if self.add_modifier.contains(Modifier::RAPID_BLINK) {
|
||||
attributes.set(CrosstermAttribute::RapidBlink);
|
||||
}
|
||||
if self.add_modifier.contains(Modifier::REVERSED) {
|
||||
attributes.set(CrosstermAttribute::Reverse);
|
||||
}
|
||||
if self.add_modifier.contains(Modifier::HIDDEN) {
|
||||
attributes.set(CrosstermAttribute::Hidden);
|
||||
}
|
||||
if self.add_modifier.contains(Modifier::CROSSED_OUT) {
|
||||
attributes.set(CrosstermAttribute::CrossedOut);
|
||||
}
|
||||
|
||||
// Sub modifiers (remove modifiers)
|
||||
if self.sub_modifier.contains(Modifier::BOLD) {
|
||||
attributes.set(CrosstermAttribute::NoBold);
|
||||
}
|
||||
if self.sub_modifier.contains(Modifier::DIM) {
|
||||
attributes.set(CrosstermAttribute::NormalIntensity);
|
||||
}
|
||||
if self.sub_modifier.contains(Modifier::ITALIC) {
|
||||
attributes.set(CrosstermAttribute::NoItalic);
|
||||
}
|
||||
if self.sub_modifier.contains(Modifier::UNDERLINED) {
|
||||
attributes.set(CrosstermAttribute::NoUnderline);
|
||||
}
|
||||
if self.sub_modifier.contains(Modifier::SLOW_BLINK)
|
||||
|| self.sub_modifier.contains(Modifier::RAPID_BLINK)
|
||||
{
|
||||
attributes.set(CrosstermAttribute::NoBlink);
|
||||
}
|
||||
if self.sub_modifier.contains(Modifier::REVERSED) {
|
||||
attributes.set(CrosstermAttribute::NoReverse);
|
||||
}
|
||||
if self.sub_modifier.contains(Modifier::HIDDEN) {
|
||||
attributes.set(CrosstermAttribute::NoHidden);
|
||||
}
|
||||
if self.sub_modifier.contains(Modifier::CROSSED_OUT) {
|
||||
attributes.set(CrosstermAttribute::NotCrossedOut);
|
||||
}
|
||||
|
||||
ContentStyle {
|
||||
foreground_color: self.fg.map(IntoCrossterm::into_crossterm),
|
||||
background_color: self.bg.map(IntoCrossterm::into_crossterm),
|
||||
#[cfg(feature = "underline-color")]
|
||||
underline_color: self.underline_color.map(IntoCrossterm::into_crossterm),
|
||||
#[cfg(not(feature = "underline-color"))]
|
||||
underline_color: None,
|
||||
attributes,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromCrossterm<CrosstermColor> for Color {
|
||||
fn from_crossterm(value: CrosstermColor) -> Self {
|
||||
match value {
|
||||
@@ -949,176 +834,4 @@ mod tests {
|
||||
Style::default().underline_color(Color::Red)
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(Style::default(), ContentStyle::default())]
|
||||
#[case(
|
||||
Style::default().fg(Color::Yellow),
|
||||
ContentStyle {
|
||||
foreground_color: Some(CrosstermColor::DarkYellow),
|
||||
..Default::default()
|
||||
}
|
||||
)]
|
||||
#[case(
|
||||
Style::default().bg(Color::Yellow),
|
||||
ContentStyle {
|
||||
background_color: Some(CrosstermColor::DarkYellow),
|
||||
..Default::default()
|
||||
}
|
||||
)]
|
||||
#[case(
|
||||
Style::default().add_modifier(Modifier::BOLD),
|
||||
ContentStyle {
|
||||
attributes: CrosstermAttributes::from(CrosstermAttribute::Bold),
|
||||
..Default::default()
|
||||
}
|
||||
)]
|
||||
#[case(
|
||||
Style::default().remove_modifier(Modifier::BOLD),
|
||||
ContentStyle {
|
||||
attributes: CrosstermAttributes::from(CrosstermAttribute::NoBold),
|
||||
..Default::default()
|
||||
}
|
||||
)]
|
||||
#[case(
|
||||
Style::default().add_modifier(Modifier::ITALIC),
|
||||
ContentStyle {
|
||||
attributes: CrosstermAttributes::from(CrosstermAttribute::Italic),
|
||||
..Default::default()
|
||||
}
|
||||
)]
|
||||
#[case(
|
||||
Style::default().remove_modifier(Modifier::ITALIC),
|
||||
ContentStyle {
|
||||
attributes: CrosstermAttributes::from(CrosstermAttribute::NoItalic),
|
||||
..Default::default()
|
||||
}
|
||||
)]
|
||||
#[case(
|
||||
Style::default().add_modifier(Modifier::UNDERLINED),
|
||||
ContentStyle {
|
||||
attributes: CrosstermAttributes::from(CrosstermAttribute::Underlined),
|
||||
..Default::default()
|
||||
}
|
||||
)]
|
||||
#[case(
|
||||
Style::default().remove_modifier(Modifier::UNDERLINED),
|
||||
ContentStyle {
|
||||
attributes: CrosstermAttributes::from(CrosstermAttribute::NoUnderline),
|
||||
..Default::default()
|
||||
}
|
||||
)]
|
||||
#[case(
|
||||
Style::default().add_modifier(Modifier::DIM),
|
||||
ContentStyle {
|
||||
attributes: CrosstermAttributes::from(CrosstermAttribute::Dim),
|
||||
..Default::default()
|
||||
}
|
||||
)]
|
||||
#[case(
|
||||
Style::default().remove_modifier(Modifier::DIM),
|
||||
ContentStyle {
|
||||
attributes: CrosstermAttributes::from(CrosstermAttribute::NormalIntensity),
|
||||
..Default::default()
|
||||
}
|
||||
)]
|
||||
#[case(
|
||||
Style::default().add_modifier(Modifier::SLOW_BLINK),
|
||||
ContentStyle {
|
||||
attributes: CrosstermAttributes::from(CrosstermAttribute::SlowBlink),
|
||||
..Default::default()
|
||||
}
|
||||
)]
|
||||
#[case(
|
||||
Style::default().add_modifier(Modifier::RAPID_BLINK),
|
||||
ContentStyle {
|
||||
attributes: CrosstermAttributes::from(CrosstermAttribute::RapidBlink),
|
||||
..Default::default()
|
||||
}
|
||||
)]
|
||||
#[case(
|
||||
Style::default().remove_modifier(Modifier::SLOW_BLINK),
|
||||
ContentStyle {
|
||||
attributes: CrosstermAttributes::from(CrosstermAttribute::NoBlink),
|
||||
..Default::default()
|
||||
}
|
||||
)]
|
||||
#[case(
|
||||
Style::default().add_modifier(Modifier::REVERSED),
|
||||
ContentStyle {
|
||||
attributes: CrosstermAttributes::from(CrosstermAttribute::Reverse),
|
||||
..Default::default()
|
||||
}
|
||||
)]
|
||||
#[case(
|
||||
Style::default().remove_modifier(Modifier::REVERSED),
|
||||
ContentStyle {
|
||||
attributes: CrosstermAttributes::from(CrosstermAttribute::NoReverse),
|
||||
..Default::default()
|
||||
}
|
||||
)]
|
||||
#[case(
|
||||
Style::default().add_modifier(Modifier::HIDDEN),
|
||||
ContentStyle {
|
||||
attributes: CrosstermAttributes::from(CrosstermAttribute::Hidden),
|
||||
..Default::default()
|
||||
}
|
||||
)]
|
||||
#[case(
|
||||
Style::default().remove_modifier(Modifier::HIDDEN),
|
||||
ContentStyle {
|
||||
attributes: CrosstermAttributes::from(CrosstermAttribute::NoHidden),
|
||||
..Default::default()
|
||||
}
|
||||
)]
|
||||
#[case(
|
||||
Style::default().add_modifier(Modifier::CROSSED_OUT),
|
||||
ContentStyle {
|
||||
attributes: CrosstermAttributes::from(CrosstermAttribute::CrossedOut),
|
||||
..Default::default()
|
||||
}
|
||||
)]
|
||||
#[case(
|
||||
Style::default().remove_modifier(Modifier::CROSSED_OUT),
|
||||
ContentStyle {
|
||||
attributes: CrosstermAttributes::from(CrosstermAttribute::NotCrossedOut),
|
||||
..Default::default()
|
||||
}
|
||||
)]
|
||||
#[case(
|
||||
Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.add_modifier(Modifier::ITALIC),
|
||||
ContentStyle {
|
||||
attributes: CrosstermAttributes::from(
|
||||
[CrosstermAttribute::Bold, CrosstermAttribute::Italic].as_ref()
|
||||
),
|
||||
..Default::default()
|
||||
}
|
||||
)]
|
||||
#[case(
|
||||
Style::default()
|
||||
.remove_modifier(Modifier::BOLD)
|
||||
.remove_modifier(Modifier::ITALIC),
|
||||
ContentStyle {
|
||||
attributes: CrosstermAttributes::from(
|
||||
[CrosstermAttribute::NoBold, CrosstermAttribute::NoItalic].as_ref()
|
||||
),
|
||||
..Default::default()
|
||||
}
|
||||
)]
|
||||
fn into_crossterm_content_style(#[case] style: Style, #[case] content_style: ContentStyle) {
|
||||
assert_eq!(style.into_crossterm(), content_style);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "underline-color")]
|
||||
fn into_crossterm_content_style_underline() {
|
||||
let style = Style::default().underline_color(Color::Red);
|
||||
let content_style = ContentStyle {
|
||||
underline_color: Some(CrosstermColor::DarkRed),
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(style.into_crossterm(), content_style);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
# Changelog
|
||||
|
||||
See the [top-level changelog](../CHANGELOG.md) for the latest changes.
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
This file is obsolete as of 0.30.0 release. [\*](https://github.com/ratatui/ratatui/pull/1652)
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.6.0](https://github.com/ratatui/ratatui-macros/compare/v0.5.0...v0.6.0) - 2024-10-21
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
[package]
|
||||
name = "ratatui-macros"
|
||||
version = "0.7.0"
|
||||
description = "Macros for Ratatui"
|
||||
version = "0.7.0-alpha.5"
|
||||
edition.workspace = true
|
||||
authors = ["The Ratatui Developers"]
|
||||
description = "Macros for Ratatui"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/ratatui/ratatui"
|
||||
documentation = "https://docs.rs/ratatui-macros"
|
||||
|
||||
@@ -9,9 +9,9 @@ error: No rules expected the token `,` while trying to match the end of the macr
|
||||
error: unexpected end of macro invocation
|
||||
--> tests/ui/fails.rs:8:18
|
||||
|
|
||||
8 | let [a, b] = constraints![
|
||||
8 | let [a, b] = constraints![
|
||||
| __________________^
|
||||
9 | | == 1/2,
|
||||
9 | | == 1/2,
|
||||
10 | | == 2,
|
||||
11 | | ];
|
||||
| |_____^ missing tokens in macro arguments
|
||||
@@ -50,8 +50,9 @@ error: argument never used
|
||||
| ------- ^^^^^^^^^^^^^ argument never used
|
||||
| |
|
||||
| formatting specifier missing
|
||||
|
|
||||
help: format specifiers use curly braces, consider adding a format specifier
|
||||
|
|
||||
19 | let _ = span!("hello{}", "hello world");
|
||||
| ++
|
||||
|
||||
error[E0527]: pattern requires 2 elements but array has 3
|
||||
--> tests/ui/fails.rs:8:9
|
||||
|
|
||||
8 | let [a, b] = constraints![
|
||||
| ^^^^^^ expected 3 elements
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "ratatui-termion"
|
||||
version = "0.1.0"
|
||||
version = "0.1.0-alpha.6"
|
||||
description = "Termion backend for the Ratatui Terminal UI library."
|
||||
documentation = "https://docs.rs/ratatui-termion"
|
||||
documentation = "https://docs.rs/ratatui-termion/"
|
||||
readme = "README.md"
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// show the feature flags in the generated documentation
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![doc(
|
||||
html_logo_url = "https://raw.githubusercontent.com/ratatui/ratatui/main/assets/logo.png",
|
||||
html_favicon_url = "https://raw.githubusercontent.com/ratatui/ratatui/main/assets/favicon.ico"
|
||||
@@ -65,7 +66,7 @@ use termion::{color as tcolor, style as tstyle};
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// ```rust,no_run
|
||||
/// use std::io::{stderr, stdout};
|
||||
///
|
||||
/// use ratatui::Terminal;
|
||||
@@ -112,7 +113,7 @@ where
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// ```rust,no_run
|
||||
/// use std::io::stdout;
|
||||
///
|
||||
/// use ratatui::backend::TermionBackend;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "ratatui-termwiz"
|
||||
version = "0.1.0"
|
||||
version = "0.1.0-alpha.6"
|
||||
description = "Termwiz backend for the Ratatui Terminal UI library."
|
||||
documentation = "https://docs.rs/ratatui-termwiz"
|
||||
documentation = "https://docs.rs/ratatui-termwiz/"
|
||||
readme = "README.md"
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
@@ -27,7 +27,7 @@ serde = ["termwiz/use_serde"]
|
||||
|
||||
## Enables the backend code that sets the underline color.
|
||||
## Underline color is not supported on Windows 7.
|
||||
underline-color = ["ratatui-core/underline-color"]
|
||||
underline-color = []
|
||||
|
||||
## Use terminal scrolling regions to make Terminal::insert_before less prone to flickering.
|
||||
scrolling-regions = ["ratatui-core/scrolling-regions"]
|
||||
@@ -38,7 +38,7 @@ ratatui-core = { workspace = true }
|
||||
termwiz.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
ratatui = { path = "../ratatui", features = ["termwiz"], default-features = false }
|
||||
ratatui = { path = "../ratatui", features = ["termwiz"] }
|
||||
rstest.workspace = true
|
||||
|
||||
[lints]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// show the feature flags in the generated documentation
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![doc(
|
||||
html_logo_url = "https://raw.githubusercontent.com/ratatui/ratatui/main/assets/logo.png",
|
||||
html_favicon_url = "https://raw.githubusercontent.com/ratatui/ratatui/main/assets/favicon.ico"
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
[package]
|
||||
name = "ratatui-widgets"
|
||||
description = "A collection of Ratatui widgets for building terminal user interfaces using Ratatui."
|
||||
# Note that this started at 0.3.0 as there was a previous crate using the name `ratatui-widgets`.
|
||||
# <https://github.com/joshka/ratatui-widgets/issues/46>
|
||||
version = "0.3.0"
|
||||
description = "A collection of Ratatui widgets for building terminal user interfaces using Ratatui."
|
||||
documentation = "https://docs.rs/ratatui-widgets"
|
||||
version = "0.3.0-alpha.6"
|
||||
readme = "README.md"
|
||||
authors.workspace = true
|
||||
documentation.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
keywords.workspace = true
|
||||
@@ -18,6 +18,7 @@ rust-version.workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[features]
|
||||
@@ -72,3 +73,8 @@ rstest.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
# Adding a single example is enough for activating rustdoc-scrape-examples
|
||||
[[example]]
|
||||
name = "barchart"
|
||||
doc-scrape-examples = true
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
# Widget Examples
|
||||
|
||||
This folder contains minimal examples for Ratatui widgets.
|
||||
There are meant to provide code snippets that can be copy-pasted into your
|
||||
application.
|
||||
|
||||
> [!TIP]
|
||||
> There are also [application examples] in the top-level `examples` folder.
|
||||
|
||||
[application examples]: ../../examples
|
||||
|
||||
You can run these examples using:
|
||||
|
||||
```shell
|
||||
cargo run -p ratatui-widgets --example example-name
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> This folder might use unreleased code. Consider viewing the examples in the `latest` branch instead
|
||||
> of the `main` branch for code which is guaranteed to work with the released Ratatui version.
|
||||
|
||||
## Barchart (grouped)
|
||||
|
||||
![Barchart (grouped)][barchart-grouped.gif]
|
||||
|
||||
## Barchart
|
||||
|
||||
![Barchart][barchart.gif]
|
||||
|
||||
## Block
|
||||
|
||||
![Block][block.gif]
|
||||
|
||||
## Calendar
|
||||
|
||||
![Calendar][calendar.gif]
|
||||
|
||||
## Canvas
|
||||
|
||||
![Canvas][canvas.gif]
|
||||
|
||||
## Chart
|
||||
|
||||
![Chart][chart.gif]
|
||||
|
||||
## Collapsed Borders
|
||||
|
||||
![Collapsed Borders][collapsed-borders.gif]
|
||||
|
||||
## Gauge
|
||||
|
||||
![Gauge][gauge.gif]
|
||||
|
||||
## Line Gauge
|
||||
|
||||
![Line Gauge][line-gauge.gif]
|
||||
|
||||
## List
|
||||
|
||||
![List][list.gif]
|
||||
|
||||
## Logo
|
||||
|
||||
![Logo][logo.gif]
|
||||
|
||||
## Paragraph
|
||||
|
||||
![Paragraph][paragraph.gif]
|
||||
|
||||
## Scrollbar
|
||||
|
||||
![Scrollbar][scrollbar.gif]
|
||||
|
||||
## Sparkline
|
||||
|
||||
![Sparkline][sparkline.gif]
|
||||
|
||||
## Table
|
||||
|
||||
![Table][table.gif]
|
||||
|
||||
## Tabs
|
||||
|
||||
![Tabs][tabs.gif]
|
||||
|
||||
[barchart-grouped.gif]: https://github.com/ratatui/ratatui/blob/images/widget-examples/barchart-grouped.gif?raw=true
|
||||
[barchart.gif]: https://github.com/ratatui/ratatui/blob/images/widget-examples/barchart.gif?raw=true
|
||||
[block.gif]: https://github.com/ratatui/ratatui/blob/images/widget-examples/block.gif?raw=true
|
||||
[calendar.gif]: https://github.com/ratatui/ratatui/blob/images/widget-examples/calendar.gif?raw=true
|
||||
[canvas.gif]: https://github.com/ratatui/ratatui/blob/images/widget-examples/canvas.gif?raw=true
|
||||
[chart.gif]: https://github.com/ratatui/ratatui/blob/images/widget-examples/chart.gif?raw=true
|
||||
[collapsed-borders.gif]: https://github.com/ratatui/ratatui/blob/images/widget-examples/collapsed-borders.gif?raw=true
|
||||
[gauge.gif]: https://github.com/ratatui/ratatui/blob/images/widget-examples/gauge.gif?raw=true
|
||||
[line-gauge.gif]: https://github.com/ratatui/ratatui/blob/images/widget-examples/line-gauge.gif?raw=true
|
||||
[list.gif]: https://github.com/ratatui/ratatui/blob/images/widget-examples/list.gif?raw=true
|
||||
[logo.gif]: https://github.com/ratatui/ratatui/blob/images/widget-examples/logo.gif?raw=true
|
||||
[paragraph.gif]: https://github.com/ratatui/ratatui/blob/images/widget-examples/paragraph.gif?raw=true
|
||||
[scrollbar.gif]: https://github.com/ratatui/ratatui/blob/images/widget-examples/scrollbar.gif?raw=true
|
||||
[sparkline.gif]: https://github.com/ratatui/ratatui/blob/images/widget-examples/sparkline.gif?raw=true
|
||||
[table.gif]: https://github.com/ratatui/ratatui/blob/images/widget-examples/table.gif?raw=true
|
||||
[tabs.gif]: https://github.com/ratatui/ratatui/blob/images/widget-examples/tabs.gif?raw=true
|
||||
@@ -53,7 +53,7 @@ fn render(frame: &mut Frame, selected_tab: usize) {
|
||||
frame.render_widget(title.centered(), top);
|
||||
|
||||
render_content(frame, main, selected_tab);
|
||||
render_tabs(frame, main + Offset::new(1, 0), selected_tab);
|
||||
render_tabs(frame, main.offset(Offset { x: 1, y: 0 }), selected_tab);
|
||||
}
|
||||
|
||||
/// Render the tabs.
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./ratatui-widgets/examples/vhs/barchart-grouped.tape`
|
||||
Output "target/barchart-grouped.gif"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 1200
|
||||
Set Height 1000
|
||||
Hide
|
||||
Type "cargo run -p ratatui-widgets --example barchart-grouped"
|
||||
Enter
|
||||
Sleep 1s
|
||||
Show
|
||||
Sleep 1s
|
||||
@@ -1,12 +0,0 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./ratatui-widgets/examples/vhs/barchart.tape`
|
||||
Output "target/barchart.gif"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 1200
|
||||
Set Height 600
|
||||
Hide
|
||||
Type "cargo run -p ratatui-widgets --example barchart"
|
||||
Enter
|
||||
Sleep 1s
|
||||
Show
|
||||
Sleep 1s
|
||||
@@ -1,12 +0,0 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./ratatui-widgets/examples/vhs/block.tape`
|
||||
Output "target/block.gif"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 1200
|
||||
Set Height 1200
|
||||
Hide
|
||||
Type "cargo run -p ratatui-widgets --example block"
|
||||
Enter
|
||||
Sleep 2s
|
||||
Show
|
||||
Sleep 2s
|
||||
@@ -1,12 +0,0 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./ratatui-widgets/examples/vhs/calendar.tape`
|
||||
Output "target/calendar.gif"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 1200
|
||||
Set Height 800
|
||||
Hide
|
||||
Type "cargo run -p ratatui-widgets --example calendar"
|
||||
Enter
|
||||
Sleep 3s
|
||||
Show
|
||||
Sleep 5s
|
||||
@@ -1,13 +0,0 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./ratatui-widgets/examples/vhs/canvas.tape`
|
||||
Output "target/canvas.gif"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set FontSize 12
|
||||
Set Width 1200
|
||||
Set Height 800
|
||||
Hide
|
||||
Type "cargo run -p ratatui-widgets --example canvas"
|
||||
Enter
|
||||
Sleep 2s
|
||||
Show
|
||||
Sleep 5s
|
||||
@@ -1,12 +0,0 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./ratatui-widgets/examples/vhs/chart.tape`
|
||||
Output "target/chart.gif"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 1200
|
||||
Set Height 800
|
||||
Hide
|
||||
Type "cargo run -p ratatui-widgets --example chart"
|
||||
Enter
|
||||
Sleep 1s
|
||||
Show
|
||||
Sleep 5s
|
||||
@@ -1,5 +1,5 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./ratatui-widgets/examples/vhs/collapsed-borders.tape`
|
||||
# To run this script, install vhs and run `vhs ./examples/vhs/gauge.tape`
|
||||
Output "target/collapsed-borders.gif"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 1200
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./ratatui-widgets/examples/vhs/gauge.tape`
|
||||
Output "target/gauge.gif"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 1200
|
||||
Set Height 850
|
||||
Hide
|
||||
Type "cargo run -p ratatui-widgets --example gauge"
|
||||
Enter
|
||||
Sleep 2s
|
||||
Show
|
||||
Sleep 5s
|
||||
@@ -1,12 +0,0 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./ratatui-widgets/examples/vhs/line-gauge.tape`
|
||||
Output "target/line-gauge.gif"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 1200
|
||||
Set Height 850
|
||||
Hide
|
||||
Type "cargo run -p ratatui-widgets --example line-gauge"
|
||||
Enter
|
||||
Sleep 2s
|
||||
Show
|
||||
Sleep 5s
|
||||
@@ -1,20 +0,0 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./ratatui-widgets/examples/vhs/list.tape`
|
||||
Output "target/list.gif"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 1200
|
||||
Set Height 612
|
||||
Hide
|
||||
Type "cargo run -p ratatui-widgets --example list"
|
||||
Enter
|
||||
Sleep 10s
|
||||
Show
|
||||
Sleep 2s
|
||||
Down@1.5s 3
|
||||
Sleep 1.5s
|
||||
Down@1.5s 3
|
||||
Sleep 1.5s
|
||||
Up@1s 1
|
||||
Sleep 1s
|
||||
Up@1s 4
|
||||
Sleep 2s
|
||||
@@ -1,14 +0,0 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./ratatui-widgets/examples/vhs/logo.tape`
|
||||
Output "target/logo.gif"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 550
|
||||
Set Height 220
|
||||
Hide
|
||||
Type "cargo run -p ratatui-widgets --example logo"
|
||||
Enter
|
||||
Sleep 2s
|
||||
Show
|
||||
Sleep 2s
|
||||
Hide
|
||||
Escape
|
||||
@@ -1,12 +0,0 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./ratatui-widgets/examples/vhs/paragraph.tape`
|
||||
Output "target/paragraph.gif"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 1200
|
||||
Set Height 600
|
||||
Hide
|
||||
Type "cargo run -p ratatui-widgets --example paragraph"
|
||||
Enter
|
||||
Sleep 1s
|
||||
Show
|
||||
Sleep 5s
|
||||
@@ -1,17 +0,0 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./ratatui-widgets/examples/vhs/scrollbar.tape`
|
||||
Output "target/scrollbar.gif"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 1200
|
||||
Set Height 600
|
||||
Hide
|
||||
Type "cargo run -p ratatui-widgets --example scrollbar"
|
||||
Enter
|
||||
Sleep 1s
|
||||
Show
|
||||
Sleep 2s
|
||||
Right@1s 4
|
||||
Sleep 1s
|
||||
Left@1s 1
|
||||
Down@1s 3
|
||||
Sleep 2s
|
||||
@@ -1,12 +0,0 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./ratatui-widgets/examples/vhs/sparkline.tape`
|
||||
Output "target/sparkline.gif"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 1200
|
||||
Set Height 600
|
||||
Hide
|
||||
Type "cargo run -p ratatui-widgets --example sparkline"
|
||||
Enter
|
||||
Sleep 1s
|
||||
Show
|
||||
Sleep 5s
|
||||
@@ -1,17 +0,0 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./ratatui-widgets/examples/vhs/table.tape`
|
||||
Output "target/table.gif"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 1400
|
||||
Set Height 768
|
||||
Hide
|
||||
Type "cargo run -p ratatui-widgets --example table"
|
||||
Enter
|
||||
Sleep 1s
|
||||
Show
|
||||
Sleep 2s
|
||||
Set TypingSpeed 1s
|
||||
Down 3
|
||||
Sleep 1s
|
||||
Right 3
|
||||
Sleep 2s
|
||||
@@ -1,15 +0,0 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./ratatui-widgets/examples/vhs/tabs.tape`
|
||||
Output "target/tabs.gif"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 1200
|
||||
Set Height 368
|
||||
Hide
|
||||
Type "cargo run -p ratatui-widgets --example tabs"
|
||||
Enter
|
||||
Sleep 2s
|
||||
Show
|
||||
Sleep 1s
|
||||
Right@2.5s 3
|
||||
Left@2.5s 3
|
||||
Sleep 2s
|
||||
@@ -1,111 +0,0 @@
|
||||
/// Implement `AsRef<Self>` for widget types to enable `as_ref()` in generic contexts.
|
||||
///
|
||||
/// This keeps widget rendering ergonomic when APIs accept `AsRef<WidgetType>` bounds, avoiding
|
||||
/// the need for `(&widget).render(...)` just to satisfy a trait bound.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui_widgets::block::Block;
|
||||
///
|
||||
/// let block = Block::default();
|
||||
/// let block_ref: &Block<'_> = block.as_ref();
|
||||
/// ```
|
||||
///
|
||||
/// # Generated impls
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// // Non-generic widgets (e.g. Clear, RatatuiLogo).
|
||||
/// impl AsRef<Clear> for Clear {
|
||||
/// fn as_ref(&self) -> &Clear {
|
||||
/// self
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// // Generic widgets (e.g. Block with a lifetime, Canvas with a lifetime + type parameter).
|
||||
/// impl<'a> AsRef<Block<'a>> for Block<'a> {
|
||||
/// fn as_ref(&self) -> &Block<'a> {
|
||||
/// self
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// impl<'a, F> AsRef<Canvas<'a, F>> for Canvas<'a, F>
|
||||
/// where
|
||||
/// F: Fn(&mut Context),
|
||||
/// {
|
||||
/// fn as_ref(&self) -> &Canvas<'a, F> {
|
||||
/// self
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
macro_rules! impl_as_ref {
|
||||
($type:ty, <$($gen:tt),+> $(where $($bounds:tt)+)?) => {
|
||||
impl<$($gen),+> AsRef<$type> for $type $(where $($bounds)+)? {
|
||||
fn as_ref(&self) -> &$type {
|
||||
self
|
||||
}
|
||||
}
|
||||
};
|
||||
($type:ty) => {
|
||||
impl AsRef<$type> for $type {
|
||||
fn as_ref(&self) -> &$type {
|
||||
self
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_as_ref!(crate::barchart::BarChart<'a>, <'a>);
|
||||
impl_as_ref!(crate::block::Block<'a>, <'a>);
|
||||
impl_as_ref!(crate::canvas::Canvas<'a, F>, <'a, F> where F: Fn(&mut crate::canvas::Context));
|
||||
impl_as_ref!(crate::chart::Chart<'a>, <'a>);
|
||||
impl_as_ref!(crate::clear::Clear);
|
||||
impl_as_ref!(crate::gauge::Gauge<'a>, <'a>);
|
||||
impl_as_ref!(crate::gauge::LineGauge<'a>, <'a>);
|
||||
impl_as_ref!(crate::list::List<'a>, <'a>);
|
||||
impl_as_ref!(crate::logo::RatatuiLogo);
|
||||
impl_as_ref!(crate::mascot::RatatuiMascot);
|
||||
impl_as_ref!(crate::paragraph::Paragraph<'a>, <'a>);
|
||||
impl_as_ref!(crate::scrollbar::Scrollbar<'a>, <'a>);
|
||||
impl_as_ref!(crate::sparkline::Sparkline<'a>, <'a>);
|
||||
impl_as_ref!(crate::table::Table<'a>, <'a>);
|
||||
impl_as_ref!(crate::tabs::Tabs<'a>, <'a>);
|
||||
#[cfg(feature = "calendar")]
|
||||
impl_as_ref!(
|
||||
crate::calendar::Monthly<'a, DS>,
|
||||
<'a, DS> where DS: crate::calendar::DateStyler
|
||||
);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use alloc::vec;
|
||||
|
||||
#[test]
|
||||
fn widgets_implement_as_ref() {
|
||||
let _ = crate::barchart::BarChart::default().as_ref();
|
||||
let _ = crate::block::Block::new().as_ref();
|
||||
let _ = crate::canvas::Canvas::default().paint(|_| {}).as_ref();
|
||||
let _ = crate::chart::Chart::new(vec![]).as_ref();
|
||||
let _ = crate::clear::Clear.as_ref();
|
||||
let _ = crate::gauge::Gauge::default().as_ref();
|
||||
let _ = crate::gauge::LineGauge::default().as_ref();
|
||||
let _ = crate::list::List::new(["foo"]).as_ref();
|
||||
let _ = crate::logo::RatatuiLogo::default().as_ref();
|
||||
let _ = crate::mascot::RatatuiMascot::default().as_ref();
|
||||
let _ = crate::paragraph::Paragraph::new("").as_ref();
|
||||
let _ = crate::scrollbar::Scrollbar::default().as_ref();
|
||||
let _ = crate::sparkline::Sparkline::default().as_ref();
|
||||
let _ = crate::table::Table::default().as_ref();
|
||||
let _ = crate::tabs::Tabs::default().as_ref();
|
||||
}
|
||||
|
||||
#[cfg(feature = "calendar")]
|
||||
#[test]
|
||||
fn calendar_widget_implements_as_ref() {
|
||||
use time::{Date, Month};
|
||||
|
||||
let date = Date::from_calendar_date(2024, Month::January, 1).unwrap();
|
||||
let _ = crate::calendar::Monthly::new(date, crate::calendar::CalendarEventStore::default())
|
||||
.as_ref();
|
||||
}
|
||||
}
|
||||
@@ -1951,18 +1951,18 @@ mod tests {
|
||||
let mut offset = Offset::ZERO;
|
||||
for (border_type_1, border_type_2) in iproduct!(border_types, border_types) {
|
||||
let title = format!("{border_type_1} + {border_type_2}");
|
||||
let title_area = Rect::new(0, 0, 43, 1) + offset;
|
||||
let title_area = Rect::new(0, 0, 43, 1).offset(offset);
|
||||
title.render(title_area, &mut buffer);
|
||||
offset.y += 1;
|
||||
for (rect_1, rect_2) in rects {
|
||||
Block::bordered()
|
||||
.border_type(border_type_1)
|
||||
.merge_borders(strategy)
|
||||
.render(rect_1 + offset, &mut buffer);
|
||||
.render(rect_1.offset(offset), &mut buffer);
|
||||
Block::bordered()
|
||||
.border_type(border_type_2)
|
||||
.merge_borders(strategy)
|
||||
.render(rect_2 + offset, &mut buffer);
|
||||
.render(rect_2.offset(offset), &mut buffer);
|
||||
}
|
||||
offset.y += 9;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user