Compare commits
80 Commits
copilot/su
...
latest
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a2a7c0363 | ||
|
|
8c620d64ae | ||
|
|
686d95574e | ||
|
|
04e86bbfa9 | ||
|
|
06a30ccc72 | ||
|
|
3936b106dc | ||
|
|
1f7efe9d86 | ||
|
|
fbd560a0c8 | ||
|
|
44957543c5 | ||
|
|
a8d8afe101 | ||
|
|
664b79404b | ||
|
|
f9c6093b66 | ||
|
|
b7ecef086d | ||
|
|
38f7ee2049 | ||
|
|
87a382f0e7 | ||
|
|
4367bb45a1 | ||
|
|
bf84c6229b | ||
|
|
3bdb9fe982 | ||
|
|
2163fd5a2c | ||
|
|
645cd55ed2 | ||
|
|
28d80365d6 | ||
|
|
c52de1a926 | ||
|
|
e338795396 | ||
|
|
f944f2fff9 | ||
|
|
58c85295ae | ||
|
|
dc4ca94e95 | ||
|
|
22610b019b | ||
|
|
c0e1b1560e | ||
|
|
55a95e67bc | ||
|
|
345f47e044 | ||
|
|
cb85c8eb4e | ||
|
|
b405a398e9 | ||
|
|
cd5c7afbac | ||
|
|
604f89a167 | ||
|
|
db40df4ed9 | ||
|
|
fae7688ed8 | ||
|
|
2c17beaee4 | ||
|
|
24e3133456 | ||
|
|
96d097ef76 | ||
|
|
a6356c157c | ||
|
|
0fbefe9389 | ||
|
|
12c49c0eac | ||
|
|
f18bcbf06b | ||
|
|
ee673476d3 | ||
|
|
3ba3735650 | ||
|
|
bf268937d4 | ||
|
|
26b05dee59 | ||
|
|
3a945dfe06 | ||
|
|
8f6782acd5 | ||
|
|
dd6d315c7b | ||
|
|
fe410c6645 | ||
|
|
5895340cb3 | ||
|
|
e869cb9750 | ||
|
|
b1d47e7718 | ||
|
|
94ba82e9ca | ||
|
|
500f6c5a52 | ||
|
|
b6588fd1fa | ||
|
|
c7c3498025 | ||
|
|
b65788ce14 | ||
|
|
5d0a362f3e | ||
|
|
7762effd93 | ||
|
|
718dcd6ccb | ||
|
|
1dc18bf3cf | ||
|
|
90a77aaf8b | ||
|
|
d709b0380f | ||
|
|
f8c1006058 | ||
|
|
c60fc65915 | ||
|
|
6c2f276d1c | ||
|
|
4cb1b06371 | ||
|
|
09b943d5b9 | ||
|
|
ad56456e23 | ||
|
|
6957bce264 | ||
|
|
06f0c67131 | ||
|
|
f10a9e8308 | ||
|
|
4c3c0540cd | ||
|
|
f919b25ea6 | ||
|
|
714c6584c3 | ||
|
|
3812f69997 | ||
|
|
16b76e36ee | ||
|
|
fbe6549d05 |
115
.cliffignore
Normal file
115
.cliffignore
Normal file
@@ -0,0 +1,115 @@
|
||||
# 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
|
||||
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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Check semver
|
||||
|
||||
111
.github/workflows/ci.yml
vendored
111
.github/workflows/ci.yml
vendored
@@ -29,15 +29,15 @@ jobs:
|
||||
name: Check Formatting
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@6d653acede28d24f02e3cd41383119e8b1b35921 # master
|
||||
- uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master
|
||||
with:
|
||||
toolchain: nightly
|
||||
components: rustfmt
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2
|
||||
- uses: taiki-e/install-action@c5b1b6f479c32f356cc6f4ba672a47f63853b13b # v2
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
|
||||
- uses: taiki-e/install-action@b9c5db3aef04caffaf95a1d03931de10fb2a140f # v2
|
||||
with:
|
||||
tool: taplo-cli
|
||||
- run: cargo xtask format --check
|
||||
@@ -48,27 +48,32 @@ jobs:
|
||||
name: Check Typos
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: crate-ci/typos@80c8a4945eec0f6d464eaf9e65ed98ef085283d1 # master
|
||||
- uses: crate-ci/typos@2d0ce569feab1f8752f1dde43cc2f2aa53236e06 # master
|
||||
|
||||
# Check for any disallowed dependencies in the codebase due to license / security issues.
|
||||
# See <https://github.com/EmbarkStudios/cargo-deny>
|
||||
dependencies:
|
||||
name: Check Dependencies
|
||||
cargo-deny:
|
||||
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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@6d653acede28d24f02e3cd41383119e8b1b35921 # master
|
||||
- uses: EmbarkStudios/cargo-deny-action@76cd80eb775d7bbbd2d80292136d74d39e1b4918 # v2
|
||||
with:
|
||||
toolchain: stable
|
||||
- uses: taiki-e/install-action@c5b1b6f479c32f356cc6f4ba672a47f63853b13b # v2
|
||||
with:
|
||||
tool: cargo-deny
|
||||
- run: cargo deny --log-level info --all-features check
|
||||
rust-toolchain: stable
|
||||
log-level: info
|
||||
arguments: --all-features --exclude-unpublished
|
||||
command: check ${{ matrix.checks }}
|
||||
|
||||
# Check for any unused dependencies in the codebase.
|
||||
# See <https://github.com/bnjbvr/cargo-machete/>
|
||||
@@ -76,7 +81,7 @@ jobs:
|
||||
name: Check Unused Dependencies
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: bnjbvr/cargo-machete@7959c845782fed02ee69303126d4a12d64f1db18 # v0.9.1
|
||||
@@ -95,14 +100,14 @@ jobs:
|
||||
toolchain: ["stable", "beta"]
|
||||
continue-on-error: ${{ matrix.toolchain == 'beta' }}
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@6d653acede28d24f02e3cd41383119e8b1b35921 # master
|
||||
- uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master
|
||||
with:
|
||||
toolchain: ${{ matrix.toolchain }}
|
||||
components: clippy
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
|
||||
- run: cargo xtask clippy
|
||||
env:
|
||||
RUSTUP_TOOLCHAIN: ${{ matrix.toolchain }}
|
||||
@@ -112,10 +117,10 @@ jobs:
|
||||
name: Check Markdown
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: DavidAnson/markdownlint-cli2-action@992badcdf24e3b8eb7e87ff9287fe931bcb00c6e # v20
|
||||
- uses: DavidAnson/markdownlint-cli2-action@07035fd053f7be764496c0f8d8f9f41f98305101 # v21
|
||||
with:
|
||||
globs: |
|
||||
'**/*.md'
|
||||
@@ -127,19 +132,19 @@ jobs:
|
||||
name: Coverage Report
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@6d653acede28d24f02e3cd41383119e8b1b35921 # master
|
||||
- uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master
|
||||
with:
|
||||
toolchain: stable
|
||||
components: llvm-tools
|
||||
- uses: taiki-e/install-action@c5b1b6f479c32f356cc6f4ba672a47f63853b13b # v2
|
||||
- uses: taiki-e/install-action@b9c5db3aef04caffaf95a1d03931de10fb2a140f # v2
|
||||
with:
|
||||
tool: cargo-llvm-cov
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
|
||||
- run: cargo xtask coverage
|
||||
- uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5
|
||||
- uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: true
|
||||
@@ -151,19 +156,19 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
toolchain: ["1.85.0", "stable"]
|
||||
toolchain: ["1.86.0", "stable"]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@6d653acede28d24f02e3cd41383119e8b1b35921 # master
|
||||
- uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master
|
||||
with:
|
||||
toolchain: ${{ matrix.toolchain }}
|
||||
- uses: taiki-e/install-action@c5b1b6f479c32f356cc6f4ba672a47f63853b13b # v2
|
||||
- uses: taiki-e/install-action@b9c5db3aef04caffaf95a1d03931de10fb2a140f # v2
|
||||
with:
|
||||
tool: cargo-hack
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
|
||||
- run: cargo xtask check --all-features
|
||||
env:
|
||||
RUSTUP_TOOLCHAIN: ${{ matrix.toolchain }}
|
||||
@@ -172,14 +177,14 @@ jobs:
|
||||
name: Build No-Std
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@6d653acede28d24f02e3cd41383119e8b1b35921 # master
|
||||
- uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master
|
||||
with:
|
||||
toolchain: stable
|
||||
targets: x86_64-unknown-none
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # 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
|
||||
@@ -195,11 +200,11 @@ jobs:
|
||||
name: Check README
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2
|
||||
- uses: taiki-e/install-action@c5b1b6f479c32f356cc6f4ba672a47f63853b13b # v2
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
|
||||
- uses: taiki-e/install-action@b9c5db3aef04caffaf95a1d03931de10fb2a140f # v2
|
||||
with:
|
||||
tool: cargo-rdme
|
||||
- run: cargo xtask readme --check
|
||||
@@ -212,19 +217,19 @@ jobs:
|
||||
env:
|
||||
RUSTDOCFLAGS: -Dwarnings
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@6d653acede28d24f02e3cd41383119e8b1b35921 # master
|
||||
- uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master
|
||||
with:
|
||||
toolchain: nightly
|
||||
- uses: dtolnay/install@74f735cdf643820234e37ae1c4089a08fd266d8a # master
|
||||
with:
|
||||
crate: cargo-docs-rs
|
||||
- uses: taiki-e/install-action@c5b1b6f479c32f356cc6f4ba672a47f63853b13b # v2
|
||||
- uses: taiki-e/install-action@b9c5db3aef04caffaf95a1d03931de10fb2a140f # v2
|
||||
with:
|
||||
tool: cargo-hack
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
|
||||
- run: cargo xtask docs
|
||||
|
||||
# Run cargo test on the documentation of the crate. This will catch any code examples that don't
|
||||
@@ -233,16 +238,16 @@ jobs:
|
||||
name: Test Docs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@6d653acede28d24f02e3cd41383119e8b1b35921 # master
|
||||
- uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master
|
||||
with:
|
||||
toolchain: stable
|
||||
- uses: taiki-e/install-action@c5b1b6f479c32f356cc6f4ba672a47f63853b13b # v2
|
||||
- uses: taiki-e/install-action@b9c5db3aef04caffaf95a1d03931de10fb2a140f # v2
|
||||
with:
|
||||
tool: cargo-hack
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
|
||||
- run: cargo xtask test-docs
|
||||
|
||||
# Run cargo test on the libraries of the crate.
|
||||
@@ -252,18 +257,18 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
toolchain: ["1.85.0", "stable"]
|
||||
toolchain: ["1.86.0", "stable"]
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@6d653acede28d24f02e3cd41383119e8b1b35921 # master
|
||||
- uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master
|
||||
with:
|
||||
toolchain: stable
|
||||
- uses: taiki-e/install-action@c5b1b6f479c32f356cc6f4ba672a47f63853b13b # v2
|
||||
- uses: taiki-e/install-action@b9c5db3aef04caffaf95a1d03931de10fb2a140f # v2
|
||||
with:
|
||||
tool: cargo-hack
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
|
||||
- run: cargo xtask test-libs
|
||||
|
||||
# Run cargo test on all the backends.
|
||||
@@ -280,11 +285,11 @@ jobs:
|
||||
- os: windows-latest
|
||||
backend: termion
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: dtolnay/rust-toolchain@6d653acede28d24f02e3cd41383119e8b1b35921 # master
|
||||
- uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master
|
||||
with:
|
||||
toolchain: stable
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
|
||||
- run: cargo xtask test-backend ${{ matrix.backend }}
|
||||
|
||||
14
.github/workflows/release-plz.yml
vendored
14
.github/workflows/release-plz.yml
vendored
@@ -23,18 +23,18 @@ jobs:
|
||||
if: ${{ github.repository_owner == 'ratatui' }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@6d653acede28d24f02e3cd41383119e8b1b35921 # master
|
||||
uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master
|
||||
with:
|
||||
toolchain: stable
|
||||
- uses: rust-lang/crates-io-auth-action@041cce5b4b821e6b0ebc9c9c38b58cac4e34dcc2 # v1
|
||||
- uses: rust-lang/crates-io-auth-action@b7e9a28eded4986ec6b1fa40eeee8f8f165559ec # v1
|
||||
id: auth
|
||||
- name: Run release-plz
|
||||
uses: release-plz/action@d529f731ae3e89610ada96eda34e5c6ba3b12214 # v0.5
|
||||
uses: release-plz/action@487eb7b5c085a664d5c5ca05f4159bd9b591182a # v0.5
|
||||
with:
|
||||
command: release
|
||||
env:
|
||||
@@ -54,16 +54,16 @@ jobs:
|
||||
cancel-in-progress: false
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@6d653acede28d24f02e3cd41383119e8b1b35921 # master
|
||||
uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master
|
||||
with:
|
||||
toolchain: stable
|
||||
- name: Run release-plz
|
||||
uses: release-plz/action@d529f731ae3e89610ada96eda34e5c6ba3b12214 # v0.5
|
||||
uses: release-plz/action@487eb7b5c085a664d5c5ca05f4159bd9b591182a # 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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run zizmor 🌈
|
||||
uses: zizmorcore/zizmor-action@e673c3917a1aef3c65c972347ed84ccd013ecda4 # v0.2.0
|
||||
uses: zizmorcore/zizmor-action@e639db99335bc9038abc0e066dfcd72e23d26fb4 # v0.3.0
|
||||
|
||||
@@ -18,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'
|
||||
- The MSRV is now 1.85.0
|
||||
- MSRV is now 1.86.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
|
||||
@@ -27,6 +27,9 @@ 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
|
||||
@@ -92,6 +95,24 @@ This is a quick summary of the sections below:
|
||||
|
||||
## v0.30.0 Unreleased
|
||||
|
||||
### `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 => { /* ... */ }
|
||||
+ _ => { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
### `Flex::SpaceAround` now mirrors flexbox: space between items is twice the size of the outer gaps ([#1952])
|
||||
|
||||
[#1952]: https://github.com/ratatui/ratatui/pull/1952
|
||||
@@ -235,11 +256,11 @@ instead.
|
||||
+ fn run(mut terminal: DefaultTerminal) -> io::Result<()> {
|
||||
```
|
||||
|
||||
### The MSRV is now 1.85.0 ([#1860])
|
||||
### MSRV is now 1.86.0 ([#2230])
|
||||
|
||||
[#1860]: https://github.com/ratatui/ratatui/pull/1860
|
||||
[#2230]: https://github.com/ratatui/ratatui/pull/2230
|
||||
|
||||
The minimum supported Rust version (MSRV) is now 1.85.0.
|
||||
The minimum supported Rust version (MSRV) is now 1.86.0.
|
||||
|
||||
### `layout::Alignment` is renamed to `layout::HorizontalAlignment` ([#1735])
|
||||
|
||||
@@ -1087,7 +1108,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.
|
||||
|
||||
|
||||
3282
CHANGELOG.md
3282
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
738
Cargo.lock
generated
738
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
22
Cargo.toml
22
Cargo.toml
@@ -25,7 +25,7 @@ readme = "README.md"
|
||||
license = "MIT"
|
||||
exclude = ["assets/*", ".github", "Makefile.toml", "CONTRIBUTING.md", "*.log", "tags"]
|
||||
edition = "2024"
|
||||
rust-version = "1.85.0"
|
||||
rust-version = "1.86.0"
|
||||
|
||||
[workspace.dependencies]
|
||||
anstyle = "1"
|
||||
@@ -33,7 +33,7 @@ bitflags = "2.10"
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
color-eyre = "0.6"
|
||||
compact_str = { version = "0.9", default-features = false }
|
||||
criterion = { version = "0.7", features = ["html_reports"] }
|
||||
criterion = { version = "0.8", features = ["html_reports"] }
|
||||
crossterm = "0.29"
|
||||
document-features = "0.2"
|
||||
fakeit = "1"
|
||||
@@ -45,18 +45,18 @@ 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.47"
|
||||
octocrab = "0.49"
|
||||
palette = "0.7"
|
||||
pretty_assertions = "1"
|
||||
rand = "0.9"
|
||||
rand_chacha = "0.9"
|
||||
ratatui = { path = "ratatui", version = "0.30.0-alpha.5" }
|
||||
ratatui-core = { path = "ratatui-core", version = "0.1.0-alpha.6" }
|
||||
ratatui-crossterm = { path = "ratatui-crossterm", version = "0.1.0-alpha.5" }
|
||||
ratatui-macros = { path = "ratatui-macros", version = "0.7.0-alpha.4" }
|
||||
ratatui-termion = { path = "ratatui-termion", version = "0.1.0-alpha.5" }
|
||||
ratatui-termwiz = { path = "ratatui-termwiz", version = "0.1.0-alpha.5" }
|
||||
ratatui-widgets = { path = "ratatui-widgets", version = "0.3.0-alpha.5" }
|
||||
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"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
@@ -107,6 +107,7 @@ 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"
|
||||
@@ -118,6 +119,5 @@ 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"
|
||||
|
||||
14
cliff.toml
14
cliff.toml
@@ -61,6 +61,18 @@ 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 -%}
|
||||
@@ -126,12 +138,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" },
|
||||
{ message = "^build\\(deps\\)", skip = true },
|
||||
{ message = "^build\\(release\\)", skip = true },
|
||||
{ message = "^build", group = "<!-- 08 -->Build" },
|
||||
{ body = ".*security", group = "<!-- 09 -->Security" },
|
||||
{ message = "^ci", group = "<!-- 10 -->Continuous Integration" },
|
||||
|
||||
@@ -206,7 +206,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/). [Source](./apps/user-input/).
|
||||
Shows how to handle user input. [Source](./apps/user-input/).
|
||||
|
||||
![User input demo][user-input.gif]
|
||||
|
||||
|
||||
@@ -122,8 +122,12 @@ impl App {
|
||||
Marker::Dot => Marker::Braille,
|
||||
Marker::Braille => Marker::Block,
|
||||
Marker::Block => Marker::HalfBlock,
|
||||
Marker::HalfBlock => Marker::Bar,
|
||||
Marker::HalfBlock => Marker::Quadrant,
|
||||
Marker::Quadrant => Marker::Sextant,
|
||||
Marker::Sextant => Marker::Octant,
|
||||
Marker::Octant => Marker::Bar,
|
||||
Marker::Bar => Marker::Dot,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -123,9 +123,9 @@ impl InputForm {
|
||||
frame.render_widget(&self.age, age_area);
|
||||
|
||||
let cursor_position = match self.focus {
|
||||
Focus::FirstName => first_name_area.offset(self.first_name.cursor_offset()),
|
||||
Focus::LastName => last_name_area.offset(self.last_name.cursor_offset()),
|
||||
Focus::Age => age_area.offset(self.age.cursor_offset()),
|
||||
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(),
|
||||
};
|
||||
frame.set_cursor_position(cursor_position);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@ 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.
|
||||
Set Width 1120
|
||||
# Please note that based on the width the demo may wrap and look corrupted.
|
||||
Set Width 1160
|
||||
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.
|
||||
"""
|
||||
version = "0.1.0-alpha.6"
|
||||
documentation = "https://docs.rs/ratatui-core"
|
||||
readme = "README.md"
|
||||
authors.workspace = true
|
||||
documentation.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
keywords.workspace = true
|
||||
|
||||
@@ -216,6 +216,9 @@ 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
|
||||
///
|
||||
/// ```
|
||||
@@ -269,10 +272,13 @@ 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
|
||||
///
|
||||
/// ```
|
||||
|
||||
@@ -316,6 +316,7 @@ mod direction;
|
||||
mod flex;
|
||||
mod layout;
|
||||
mod margin;
|
||||
mod offset;
|
||||
mod position;
|
||||
mod rect;
|
||||
mod size;
|
||||
@@ -326,6 +327,7 @@ 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, Offset, Positions, Rect, Rows};
|
||||
pub use rect::{Columns, Positions, Rect, Rows};
|
||||
pub use size::Size;
|
||||
|
||||
@@ -12,11 +12,27 @@ 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;
|
||||
@@ -37,4 +53,11 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
66
ratatui-core/src/layout/offset.rs
Normal file
66
ratatui-core/src/layout/offset.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
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,7 +1,8 @@
|
||||
#![warn(missing_docs)]
|
||||
use core::fmt;
|
||||
use core::ops::{Add, AddAssign, Sub, SubAssign};
|
||||
|
||||
use crate::layout::Rect;
|
||||
use crate::layout::{Offset, Rect};
|
||||
|
||||
/// Position in the terminal coordinate system.
|
||||
///
|
||||
@@ -23,10 +24,18 @@ use crate::layout::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::{Position, Rect};
|
||||
/// use ratatui_core::layout::{Offset, Position, Rect};
|
||||
///
|
||||
/// // the following are all equivalent
|
||||
/// let position = Position { x: 1, y: 2 };
|
||||
@@ -36,6 +45,10 @@ use crate::layout::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.
|
||||
@@ -57,12 +70,27 @@ pub struct Position {
|
||||
|
||||
impl Position {
|
||||
/// Position at the origin, the top left edge at 0,0
|
||||
pub const ORIGIN: Self = Self { x: 0, y: 0 };
|
||||
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);
|
||||
|
||||
/// 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 {
|
||||
@@ -89,6 +117,68 @@ 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;
|
||||
@@ -98,15 +188,15 @@ mod tests {
|
||||
#[test]
|
||||
fn new() {
|
||||
let position = Position::new(1, 2);
|
||||
assert_eq!(position.x, 1);
|
||||
assert_eq!(position.y, 2);
|
||||
|
||||
assert_eq!(position, Position { x: 1, y: 2 });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_tuple() {
|
||||
let position = Position::from((1, 2));
|
||||
assert_eq!(position.x, 1);
|
||||
assert_eq!(position.y, 2);
|
||||
|
||||
assert_eq!(position, Position { x: 1, y: 2 });
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -121,8 +211,8 @@ mod tests {
|
||||
fn from_rect() {
|
||||
let rect = Rect::new(1, 2, 3, 4);
|
||||
let position = Position::from(rect);
|
||||
assert_eq!(position.x, 1);
|
||||
assert_eq!(position.y, 2);
|
||||
|
||||
assert_eq!(position, Position { x: 1, y: 2 });
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -130,4 +220,34 @@ 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,10 +3,11 @@ use core::array::TryFromSliceError;
|
||||
use core::cmp::{max, min};
|
||||
use core::fmt;
|
||||
|
||||
use crate::layout::{Margin, Position, Size};
|
||||
pub use self::iter::{Columns, Positions, Rows};
|
||||
use crate::layout::{Margin, Offset, Position, Size};
|
||||
|
||||
mod iter;
|
||||
pub use iter::*;
|
||||
mod ops;
|
||||
|
||||
use super::{Constraint, Flex, Layout};
|
||||
|
||||
@@ -45,6 +46,7 @@ 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
|
||||
@@ -66,21 +68,67 @@ 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};
|
||||
///
|
||||
/// // 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);
|
||||
/// 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 from position and size
|
||||
/// let rect = Rect::from((Position::new(10, 5), Size::new(80, 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));
|
||||
/// ```
|
||||
///
|
||||
/// 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 {
|
||||
@@ -94,30 +142,6 @@ 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)
|
||||
@@ -133,6 +157,12 @@ 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
|
||||
@@ -147,15 +177,8 @@ impl Rect {
|
||||
/// let rect = Rect::new(1, 2, 3, 4);
|
||||
/// ```
|
||||
pub const fn new(x: u16, y: u16, width: u16, height: u16) -> Self {
|
||||
// these calculations avoid using min so that this function can be const
|
||||
let max_width = u16::MAX - x;
|
||||
let max_height = u16::MAX - y;
|
||||
let width = if width > max_width { max_width } else { width };
|
||||
let height = if height > max_height {
|
||||
max_height
|
||||
} else {
|
||||
height
|
||||
};
|
||||
let width = x.saturating_add(width) - x;
|
||||
let height = y.saturating_add(height) - y;
|
||||
Self {
|
||||
x,
|
||||
y,
|
||||
@@ -260,13 +283,19 @@ 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 {
|
||||
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,
|
||||
width: self.x.saturating_add(size.width).saturating_sub(self.x),
|
||||
height: self.y.saturating_add(size.height).saturating_sub(self.y),
|
||||
..self
|
||||
}
|
||||
}
|
||||
@@ -811,6 +840,16 @@ 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)]
|
||||
@@ -857,6 +896,18 @@ 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 {
|
||||
|
||||
136
ratatui-core/src/layout/rect/ops.rs
Normal file
136
ratatui-core/src/layout/rect/ops.rs
Normal file
@@ -0,0 +1,136 @@
|
||||
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,14 +24,20 @@ 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.
|
||||
@@ -48,10 +54,24 @@ 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 {
|
||||
@@ -60,6 +80,12 @@ 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()
|
||||
@@ -92,6 +118,14 @@ 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));
|
||||
@@ -103,4 +137,11 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,17 +250,41 @@ pub struct Style {
|
||||
/// The modifiers to add.
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
serde(default, skip_serializing_if = "Modifier::is_empty")
|
||||
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")
|
||||
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 {
|
||||
@@ -409,6 +433,22 @@ 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.
|
||||
///
|
||||
@@ -788,6 +828,28 @@ 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)]
|
||||
@@ -989,4 +1051,19 @@ mod tests {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,5 +10,6 @@ pub mod half_block;
|
||||
pub mod line;
|
||||
pub mod marker;
|
||||
pub mod merge;
|
||||
pub mod pixel;
|
||||
pub mod scrollbar;
|
||||
pub mod shade;
|
||||
|
||||
@@ -1,7 +1,21 @@
|
||||
pub const BLANK: u16 = 0x2800;
|
||||
pub const DOTS: [[u16; 2]; 4] = [
|
||||
[0x0001, 0x0008],
|
||||
[0x0002, 0x0010],
|
||||
[0x0004, 0x0020],
|
||||
[0x0040, 0x0080],
|
||||
//! 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] = [
|
||||
'⠀', '⠁', '⠈', '⠉', '⠂', '⠃', '⠊', '⠋', '⠐', '⠑', '⠘', '⠙', '⠒', '⠓', '⠚', '⠛', '⠄', '⠅', '⠌',
|
||||
'⠍', '⠆', '⠇', '⠎', '⠏', '⠔', '⠕', '⠜', '⠝', '⠖', '⠗', '⠞', '⠟', '⠠', '⠡', '⠨', '⠩', '⠢', '⠣',
|
||||
'⠪', '⠫', '⠰', '⠱', '⠸', '⠹', '⠲', '⠳', '⠺', '⠻', '⠤', '⠥', '⠬', '⠭', '⠦', '⠧', '⠮', '⠯', '⠴',
|
||||
'⠵', '⠼', '⠽', '⠶', '⠷', '⠾', '⠿', '⡀', '⡁', '⡈', '⡉', '⡂', '⡃', '⡊', '⡋', '⡐', '⡑', '⡘', '⡙',
|
||||
'⡒', '⡓', '⡚', '⡛', '⡄', '⡅', '⡌', '⡍', '⡆', '⡇', '⡎', '⡏', '⡔', '⡕', '⡜', '⡝', '⡖', '⡗', '⡞',
|
||||
'⡟', '⡠', '⡡', '⡨', '⡩', '⡢', '⡣', '⡪', '⡫', '⡰', '⡱', '⡸', '⡹', '⡲', '⡳', '⡺', '⡻', '⡤', '⡥',
|
||||
'⡬', '⡭', '⡦', '⡧', '⡮', '⡯', '⡴', '⡵', '⡼', '⡽', '⡶', '⡷', '⡾', '⡿', '⢀', '⢁', '⢈', '⢉', '⢂',
|
||||
'⢃', '⢊', '⢋', '⢐', '⢑', '⢘', '⢙', '⢒', '⢓', '⢚', '⢛', '⢄', '⢅', '⢌', '⢍', '⢆', '⢇', '⢎', '⢏',
|
||||
'⢔', '⢕', '⢜', '⢝', '⢖', '⢗', '⢞', '⢟', '⢠', '⢡', '⢨', '⢩', '⢢', '⢣', '⢪', '⢫', '⢰', '⢱', '⢸',
|
||||
'⢹', '⢲', '⢳', '⢺', '⢻', '⢤', '⢥', '⢬', '⢭', '⢦', '⢧', '⢮', '⢯', '⢴', '⢵', '⢼', '⢽', '⢶', '⢷',
|
||||
'⢾', '⢿', '⣀', '⣁', '⣈', '⣉', '⣂', '⣃', '⣊', '⣋', '⣐', '⣑', '⣘', '⣙', '⣒', '⣓', '⣚', '⣛', '⣄',
|
||||
'⣅', '⣌', '⣍', '⣆', '⣇', '⣎', '⣏', '⣔', '⣕', '⣜', '⣝', '⣖', '⣗', '⣞', '⣟', '⣠', '⣡', '⣨', '⣩',
|
||||
'⣢', '⣣', '⣪', '⣫', '⣰', '⣱', '⣸', '⣹', '⣲', '⣳', '⣺', '⣻', '⣤', '⣥', '⣬', '⣭', '⣦', '⣧', '⣮',
|
||||
'⣯', '⣴', '⣵', '⣼', '⣽', '⣶', '⣷', '⣾', '⣿',
|
||||
];
|
||||
|
||||
@@ -4,6 +4,7 @@ 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]
|
||||
@@ -25,6 +26,33 @@ 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)]
|
||||
|
||||
30
ratatui-core/src/symbols/pixel.rs
Normal file
30
ratatui-core/src/symbols/pixel.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
//! 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] = [
|
||||
' ', '', '', '🮂', '', '▘', '', '', '', '', '▝', '', '', '', '', '▀', '', '', '',
|
||||
'', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
|
||||
'', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
|
||||
'', '', '', '', '', '', '🮅', '', '', '', '', '', '', '', '', '', '', '', '',
|
||||
'', '', '', '', '▖', '', '', '', '', '▌', '', '', '', '', '▞', '', '', '', '',
|
||||
'▛', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
|
||||
'', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
|
||||
'', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
|
||||
'', '', '', '', '', '', '', '', '▗', '', '', '', '', '▚', '', '', '', '', '▐',
|
||||
'', '', '', '', '▜', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
|
||||
'', '', '▂', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
|
||||
'', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
|
||||
'', '', '', '', '', '', '', '', '', '', '', '', '▄', '', '', '', '', '▙', '',
|
||||
'', '', '', '▟', '', '▆', '', '', '█',
|
||||
];
|
||||
@@ -72,7 +72,7 @@ fn outer() {
|
||||
#[test]
|
||||
fn offset() {
|
||||
let base = Rect::new(2, 2, 5, 3);
|
||||
let moved = base.offset(Offset { x: 4, y: 2 });
|
||||
let moved = base + Offset::new(4, 2);
|
||||
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 10));
|
||||
Filled { symbol: "░" }.render(base, &mut buf);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "ratatui-crossterm"
|
||||
version = "0.1.0-alpha.5"
|
||||
version = "0.1.0"
|
||||
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
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See the [top-level changelog](../CHANGELOG.md) for the latest changes.
|
||||
|
||||
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).
|
||||
This file is obsolete as of 0.30.0 release. [\*](https://github.com/ratatui/ratatui/pull/1652)
|
||||
|
||||
## [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-alpha.4"
|
||||
version = "0.7.0"
|
||||
description = "Macros for Ratatui"
|
||||
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"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "ratatui-termion"
|
||||
version = "0.1.0-alpha.5"
|
||||
version = "0.1.0"
|
||||
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,8 +1,8 @@
|
||||
[package]
|
||||
name = "ratatui-termwiz"
|
||||
version = "0.1.0-alpha.5"
|
||||
version = "0.1.0"
|
||||
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
|
||||
|
||||
@@ -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-alpha.5"
|
||||
version = "0.3.0"
|
||||
description = "A collection of Ratatui widgets for building terminal user interfaces using Ratatui."
|
||||
documentation = "https://docs.rs/ratatui-widgets"
|
||||
readme = "README.md"
|
||||
authors.workspace = true
|
||||
documentation.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
keywords.workspace = true
|
||||
@@ -18,7 +18,6 @@ rust-version.workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[features]
|
||||
@@ -73,8 +72,3 @@ rstest.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
# Adding a single example is enough for activating rustdoc-scrape-examples
|
||||
[[example]]
|
||||
name = "barchart"
|
||||
doc-scrape-examples = 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(Offset { x: 1, y: 0 }), selected_tab);
|
||||
render_tabs(frame, main + Offset::new(1, 0), selected_tab);
|
||||
}
|
||||
|
||||
/// Render the tabs.
|
||||
|
||||
@@ -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(offset);
|
||||
let title_area = Rect::new(0, 0, 43, 1) + 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(offset), &mut buffer);
|
||||
.render(rect_1 + offset, &mut buffer);
|
||||
Block::bordered()
|
||||
.border_type(border_type_2)
|
||||
.merge_borders(strategy)
|
||||
.render(rect_2.offset(offset), &mut buffer);
|
||||
.render(rect_2 + offset, &mut buffer);
|
||||
}
|
||||
offset.y += 9;
|
||||
}
|
||||
|
||||
@@ -104,6 +104,36 @@ impl<'a, DS: DateStyler> Monthly<'a, DS> {
|
||||
self
|
||||
}
|
||||
|
||||
/// Return the width required to render the calendar.
|
||||
#[must_use]
|
||||
pub fn width(&self) -> u16 {
|
||||
const DAYS_PER_WEEK: u16 = 7;
|
||||
const GUTTER_WIDTH: u16 = 1;
|
||||
const DAY_WIDTH: u16 = 2;
|
||||
|
||||
let mut width = DAYS_PER_WEEK * (GUTTER_WIDTH + DAY_WIDTH);
|
||||
if let Some(block) = &self.block {
|
||||
let (left, right) = block.horizontal_space();
|
||||
width = width.saturating_add(left).saturating_add(right);
|
||||
}
|
||||
width
|
||||
}
|
||||
|
||||
/// Return the height required to render the calendar.
|
||||
#[must_use]
|
||||
pub fn height(&self) -> u16 {
|
||||
let mut height = u16::from(sunday_based_weeks(self.display_date))
|
||||
.saturating_add(u16::from(self.show_month.is_some()))
|
||||
.saturating_add(u16::from(self.show_weekday.is_some()));
|
||||
|
||||
if let Some(block) = &self.block {
|
||||
let (top, bottom) = block.vertical_space();
|
||||
height = height.saturating_add(top).saturating_add(bottom);
|
||||
}
|
||||
|
||||
height
|
||||
}
|
||||
|
||||
/// Return a style with only the background from the default style
|
||||
const fn default_bg(&self) -> Style {
|
||||
match self.default_style.bg {
|
||||
@@ -200,6 +230,22 @@ impl<DS: DateStyler> Monthly<'_, DS> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute how many Sunday-based week rows are needed to render `display_date`.
|
||||
///
|
||||
/// Mirrors the rendering logic by taking the difference between the first and last day
|
||||
/// Sunday-based week numbers (inclusive).
|
||||
fn sunday_based_weeks(display_date: Date) -> u8 {
|
||||
let first_of_month = display_date
|
||||
.replace_day(1)
|
||||
.expect("valid first day of month");
|
||||
let last_of_month = first_of_month
|
||||
.replace_day(first_of_month.month().length(first_of_month.year()))
|
||||
.expect("valid last of month");
|
||||
let first_week = first_of_month.sunday_based_week();
|
||||
let last_week = last_of_month.sunday_based_week();
|
||||
last_week.saturating_sub(first_week) + 1
|
||||
}
|
||||
|
||||
/// Provides a method for styling a given date. [Monthly] is generic on this trait, so any type
|
||||
/// that implements this trait can be used.
|
||||
pub trait DateStyler {
|
||||
@@ -268,10 +314,11 @@ impl Default for CalendarEventStore {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ratatui_core::style::Color;
|
||||
use ratatui_core::style::{Color, Style};
|
||||
use time::Month;
|
||||
|
||||
use super::*;
|
||||
use crate::block::{Block, Padding};
|
||||
|
||||
#[test]
|
||||
fn event_store() {
|
||||
@@ -325,4 +372,87 @@ mod tests {
|
||||
// This should not panic, even if the buffer has zero size.
|
||||
calendar.render(buffer.area, &mut buffer);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn calendar_width_reflects_grid_layout() {
|
||||
let date = Date::from_calendar_date(2023, Month::January, 1).unwrap();
|
||||
let calendar = Monthly::new(date, CalendarEventStore::default());
|
||||
assert_eq!(calendar.width(), 21);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn calendar_height_counts_weeks_and_headers() {
|
||||
let date = Date::from_calendar_date(2015, Month::February, 1).unwrap();
|
||||
let base_calendar = Monthly::new(date, CalendarEventStore::default());
|
||||
assert_eq!(base_calendar.height(), 4);
|
||||
|
||||
let decorated_calendar = Monthly::new(date, CalendarEventStore::default())
|
||||
.show_month_header(Style::default())
|
||||
.show_weekdays_header(Style::default());
|
||||
assert_eq!(decorated_calendar.height(), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn calendar_dimensions_examples() {
|
||||
// Feb 2015 starts Sunday and spans 4 rows.
|
||||
let feb_2015 = Date::from_calendar_date(2015, Month::February, 1).unwrap();
|
||||
let cal = Monthly::new(feb_2015, CalendarEventStore::default());
|
||||
assert_eq!(cal.width(), 21, "4w base width");
|
||||
assert_eq!(cal.height(), 4, "Feb 2015 rows");
|
||||
|
||||
let cal = Monthly::new(feb_2015, CalendarEventStore::default())
|
||||
.show_month_header(Style::default())
|
||||
.show_weekdays_header(Style::default());
|
||||
assert_eq!(cal.height(), 6, "Headers add 2 rows");
|
||||
|
||||
let block = Block::bordered().padding(Padding::new(2, 3, 1, 2));
|
||||
let cal = Monthly::new(feb_2015, CalendarEventStore::default()).block(block);
|
||||
assert_eq!(cal.width(), 28, "Padding widens width");
|
||||
assert_eq!(cal.height(), 9, "Padding grows height");
|
||||
|
||||
// Feb 2024 starts Thursday and spans 5 rows.
|
||||
let feb_2024 = Date::from_calendar_date(2024, Month::February, 1).unwrap();
|
||||
let cal = Monthly::new(feb_2024, CalendarEventStore::default());
|
||||
assert_eq!(cal.width(), 21, "5w base width");
|
||||
assert_eq!(cal.height(), 5, "Feb 2024 rows");
|
||||
|
||||
let cal = Monthly::new(feb_2024, CalendarEventStore::default())
|
||||
.show_month_header(Style::default())
|
||||
.show_weekdays_header(Style::default());
|
||||
assert_eq!(cal.height(), 7, "Headers add 2 rows (5w)");
|
||||
|
||||
let cal = Monthly::new(feb_2024, CalendarEventStore::default()).block(Block::bordered());
|
||||
assert_eq!(cal.width(), 23, "Border adds 2 cols");
|
||||
assert_eq!(cal.height(), 7, "Border adds 2 rows");
|
||||
|
||||
// Apr 2023 starts Saturday and spans 6 rows.
|
||||
let apr_2023 = Date::from_calendar_date(2023, Month::April, 1).unwrap();
|
||||
let cal = Monthly::new(apr_2023, CalendarEventStore::default());
|
||||
assert_eq!(cal.width(), 21, "6w base width");
|
||||
assert_eq!(cal.height(), 6, "Apr 2023 rows");
|
||||
|
||||
let cal = Monthly::new(apr_2023, CalendarEventStore::default())
|
||||
.show_month_header(Style::default())
|
||||
.show_weekdays_header(Style::default());
|
||||
assert_eq!(cal.height(), 8, "Headers add 2 rows (6w)");
|
||||
|
||||
let block = Block::bordered().padding(Padding::symmetric(1, 1));
|
||||
let cal = Monthly::new(apr_2023, CalendarEventStore::default()).block(block);
|
||||
assert_eq!(cal.width(), 25, "Symmetric padding width");
|
||||
assert_eq!(cal.height(), 10, "Symmetric padding height");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sunday_based_weeks_shapes() {
|
||||
let sunday_start =
|
||||
Date::from_calendar_date(2015, Month::February, 11).expect("valid test date");
|
||||
let saturday_start =
|
||||
Date::from_calendar_date(2023, Month::April, 9).expect("valid test date");
|
||||
let leap_year =
|
||||
Date::from_calendar_date(2024, Month::February, 29).expect("valid test date");
|
||||
|
||||
assert_eq!(sunday_based_weeks(sunday_start), 4);
|
||||
assert_eq!(sunday_based_weeks(saturday_start), 6);
|
||||
assert_eq!(sunday_based_weeks(leap_year), 5);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
//! You can also implement your own custom [`Shape`]s.
|
||||
|
||||
use alloc::boxed::Box;
|
||||
use alloc::string::String;
|
||||
use alloc::vec;
|
||||
use alloc::vec::Vec;
|
||||
use core::fmt;
|
||||
@@ -24,6 +23,8 @@ use itertools::Itertools;
|
||||
use ratatui_core::buffer::Buffer;
|
||||
use ratatui_core::layout::Rect;
|
||||
use ratatui_core::style::{Color, Style};
|
||||
use ratatui_core::symbols::braille::BRAILLE;
|
||||
use ratatui_core::symbols::pixel::{OCTANTS, QUADRANTS, SEXTANTS};
|
||||
use ratatui_core::symbols::{self, Marker};
|
||||
use ratatui_core::text::Line as TextLine;
|
||||
use ratatui_core::widgets::Widget;
|
||||
@@ -69,11 +70,20 @@ pub struct Label<'a> {
|
||||
/// multiple shapes on the canvas in specific order.
|
||||
#[derive(Debug)]
|
||||
struct Layer {
|
||||
// A string of characters representing the grid. This will be wrapped to the width of the grid
|
||||
// when rendering
|
||||
string: String,
|
||||
// Colors for foreground and background of each cell
|
||||
colors: Vec<(Color, Color)>,
|
||||
contents: Vec<LayerCell>,
|
||||
}
|
||||
|
||||
/// A cell within a layer.
|
||||
///
|
||||
/// If a [`Context`] contains multiple layers, then the symbol, foreground, and background colors
|
||||
/// for a character will be determined by the top-most layer that provides a value for that
|
||||
/// character. For example, a chart drawn with [`Marker::Block`] may provide the background color,
|
||||
/// and a later chart drawn with [`Marker::Braille`] may provide the symbol and foreground color.
|
||||
#[derive(Debug)]
|
||||
struct LayerCell {
|
||||
symbol: Option<char>,
|
||||
fg: Option<Color>,
|
||||
bg: Option<Color>,
|
||||
}
|
||||
|
||||
/// A grid of cells that can be painted on.
|
||||
@@ -100,73 +110,111 @@ trait Grid: fmt::Debug {
|
||||
fn reset(&mut self);
|
||||
}
|
||||
|
||||
/// The `BrailleGrid` is a grid made up of cells each containing a Braille pattern.
|
||||
/// The pattern and color of a `PatternGrid` cell.
|
||||
#[derive(Copy, Clone, Debug, Default)]
|
||||
struct PatternCell {
|
||||
/// The pattern of a grid character.
|
||||
///
|
||||
/// The pattern is stored in the lower bits in a row-major order. For instance, for a 2x4
|
||||
/// pattern marker, bits 0 to 7 of this field should represent the following pseudo-pixels:
|
||||
///
|
||||
/// | 0 1 |
|
||||
/// | 2 3 |
|
||||
/// | 4 5 |
|
||||
/// | 6 7 |
|
||||
pattern: u8,
|
||||
/// The color of a cell only supports foreground colors for now as there's no way to
|
||||
/// individually set the background color of each pseudo-pixel in a pattern character.
|
||||
color: Option<Color>,
|
||||
}
|
||||
|
||||
/// The `PatternGrid` is a grid made up of cells each containing a `W`x`H` pattern character.
|
||||
///
|
||||
/// This makes it possible to draw shapes with a resolution of 2x4 dots per cell. This is useful
|
||||
/// when you want to draw shapes with a high resolution. Font support for Braille patterns is
|
||||
/// required to see the dots. If your terminal or font does not support this unicode block, you
|
||||
/// will see unicode replacement characters (<28>) instead of braille dots.
|
||||
/// This makes it possible to draw shapes with a resolution of e.g. 2x4 (Braille or unicode octant)
|
||||
/// per cell.
|
||||
/// Font support for the relevant pattern character is required. If your terminal or font does not
|
||||
/// support the relevant unicode block, you will see unicode replacement characters (<28>) instead.
|
||||
///
|
||||
/// This grid type only supports a single foreground color for each 2x4 dots cell. There is no way
|
||||
/// to set the individual color of each dot in the braille pattern.
|
||||
/// This grid type only supports a single foreground color for each `W`x`H` pattern character.
|
||||
/// There is no way to set the individual color of each pseudo-pixel.
|
||||
#[derive(Debug)]
|
||||
struct BrailleGrid {
|
||||
struct PatternGrid<const W: usize, const H: usize> {
|
||||
/// Width of the grid in number of terminal columns
|
||||
width: u16,
|
||||
/// Height of the grid in number of terminal rows
|
||||
height: u16,
|
||||
/// Represents the unicode braille patterns. Will take a value between `0x2800` and `0x28FF`
|
||||
/// this is converted to an utf16 string when converting to a layer. See
|
||||
/// <https://en.wikipedia.org/wiki/Braille_Patterns> for more info.
|
||||
utf16_code_points: Vec<u16>,
|
||||
/// The color of each cell only supports foreground colors for now as there's no way to
|
||||
/// individually set the background color of each dot in the braille pattern.
|
||||
colors: Vec<Color>,
|
||||
/// Pattern and color of the cells.
|
||||
cells: Vec<PatternCell>,
|
||||
/// Lookup table mapping patterns to characters.
|
||||
char_table: &'static [char],
|
||||
}
|
||||
|
||||
impl BrailleGrid {
|
||||
/// Create a new `BrailleGrid` with the given width and height measured in terminal columns and
|
||||
/// rows respectively.
|
||||
fn new(width: u16, height: u16) -> Self {
|
||||
impl<const W: usize, const H: usize> PatternGrid<W, H> {
|
||||
/// Statically check that the dimension of the pattern is supported.
|
||||
const _PATTERN_DIMENSION_CHECK: usize = u8::BITS as usize - W * H;
|
||||
|
||||
/// Create a new `PatternGrid` with the given width and height measured in terminal columns
|
||||
/// and rows respectively.
|
||||
fn new(width: u16, height: u16, char_table: &'static [char]) -> Self {
|
||||
// Cause a static error if the pattern doesn't fit within 8 bits.
|
||||
let _ = Self::_PATTERN_DIMENSION_CHECK;
|
||||
|
||||
let length = usize::from(width) * usize::from(height);
|
||||
Self {
|
||||
width,
|
||||
height,
|
||||
utf16_code_points: vec![symbols::braille::BLANK; length],
|
||||
colors: vec![Color::Reset; length],
|
||||
cells: vec![PatternCell::default(); length],
|
||||
char_table,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Grid for BrailleGrid {
|
||||
impl<const W: usize, const H: usize> Grid for PatternGrid<W, H> {
|
||||
fn resolution(&self) -> (f64, f64) {
|
||||
(f64::from(self.width) * 2.0, f64::from(self.height) * 4.0)
|
||||
(
|
||||
f64::from(self.width) * W as f64,
|
||||
f64::from(self.height) * H as f64,
|
||||
)
|
||||
}
|
||||
|
||||
fn save(&self) -> Layer {
|
||||
let string = String::from_utf16(&self.utf16_code_points).unwrap();
|
||||
// the background color is always reset for braille patterns
|
||||
let colors = self.colors.iter().map(|c| (*c, Color::Reset)).collect();
|
||||
Layer { string, colors }
|
||||
let contents = self
|
||||
.cells
|
||||
.iter()
|
||||
.map(|&cell| {
|
||||
let symbol = match cell.pattern {
|
||||
// Skip rendering blank patterns to allow layers underneath
|
||||
// to show through.
|
||||
0 => None,
|
||||
idx => Some(self.char_table[idx as usize]),
|
||||
};
|
||||
|
||||
LayerCell {
|
||||
symbol,
|
||||
fg: cell.color,
|
||||
// Patterns only affect foreground.
|
||||
bg: None,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Layer { contents }
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.utf16_code_points.fill(symbols::braille::BLANK);
|
||||
self.colors.fill(Color::Reset);
|
||||
self.cells.fill_with(Default::default);
|
||||
}
|
||||
|
||||
fn paint(&mut self, x: usize, y: usize, color: Color) {
|
||||
let index = y
|
||||
.saturating_div(4)
|
||||
.saturating_div(H)
|
||||
.saturating_mul(self.width as usize)
|
||||
.saturating_add(x.saturating_div(2));
|
||||
.saturating_add(x.saturating_div(W));
|
||||
// using get_mut here because we are indexing the vector with usize values
|
||||
// and we want to make sure we don't panic if the index is out of bounds
|
||||
if let Some(c) = self.utf16_code_points.get_mut(index) {
|
||||
*c |= symbols::braille::DOTS[y % 4][x % 2];
|
||||
}
|
||||
if let Some(c) = self.colors.get_mut(index) {
|
||||
*c = color;
|
||||
if let Some(cell) = self.cells.get_mut(index) {
|
||||
cell.pattern |= 1u8 << ((x % W) + W * (y % H));
|
||||
cell.color = Some(color);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -181,12 +229,16 @@ struct CharGrid {
|
||||
width: u16,
|
||||
/// Height of the grid in number of terminal rows
|
||||
height: u16,
|
||||
/// Represents a single character for each cell
|
||||
cells: Vec<char>,
|
||||
/// The color of each cell
|
||||
colors: Vec<Color>,
|
||||
cells: Vec<Option<Color>>,
|
||||
|
||||
/// The character to use for every cell - e.g. a block, dot, etc.
|
||||
cell_char: char,
|
||||
|
||||
/// If true, apply the color to the background as well as the foreground. This is used for
|
||||
/// [`Marker::Block`], so that it will overwrite any previous foreground character, but also
|
||||
/// leave a background that can be overlaid with an additional foreground character.
|
||||
apply_color_to_bg: bool,
|
||||
}
|
||||
|
||||
impl CharGrid {
|
||||
@@ -197,9 +249,16 @@ impl CharGrid {
|
||||
Self {
|
||||
width,
|
||||
height,
|
||||
cells: vec![' '; length],
|
||||
colors: vec![Color::Reset; length],
|
||||
cells: vec![None; length],
|
||||
cell_char,
|
||||
apply_color_to_bg: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_color_to_bg(self) -> Self {
|
||||
Self {
|
||||
apply_color_to_bg: true,
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -211,14 +270,20 @@ impl Grid for CharGrid {
|
||||
|
||||
fn save(&self) -> Layer {
|
||||
Layer {
|
||||
string: self.cells.iter().collect(),
|
||||
colors: self.colors.iter().map(|c| (*c, Color::Reset)).collect(),
|
||||
contents: self
|
||||
.cells
|
||||
.iter()
|
||||
.map(|&color| LayerCell {
|
||||
symbol: color.map(|_| self.cell_char),
|
||||
fg: color,
|
||||
bg: color.filter(|_| self.apply_color_to_bg),
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.cells.fill(' ');
|
||||
self.colors.fill(Color::Reset);
|
||||
self.cells.fill(None);
|
||||
}
|
||||
|
||||
fn paint(&mut self, x: usize, y: usize, color: Color) {
|
||||
@@ -226,10 +291,7 @@ impl Grid for CharGrid {
|
||||
// using get_mut here because we are indexing the vector with usize values
|
||||
// and we want to make sure we don't panic if the index is out of bounds
|
||||
if let Some(c) = self.cells.get_mut(index) {
|
||||
*c = self.cell_char;
|
||||
}
|
||||
if let Some(c) = self.colors.get_mut(index) {
|
||||
*c = color;
|
||||
*c = Some(color);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -244,7 +306,7 @@ impl Grid for CharGrid {
|
||||
/// and lower half of each cell. This allows us to draw shapes with a resolution of 1x2 "pixels" per
|
||||
/// cell.
|
||||
///
|
||||
/// This allows for more flexibility than the `BrailleGrid` which only supports a single
|
||||
/// This allows for more flexibility than the `PatternGrid` which only supports a single
|
||||
/// foreground color for each 2x4 dots cell, and the `CharGrid` which only supports a single
|
||||
/// character for each cell.
|
||||
#[derive(Debug)]
|
||||
@@ -254,7 +316,7 @@ struct HalfBlockGrid {
|
||||
/// Height of the grid in number of terminal rows
|
||||
height: u16,
|
||||
/// Represents a single color for each "pixel" arranged in column, row order
|
||||
pixels: Vec<Vec<Color>>,
|
||||
pixels: Vec<Vec<Option<Color>>>,
|
||||
}
|
||||
|
||||
impl HalfBlockGrid {
|
||||
@@ -264,7 +326,7 @@ impl HalfBlockGrid {
|
||||
Self {
|
||||
width,
|
||||
height,
|
||||
pixels: vec![vec![Color::Reset; width as usize]; (height as usize) * 2],
|
||||
pixels: vec![vec![None; width as usize]; (height as usize) * 2],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -302,45 +364,34 @@ impl Grid for HalfBlockGrid {
|
||||
.tuples()
|
||||
.flat_map(|(upper_row, lower_row)| zip(upper_row, lower_row));
|
||||
|
||||
// then we work out what character to print for each pair of pixels
|
||||
let string = vertical_color_pairs
|
||||
.clone()
|
||||
.map(|(upper, lower)| match (upper, lower) {
|
||||
(Color::Reset, Color::Reset) => ' ',
|
||||
(Color::Reset, _) => symbols::half_block::LOWER,
|
||||
(_, Color::Reset) => symbols::half_block::UPPER,
|
||||
(&lower, &upper) => {
|
||||
if lower == upper {
|
||||
symbols::half_block::FULL
|
||||
} else {
|
||||
symbols::half_block::UPPER
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// then we convert these each vertical pair of pixels into a foreground and background color
|
||||
let colors = vertical_color_pairs
|
||||
// Then we determine the character to print for each pair, along with the color of the
|
||||
// foreground and background.
|
||||
let contents = vertical_color_pairs
|
||||
.map(|(upper, lower)| {
|
||||
let (fg, bg) = match (upper, lower) {
|
||||
(Color::Reset, Color::Reset) => (Color::Reset, Color::Reset),
|
||||
(Color::Reset, &lower) => (lower, Color::Reset),
|
||||
(&upper, Color::Reset) => (upper, Color::Reset),
|
||||
(&upper, &lower) => (upper, lower),
|
||||
let (symbol, fg, bg) = match (upper, lower) {
|
||||
(None, None) => (None, None, None),
|
||||
(None, Some(lower)) => (Some(symbols::half_block::LOWER), Some(*lower), None),
|
||||
(Some(upper), None) => (Some(symbols::half_block::UPPER), Some(*upper), None),
|
||||
(Some(upper), Some(lower)) if lower == upper => {
|
||||
(Some(symbols::half_block::FULL), Some(*upper), Some(*lower))
|
||||
}
|
||||
(Some(upper), Some(lower)) => {
|
||||
(Some(symbols::half_block::UPPER), Some(*upper), Some(*lower))
|
||||
}
|
||||
};
|
||||
(fg, bg)
|
||||
LayerCell { symbol, fg, bg }
|
||||
})
|
||||
.collect();
|
||||
|
||||
Layer { string, colors }
|
||||
Layer { contents }
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.pixels.fill(vec![Color::Reset; self.width as usize]);
|
||||
self.pixels.fill(vec![None; self.width as usize]);
|
||||
}
|
||||
|
||||
fn paint(&mut self, x: usize, y: usize, color: Color) {
|
||||
self.pixels[y][x] = color;
|
||||
self.pixels[y][x] = Some(color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -462,7 +513,17 @@ impl<'a, 'b> From<&'a mut Context<'b>> for Painter<'a, 'b> {
|
||||
/// this as similar to the `Frame` struct that is used to draw widgets on the terminal.
|
||||
#[derive(Debug)]
|
||||
pub struct Context<'a> {
|
||||
// Width of the canvas in cells.
|
||||
//
|
||||
// This is NOT the resolution in dots/pixels as this varies by marker type.
|
||||
width: u16,
|
||||
// Height of the canvas in cells.
|
||||
//
|
||||
// This is NOT the resolution in dots/pixels as this varies by marker type.
|
||||
height: u16,
|
||||
// Canvas coordinate system width
|
||||
x_bounds: [f64; 2],
|
||||
// Canvas coordinate system height
|
||||
y_bounds: [f64; 2],
|
||||
grid: Box<dyn Grid>,
|
||||
dirty: bool,
|
||||
@@ -501,17 +562,10 @@ impl<'a> Context<'a> {
|
||||
y_bounds: [f64; 2],
|
||||
marker: Marker,
|
||||
) -> Self {
|
||||
let dot = symbols::DOT.chars().next().unwrap();
|
||||
let block = symbols::block::FULL.chars().next().unwrap();
|
||||
let bar = symbols::bar::HALF.chars().next().unwrap();
|
||||
let grid: Box<dyn Grid> = match marker {
|
||||
Marker::Dot => Box::new(CharGrid::new(width, height, dot)),
|
||||
Marker::Block => Box::new(CharGrid::new(width, height, block)),
|
||||
Marker::Bar => Box::new(CharGrid::new(width, height, bar)),
|
||||
Marker::Braille => Box::new(BrailleGrid::new(width, height)),
|
||||
Marker::HalfBlock => Box::new(HalfBlockGrid::new(width, height)),
|
||||
};
|
||||
let grid = Self::marker_to_grid(width, height, marker);
|
||||
Self {
|
||||
width,
|
||||
height,
|
||||
x_bounds,
|
||||
y_bounds,
|
||||
grid,
|
||||
@@ -521,6 +575,30 @@ impl<'a> Context<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn marker_to_grid(width: u16, height: u16, marker: Marker) -> Box<dyn Grid> {
|
||||
let dot = symbols::DOT.chars().next().unwrap();
|
||||
let block = symbols::block::FULL.chars().next().unwrap();
|
||||
let bar = symbols::bar::HALF.chars().next().unwrap();
|
||||
match marker {
|
||||
Marker::Block => Box::new(CharGrid::new(width, height, block).apply_color_to_bg()),
|
||||
Marker::Bar => Box::new(CharGrid::new(width, height, bar)),
|
||||
Marker::Braille => Box::new(PatternGrid::<2, 4>::new(width, height, &BRAILLE)),
|
||||
Marker::HalfBlock => Box::new(HalfBlockGrid::new(width, height)),
|
||||
Marker::Quadrant => Box::new(PatternGrid::<2, 2>::new(width, height, &QUADRANTS)),
|
||||
Marker::Sextant => Box::new(PatternGrid::<2, 3>::new(width, height, &SEXTANTS)),
|
||||
Marker::Octant => Box::new(PatternGrid::<2, 4>::new(width, height, &OCTANTS)),
|
||||
Marker::Dot | _ => Box::new(CharGrid::new(width, height, dot)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Change the marker being used in this context.
|
||||
///
|
||||
/// This will save the last layer if necessary and reset the grid to use the new marker.
|
||||
pub fn marker(&mut self, marker: Marker) {
|
||||
self.finish();
|
||||
self.grid = Self::marker_to_grid(self.width, self.height, marker);
|
||||
}
|
||||
|
||||
/// Draw the given [`Shape`] in this context
|
||||
pub fn draw<S>(&mut self, shape: &S)
|
||||
where
|
||||
@@ -573,16 +651,25 @@ impl<'a> Context<'a> {
|
||||
///
|
||||
/// By default the grid is made of Braille patterns but you may change the marker to use a different
|
||||
/// set of symbols. If your terminal or font does not support this unicode block, you will see
|
||||
/// unicode replacement characters (<28>) instead of braille dots. The Braille patterns provide a more
|
||||
/// fine grained result (2x4 dots) but you might want to use a simple dot, block, or bar instead by
|
||||
/// calling the [`marker`] method if your target environment does not support those symbols.
|
||||
/// unicode replacement characters (<28>) instead of braille dots. The Braille patterns (as well the
|
||||
/// octant character patterns) provide a more fine grained result with a 2x4 resolution per
|
||||
/// character, but you might want to use a simple dot, block, or bar instead by calling the
|
||||
/// [`marker`] method if your target environment does not support those symbols.
|
||||
///
|
||||
/// See [Unicode Braille Patterns](https://en.wikipedia.org/wiki/Braille_Patterns) for more info.
|
||||
///
|
||||
/// The `Octant` marker is similar to the `Braille` marker but, instead of sparse dots, displays
|
||||
/// densely packed and regularly spaced pseudo-pixels, without visible bands between rows and
|
||||
/// columns. However, it uses characters that are not yet as widely supported as the Braille
|
||||
/// unicode block.
|
||||
///
|
||||
/// The `Quadrant` and `Sextant` markers are in turn akin to the `Octant` marker, but with a 2x2
|
||||
/// and 2x3 resolution, respectively.
|
||||
///
|
||||
/// The `HalfBlock` marker is useful when you want to draw shapes with a higher resolution than a
|
||||
/// `CharGrid` but lower than a `BrailleGrid`. This grid type supports a foreground and background
|
||||
/// color for each terminal cell. This allows for more flexibility than the `BrailleGrid` which only
|
||||
/// supports a single foreground color for each 2x4 dots cell.
|
||||
/// `CharGrid` but lower than a `PatternGrid`. This grid type supports a foreground and background
|
||||
/// color for each terminal cell. This allows for more flexibility than the `PatternGrid` which
|
||||
/// only supports a single foreground color for each 2x4 dots cell.
|
||||
///
|
||||
/// The Canvas widget is used by calling the [`Canvas::paint`] method and passing a closure that
|
||||
/// will be used to draw on the canvas. The closure will be passed a [`Context`] object that can be
|
||||
@@ -723,7 +810,7 @@ where
|
||||
/// The [`HalfBlock`] marker is useful when you want to draw shapes with a higher resolution
|
||||
/// than with a grid of characters (e.g. with [`Block`] or [`Dot`]) but lower than with
|
||||
/// [`Braille`]. This grid type supports a foreground and background color for each terminal
|
||||
/// cell. This allows for more flexibility than the `BrailleGrid` which only supports a single
|
||||
/// cell. This allows for more flexibility than the `PatternGrid` which only supports a single
|
||||
/// foreground color for each 2x4 dots cell.
|
||||
///
|
||||
/// [`Braille`]: ratatui_core::symbols::Marker::Braille
|
||||
@@ -802,19 +889,21 @@ where
|
||||
|
||||
// Retrieve painted points for each layer
|
||||
for layer in ctx.layers {
|
||||
for (index, (ch, colors)) in layer.string.chars().zip(layer.colors).enumerate() {
|
||||
if ch != ' ' && ch != '\u{2800}' {
|
||||
let (x, y) = (
|
||||
(index % width) as u16 + canvas_area.left(),
|
||||
(index / width) as u16 + canvas_area.top(),
|
||||
);
|
||||
let cell = buf[(x, y)].set_char(ch);
|
||||
if colors.0 != Color::Reset {
|
||||
cell.set_fg(colors.0);
|
||||
}
|
||||
if colors.1 != Color::Reset {
|
||||
cell.set_bg(colors.1);
|
||||
}
|
||||
for (index, layer_cell) in layer.contents.iter().enumerate() {
|
||||
let (x, y) = (
|
||||
(index % width) as u16 + canvas_area.left(),
|
||||
(index / width) as u16 + canvas_area.top(),
|
||||
);
|
||||
let cell = &mut buf[(x, y)];
|
||||
|
||||
if let Some(symbol) = layer_cell.symbol {
|
||||
cell.set_char(symbol);
|
||||
}
|
||||
if let Some(fg) = layer_cell.fg {
|
||||
cell.set_fg(fg);
|
||||
}
|
||||
if let Some(bg) = layer_cell.bg {
|
||||
cell.set_bg(bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -847,12 +936,76 @@ where
|
||||
mod tests {
|
||||
use indoc::indoc;
|
||||
use ratatui_core::buffer::Cell;
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
|
||||
// helper to test the canvas checks that drawing a vertical and horizontal line
|
||||
// results in the expected output
|
||||
fn test_marker(marker: Marker, expected: &str) {
|
||||
#[rstest]
|
||||
#[case::block(Marker::Block, indoc!(
|
||||
"
|
||||
█xxxx
|
||||
█xxxx
|
||||
█xxxx
|
||||
█xxxx
|
||||
█████"
|
||||
))]
|
||||
#[case::half_block(Marker::HalfBlock, indoc!(
|
||||
"
|
||||
█xxxx
|
||||
█xxxx
|
||||
█xxxx
|
||||
█xxxx
|
||||
█▄▄▄▄"
|
||||
))]
|
||||
#[case::bar(Marker::Bar, indoc!(
|
||||
"
|
||||
▄xxxx
|
||||
▄xxxx
|
||||
▄xxxx
|
||||
▄xxxx
|
||||
▄▄▄▄▄"
|
||||
))]
|
||||
#[case::braille(Marker::Braille, indoc!(
|
||||
"
|
||||
⡇xxxx
|
||||
⡇xxxx
|
||||
⡇xxxx
|
||||
⡇xxxx
|
||||
⣇⣀⣀⣀⣀"
|
||||
))]
|
||||
#[case::quadrant(Marker::Quadrant, indoc!(
|
||||
"
|
||||
▌xxxx
|
||||
▌xxxx
|
||||
▌xxxx
|
||||
▌xxxx
|
||||
▙▄▄▄▄"
|
||||
))]
|
||||
#[case::sextant(Marker::Sextant, indoc!(
|
||||
"
|
||||
▌xxxx
|
||||
▌xxxx
|
||||
▌xxxx
|
||||
▌xxxx
|
||||
🬲🬭🬭🬭🬭"
|
||||
))]
|
||||
#[case::octant(Marker::Octant, indoc!(
|
||||
"
|
||||
▌xxxx
|
||||
▌xxxx
|
||||
▌xxxx
|
||||
▌xxxx
|
||||
▂▂▂▂"
|
||||
))]
|
||||
#[case::dot(Marker::Dot, indoc!(
|
||||
"
|
||||
•xxxx
|
||||
•xxxx
|
||||
•xxxx
|
||||
•xxxx
|
||||
•••••"
|
||||
))]
|
||||
fn test_horizontal_with_vertical(#[case] marker: Marker, #[case] expected: &'static str) {
|
||||
let area = Rect::new(0, 0, 5, 5);
|
||||
let mut buf = Buffer::filled(area, Cell::new("x"));
|
||||
let horizontal_line = Line {
|
||||
@@ -881,71 +1034,104 @@ mod tests {
|
||||
assert_eq!(buf, Buffer::with_lines(expected.lines()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bar_marker() {
|
||||
test_marker(
|
||||
Marker::Bar,
|
||||
indoc!(
|
||||
#[rstest]
|
||||
#[case::block(Marker::Block, indoc!(
|
||||
"
|
||||
▄xxxx
|
||||
▄xxxx
|
||||
▄xxxx
|
||||
▄xxxx
|
||||
▄▄▄▄▄"
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_block_marker() {
|
||||
test_marker(
|
||||
Marker::Block,
|
||||
indoc!(
|
||||
█xxx█
|
||||
x█x█x
|
||||
xx█xx
|
||||
x█x█x
|
||||
█xxx█"))]
|
||||
#[case::half_block(Marker::HalfBlock,
|
||||
indoc!(
|
||||
"
|
||||
█xxxx
|
||||
█xxxx
|
||||
█xxxx
|
||||
█xxxx
|
||||
█████"
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_braille_marker() {
|
||||
test_marker(
|
||||
Marker::Braille,
|
||||
indoc!(
|
||||
█xxx█
|
||||
x█x█x
|
||||
xx█xx
|
||||
x█x█x
|
||||
█xxx█")
|
||||
)]
|
||||
#[case::bar(Marker::Bar, indoc!(
|
||||
"
|
||||
⡇xxxx
|
||||
⡇xxxx
|
||||
⡇xxxx
|
||||
⡇xxxx
|
||||
⣇⣀⣀⣀⣀"
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dot_marker() {
|
||||
test_marker(
|
||||
Marker::Dot,
|
||||
indoc!(
|
||||
▄xxx▄
|
||||
x▄x▄x
|
||||
xx▄xx
|
||||
x▄x▄x
|
||||
▄xxx▄"))]
|
||||
#[case::braille(Marker::Braille, indoc!(
|
||||
"
|
||||
•xxxx
|
||||
•xxxx
|
||||
•xxxx
|
||||
•xxxx
|
||||
•••••"
|
||||
),
|
||||
);
|
||||
⢣xxx⡜
|
||||
x⢣x⡜x
|
||||
xx⣿xx
|
||||
x⡜x⢣x
|
||||
⡜xxx⢣"
|
||||
))]
|
||||
#[case::quadrant(Marker::Quadrant, indoc!(
|
||||
"
|
||||
▚xxx▞
|
||||
x▚x▞x
|
||||
xx█xx
|
||||
x▞x▚x
|
||||
▞xxx▚"
|
||||
))]
|
||||
#[case::sextant(Marker::Sextant, indoc!(
|
||||
"
|
||||
🬧xxx🬔
|
||||
x🬧x🬔x
|
||||
xx█xx
|
||||
x🬘x🬣x
|
||||
🬘xxx🬣"
|
||||
))]
|
||||
#[case::octant(Marker::Octant, indoc!(
|
||||
"
|
||||
▚xxx▞
|
||||
x▚x▞x
|
||||
xx█xx
|
||||
x▞x▚x
|
||||
▞xxx▚"
|
||||
))]
|
||||
#[case::dot(Marker::Dot, indoc!(
|
||||
"
|
||||
•xxx•
|
||||
x•x•x
|
||||
xx•xx
|
||||
x•x•x
|
||||
•xxx•"
|
||||
))]
|
||||
fn test_diagonal_lines(#[case] marker: Marker, #[case] expected: &'static str) {
|
||||
let area = Rect::new(0, 0, 5, 5);
|
||||
let mut buf = Buffer::filled(area, Cell::new("x"));
|
||||
let diagonal_up = Line {
|
||||
x1: 0.0,
|
||||
y1: 0.0,
|
||||
x2: 10.0,
|
||||
y2: 10.0,
|
||||
color: Color::Reset,
|
||||
};
|
||||
let diagonal_down = Line {
|
||||
x1: 0.0,
|
||||
y1: 10.0,
|
||||
x2: 10.0,
|
||||
y2: 0.0,
|
||||
color: Color::Reset,
|
||||
};
|
||||
Canvas::default()
|
||||
.marker(marker)
|
||||
.paint(|ctx| {
|
||||
ctx.draw(&diagonal_down);
|
||||
ctx.draw(&diagonal_up);
|
||||
})
|
||||
.x_bounds([0.0, 10.0])
|
||||
.y_bounds([0.0, 10.0])
|
||||
.render(area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(expected.lines()));
|
||||
}
|
||||
|
||||
// The canvas methods work a lot with arithmetic so here we enter various width and height
|
||||
// values to check if there are any integer overflows we just initialize the canvas painters
|
||||
#[test]
|
||||
fn check_canvas_paint_max() {
|
||||
let mut b_grid = BrailleGrid::new(u16::MAX, 2);
|
||||
let mut b_grid = PatternGrid::<2, 4>::new(u16::MAX, 2, &OCTANTS);
|
||||
let mut c_grid = CharGrid::new(u16::MAX, 2, 'd');
|
||||
|
||||
let max = u16::MAX as usize;
|
||||
@@ -964,7 +1150,7 @@ mod tests {
|
||||
// We delibately cause integer overflow to check if we don't panic and don't get weird behavior
|
||||
#[test]
|
||||
fn check_canvas_paint_overflow() {
|
||||
let mut b_grid = BrailleGrid::new(u16::MAX, 3);
|
||||
let mut b_grid = PatternGrid::<2, 4>::new(u16::MAX, 3, &BRAILLE);
|
||||
let mut c_grid = CharGrid::new(u16::MAX, 3, 'd');
|
||||
|
||||
let max = u16::MAX as usize + 10;
|
||||
|
||||
@@ -115,7 +115,7 @@ mod tests {
|
||||
"█ █",
|
||||
"██████████",
|
||||
]);
|
||||
expected.set_style(buffer.area, Style::new().red());
|
||||
expected.set_style(buffer.area, Style::new().red().on_red());
|
||||
expected.set_style(buffer.area.inner(Margin::new(1, 1)), Style::reset());
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
@@ -1017,16 +1017,18 @@ impl Widget for &Chart<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
for dataset in &self.datasets {
|
||||
Canvas::default()
|
||||
.background_color(self.style.bg.unwrap_or(Color::Reset))
|
||||
.x_bounds(self.x_axis.bounds)
|
||||
.y_bounds(self.y_axis.bounds)
|
||||
.marker(dataset.marker)
|
||||
.paint(|ctx| {
|
||||
Canvas::default()
|
||||
.background_color(self.style.bg.unwrap_or(Color::Reset))
|
||||
.x_bounds(self.x_axis.bounds)
|
||||
.y_bounds(self.y_axis.bounds)
|
||||
.paint(|ctx| {
|
||||
for dataset in &self.datasets {
|
||||
ctx.marker(dataset.marker);
|
||||
|
||||
let color = dataset.style.fg.unwrap_or(Color::Reset);
|
||||
ctx.draw(&Points {
|
||||
coords: dataset.data,
|
||||
color: dataset.style.fg.unwrap_or(Color::Reset),
|
||||
color,
|
||||
});
|
||||
match dataset.graph_type {
|
||||
GraphType::Line => {
|
||||
@@ -1036,7 +1038,7 @@ impl Widget for &Chart<'_> {
|
||||
y1: data[0].1,
|
||||
x2: data[1].0,
|
||||
y2: data[1].1,
|
||||
color: dataset.style.fg.unwrap_or(Color::Reset),
|
||||
color,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1047,15 +1049,15 @@ impl Widget for &Chart<'_> {
|
||||
y1: 0.0,
|
||||
x2: *x,
|
||||
y2: *y,
|
||||
color: dataset.style.fg.unwrap_or(Color::Reset),
|
||||
color,
|
||||
});
|
||||
}
|
||||
}
|
||||
GraphType::Scatter => {}
|
||||
}
|
||||
})
|
||||
.render(graph_area, buf);
|
||||
}
|
||||
}
|
||||
})
|
||||
.render(graph_area, buf);
|
||||
|
||||
if let Some(Position { x, y }) = layout.title_x {
|
||||
let title = self.x_axis.title.as_ref().unwrap();
|
||||
@@ -1548,6 +1550,62 @@ mod tests {
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::dot(symbols::Marker::Dot, '•')]
|
||||
#[case::dot(symbols::Marker::Braille, '⢣')]
|
||||
fn overlapping_lines(#[case] marker: symbols::Marker, #[case] symbol: char) {
|
||||
let data_diagonal_up = [(0.0, 0.0), (5.0, 5.0)];
|
||||
let data_diagonal_down = [(0.0, 5.0), (5.0, 0.0)];
|
||||
let lines = vec![
|
||||
Dataset::default()
|
||||
.data(&data_diagonal_up)
|
||||
.marker(symbols::Marker::Block)
|
||||
.graph_type(GraphType::Line)
|
||||
.blue(),
|
||||
Dataset::default()
|
||||
.data(&data_diagonal_down)
|
||||
.marker(marker)
|
||||
.graph_type(GraphType::Line)
|
||||
.red(),
|
||||
];
|
||||
let chart = Chart::new(lines)
|
||||
.x_axis(Axis::default().bounds([0.0, 5.0]))
|
||||
.y_axis(Axis::default().bounds([0.0, 5.0]));
|
||||
let area = Rect::new(0, 0, 5, 5);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
chart.render(buffer.area, &mut buffer);
|
||||
#[rustfmt::skip]
|
||||
let mut expected = Buffer::with_lines([
|
||||
format!("{symbol} █"),
|
||||
format!(" {symbol} █ "),
|
||||
format!(" {symbol} "),
|
||||
format!(" █ {symbol} "),
|
||||
format!("█ {symbol}"),
|
||||
]);
|
||||
for i in 0..5 {
|
||||
// The Marker::Dot and Marker::Braille tiles have the
|
||||
// foreground set to Red.
|
||||
expected.set_style(Rect::new(i, i, 1, 1), Style::new().fg(Color::Red));
|
||||
// The Marker::Block tiles have both the foreground and
|
||||
// background set to Blue.
|
||||
expected.set_style(
|
||||
Rect::new(i, 4 - i, 1, 1),
|
||||
Style::new().fg(Color::Blue).bg(Color::Blue),
|
||||
);
|
||||
}
|
||||
// Where the Marker::Dot/Braille overlaps with Marker::Block,
|
||||
// the background is set to blue from the Block, but the
|
||||
// foreground is set to red from the Dot/Braille. This allows
|
||||
// two line plots to overlap, so long as one of them is a
|
||||
// Block.
|
||||
expected.set_style(
|
||||
Rect::new(2, 2, 1, 1),
|
||||
Style::new().fg(Color::Red).bg(Color::Blue),
|
||||
);
|
||||
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_in_minimal_buffer() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "ratatui"
|
||||
description = "A library that's all about cooking up terminal user interfaces"
|
||||
version = "0.30.0-alpha.5"
|
||||
version = "0.30.0"
|
||||
authors.workspace = true
|
||||
documentation.workspace = true
|
||||
repository.workspace = true
|
||||
@@ -32,6 +32,10 @@ default = ["crossterm", "underline-color", "all-widgets", "macros", "layout-cach
|
||||
#! Generally an application will only use one backend, so you should only enable one of the following features:
|
||||
## enables the [`CrosstermBackend`](backend::CrosstermBackend) backend and adds a dependency on [`crossterm`].
|
||||
crossterm = ["std", "dep:ratatui-crossterm"]
|
||||
## selects the crossterm 0.28.x backend implementation
|
||||
crossterm_0_28 = ["crossterm", "ratatui-crossterm/crossterm_0_28"]
|
||||
## selects the crossterm 0.29.x backend implementation (default)
|
||||
crossterm_0_29 = ["crossterm", "ratatui-crossterm/crossterm_0_29"]
|
||||
## enables the [`TermionBackend`](backend::TermionBackend) backend and adds a dependency on [`termion`].
|
||||
termion = ["std", "dep:ratatui-termion"]
|
||||
## enables the [`TermwizBackend`](backend::TermwizBackend) backend and adds a dependency on [`termwiz`].
|
||||
|
||||
@@ -3,6 +3,7 @@ pub mod main {
|
||||
pub mod block;
|
||||
pub mod buffer;
|
||||
pub mod constraints;
|
||||
pub mod gauge;
|
||||
pub mod line;
|
||||
pub mod list;
|
||||
pub mod paragraph;
|
||||
@@ -25,4 +26,5 @@ criterion::criterion_main!(
|
||||
table::benches,
|
||||
text::benches,
|
||||
constraints::benches,
|
||||
gauge::benches,
|
||||
);
|
||||
|
||||
@@ -52,7 +52,7 @@ fn barchart(c: &mut Criterion) {
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Render the widget in a classical size buffer
|
||||
/// Render the widget in a classical size buffer.
|
||||
fn render(bencher: &mut Bencher, barchart: &BarChart) {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 200, 50));
|
||||
// We use `iter_batched` to clone the value in the setup function.
|
||||
|
||||
@@ -36,7 +36,7 @@ fn block(c: &mut Criterion) {
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// render the block into a buffer of the given `size`
|
||||
/// Render the block into a buffer of the given `size`.
|
||||
fn render(bencher: &mut Bencher, block: &Block, size: Rect) {
|
||||
let mut buffer = Buffer::empty(size);
|
||||
// We use `iter_batched` to clone the value in the setup function.
|
||||
|
||||
58
ratatui/benches/main/gauge.rs
Normal file
58
ratatui/benches/main/gauge.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
use criterion::{BatchSize, Criterion, criterion_group};
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::Style;
|
||||
use ratatui::widgets::{Block, Gauge, Widget};
|
||||
|
||||
/// Benchmark for rendering a gauge.
|
||||
fn gauge(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("gauge");
|
||||
|
||||
let (width, height) = (200, 50); // 1080p fullscreen with medium font
|
||||
let buffer_size = Rect::new(0, 0, width, height);
|
||||
|
||||
// Render an empty gauge
|
||||
group.bench_with_input(
|
||||
format!("render_empty/{width}x{height}"),
|
||||
&Gauge::default(),
|
||||
|b, gauge| {
|
||||
let mut buffer = Buffer::empty(buffer_size);
|
||||
// We use `iter_batched` to clone the value in the setup function because
|
||||
// `Widget::render` consumes the widget.
|
||||
b.iter_batched(
|
||||
|| gauge.to_owned(),
|
||||
|bench_gauge| {
|
||||
bench_gauge.render(buffer.area, &mut buffer);
|
||||
},
|
||||
BatchSize::SmallInput,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// Render with all features
|
||||
group.bench_with_input(
|
||||
format!("render_all_feature/{width}x{height}"),
|
||||
&Gauge::default()
|
||||
.block(Block::bordered().title("Progress"))
|
||||
.gauge_style(Style::new().white().on_black().italic())
|
||||
.percent(20)
|
||||
.label("20%")
|
||||
.use_unicode(true),
|
||||
|b, gauge| {
|
||||
let mut buffer = Buffer::empty(buffer_size);
|
||||
// We use `iter_batched` to clone the value in the setup function because
|
||||
// `Widget::render` consumes the widget.
|
||||
b.iter_batched(
|
||||
|| gauge.to_owned(),
|
||||
|bench_gauge| {
|
||||
bench_gauge.render(buffer.area, &mut buffer);
|
||||
},
|
||||
BatchSize::SmallInput,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(benches, gauge);
|
||||
@@ -39,7 +39,7 @@ fn list(c: &mut Criterion) {
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// render the list into a common size buffer
|
||||
/// Render the list into a common size buffer.
|
||||
fn render(bencher: &mut Bencher, list: &List) {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 200, 50));
|
||||
// We use `iter_batched` to clone the value in the setup function.
|
||||
|
||||
@@ -67,7 +67,7 @@ fn paragraph(c: &mut Criterion) {
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// render the paragraph into a buffer with the given width
|
||||
/// Render the paragraph into a buffer with the given width.
|
||||
fn render(bencher: &mut Bencher, paragraph: &Paragraph, width: u16) {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, width, PARAGRAPH_DEFAULT_HEIGHT));
|
||||
// We use `iter_batched` to clone the value in the setup function.
|
||||
|
||||
@@ -25,7 +25,7 @@ fn sparkline(c: &mut Criterion) {
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// render the block into a buffer of the given `size`
|
||||
/// Render the block into a buffer of the given `size`.
|
||||
fn render(bencher: &mut Bencher, sparkline: &Sparkline) {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 200, 50));
|
||||
// We use `iter_batched` to clone the value in the setup function.
|
||||
|
||||
@@ -46,7 +46,7 @@ fn text(c: &mut Criterion) {
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Renders the text into a buffer of the given `size`
|
||||
/// Render the text into a buffer of the given `size`.
|
||||
fn render(bencher: &mut Bencher, text: &Text, size: Rect) {
|
||||
let mut buffer = Buffer::empty(size);
|
||||
// We use `iter_batched` to clone the value in the setup function.
|
||||
|
||||
@@ -616,6 +616,9 @@
|
||||
//! library `no_std` compatible. This is often easier than you might expect and broadens the range
|
||||
//! of projects that can use your widgets.
|
||||
//!
|
||||
//! For more detail on advantages of this, maintenance tips and feature flags, see the
|
||||
//! [no-std concept guide].
|
||||
//!
|
||||
//! To implement `no_std` compatibility, add the `#![no_std]` attribute at the top of your `lib.rs`.
|
||||
//! When working in a `no_std` environment, you'll need to make a few adjustments:
|
||||
//!
|
||||
@@ -658,6 +661,7 @@
|
||||
//! straightforward to implement
|
||||
//!
|
||||
//! [`ratatui-core`]: https://crates.io/crates/ratatui-core
|
||||
//! [no-std concept guide]: https://ratatui.rs/concepts/no-std/
|
||||
|
||||
pub use ratatui_core::widgets::{StatefulWidget, Widget};
|
||||
pub use ratatui_widgets::barchart::{Bar, BarChart, BarGroup};
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
[default.extend-words]
|
||||
ratatui = "ratatui"
|
||||
sectore = "sectore" # https://github.com/sectore
|
||||
|
||||
[type.md]
|
||||
extend-ignore-re = [
|
||||
|
||||
Reference in New Issue
Block a user