Compare commits
72 Commits
ratatui-cr
...
release-pl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6dd59b3bf9 | ||
|
|
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 |
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@0b1efabc08b657293548b77fb76cc02d26091c7e # 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@d850aa816998e5cf15f67a78c7b933f2a5033f8a # 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@0b1efabc08b657293548b77fb76cc02d26091c7e # 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@0b1efabc08b657293548b77fb76cc02d26091c7e # master
|
||||
with:
|
||||
toolchain: stable
|
||||
components: llvm-tools
|
||||
- uses: taiki-e/install-action@c5b1b6f479c32f356cc6f4ba672a47f63853b13b # v2
|
||||
- uses: taiki-e/install-action@d850aa816998e5cf15f67a78c7b933f2a5033f8a # 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@0b1efabc08b657293548b77fb76cc02d26091c7e # master
|
||||
with:
|
||||
toolchain: ${{ matrix.toolchain }}
|
||||
- uses: taiki-e/install-action@c5b1b6f479c32f356cc6f4ba672a47f63853b13b # v2
|
||||
- uses: taiki-e/install-action@d850aa816998e5cf15f67a78c7b933f2a5033f8a # 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@0b1efabc08b657293548b77fb76cc02d26091c7e # 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@d850aa816998e5cf15f67a78c7b933f2a5033f8a # 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@0b1efabc08b657293548b77fb76cc02d26091c7e # 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@d850aa816998e5cf15f67a78c7b933f2a5033f8a # 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@0b1efabc08b657293548b77fb76cc02d26091c7e # master
|
||||
with:
|
||||
toolchain: stable
|
||||
- uses: taiki-e/install-action@c5b1b6f479c32f356cc6f4ba672a47f63853b13b # v2
|
||||
- uses: taiki-e/install-action@d850aa816998e5cf15f67a78c7b933f2a5033f8a # 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@0b1efabc08b657293548b77fb76cc02d26091c7e # master
|
||||
with:
|
||||
toolchain: stable
|
||||
- uses: taiki-e/install-action@c5b1b6f479c32f356cc6f4ba672a47f63853b13b # v2
|
||||
- uses: taiki-e/install-action@d850aa816998e5cf15f67a78c7b933f2a5033f8a # 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@0b1efabc08b657293548b77fb76cc02d26091c7e # 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@0b1efabc08b657293548b77fb76cc02d26091c7e # 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@0b1efabc08b657293548b77fb76cc02d26091c7e # 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.
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file.
|
||||
<!-- ignore lint rules that are often triggered by content generated from commits / git-cliff -->
|
||||
<!-- markdownlint-disable line-length no-bare-urls ul-style emphasis-style -->
|
||||
|
||||
|
||||
## [v0.30.0-beta.0](https://github.com/ratatui/ratatui/releases/tag/v0.30.0-beta.0) - 2025-10-28
|
||||
|
||||
> _"Rats don't just survive; they discover; they create. ... I mean, just look at what they do with
|
||||
|
||||
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-beta.0" }
|
||||
ratatui-core = { path = "ratatui-core", version = "0.1.0-beta.0" }
|
||||
ratatui-crossterm = { path = "ratatui-crossterm", version = "0.1.0-beta.0" }
|
||||
ratatui-macros = { path = "ratatui-macros", version = "0.7.0-beta.0" }
|
||||
ratatui-termion = { path = "ratatui-termion", version = "0.1.0-beta.0" }
|
||||
ratatui-termwiz = { path = "ratatui-termwiz", version = "0.1.0-beta.0" }
|
||||
ratatui-widgets = { path = "ratatui-widgets", version = "0.3.0-beta.0" }
|
||||
ratatui = { path = "ratatui", version = "0.30.0-beta.1" }
|
||||
ratatui-core = { path = "ratatui-core", version = "0.1.0-beta.1" }
|
||||
ratatui-crossterm = { path = "ratatui-crossterm", version = "0.1.0-beta.1" }
|
||||
ratatui-macros = { path = "ratatui-macros", version = "0.7.0-beta.1" }
|
||||
ratatui-termion = { path = "ratatui-termion", version = "0.1.0-beta.1" }
|
||||
ratatui-termwiz = { path = "ratatui-termwiz", version = "0.1.0-beta.1" }
|
||||
ratatui-widgets = { path = "ratatui-widgets", version = "0.3.0-beta.1" }
|
||||
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"
|
||||
|
||||
@@ -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-beta.1"
|
||||
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-beta.0"
|
||||
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-beta.0"
|
||||
version = "0.1.0-beta.1"
|
||||
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,9 @@
|
||||
[package]
|
||||
name = "ratatui-macros"
|
||||
version = "0.7.0-beta.0"
|
||||
version = "0.7.0-beta.1"
|
||||
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-beta.0"
|
||||
version = "0.1.0-beta.1"
|
||||
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-beta.0"
|
||||
version = "0.1.0-beta.1"
|
||||
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-beta.0"
|
||||
version = "0.3.0-beta.1"
|
||||
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
|
||||
@@ -68,7 +68,7 @@ unicode-width.workspace = true
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
pretty_assertions.workspace = true
|
||||
ratatui = { path = "../ratatui" }
|
||||
ratatui.workspace = true
|
||||
rstest.workspace = true
|
||||
|
||||
[lints]
|
||||
|
||||
@@ -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-beta.0"
|
||||
version = "0.30.0-beta.1"
|
||||
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.
|
||||
|
||||
@@ -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