Compare commits
228 Commits
revert-583
...
v0.27.0-al
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8719608bda | ||
|
|
f6c4e447e6 | ||
|
|
c56f49b9fb | ||
|
|
078e97e4ff | ||
|
|
8e68db9e2f | ||
|
|
541f0f9953 | ||
|
|
88bfb5a430 | ||
|
|
c4ce7e8ff6 | ||
|
|
3be189e3c6 | ||
|
|
5c4efacd1d | ||
|
|
bbb6d65e06 | ||
|
|
fdb14dc7cd | ||
|
|
9b3b23ac14 | ||
|
|
58b6e0be0f | ||
|
|
6e6ba27a12 | ||
|
|
c870a41057 | ||
|
|
a6036ad789 | ||
|
|
060d26b6dc | ||
|
|
a4e84a6a7f | ||
|
|
fcbea9ee68 | ||
|
|
14b24e7585 | ||
|
|
5ed1f43c62 | ||
|
|
c8c7924e0c | ||
|
|
e3afe7c8a1 | ||
|
|
a1f54de7d6 | ||
|
|
b8ea190bf2 | ||
|
|
0de5238ed3 | ||
|
|
df5dddfbc9 | ||
|
|
f1398ae6cb | ||
|
|
525848ff4e | ||
|
|
660c7183c7 | ||
|
|
ab951fae81 | ||
|
|
3cd4369176 | ||
|
|
9bc014d7f1 | ||
|
|
36a0cd56e5 | ||
|
|
b831c5688c | ||
|
|
8195f526cb | ||
|
|
f7f66928a8 | ||
|
|
01418eb7c2 | ||
|
|
8536760e78 | ||
|
|
a558b19c9a | ||
|
|
183c07ef43 | ||
|
|
a13867ffce | ||
|
|
5b00e3aae9 | ||
|
|
38c17e091c | ||
|
|
3834374652 | ||
|
|
27680c05ce | ||
|
|
6fd5f631bb | ||
|
|
37b957c7e1 | ||
|
|
e02f4768ce | ||
|
|
c12bcfefa2 | ||
|
|
94f4547dcf | ||
|
|
3a6b8808ed | ||
|
|
1cff511934 | ||
|
|
b5bdde079e | ||
|
|
654949bb00 | ||
|
|
943c0431d9 | ||
|
|
65e7923753 | ||
|
|
d0067c8815 | ||
|
|
35e971f7eb | ||
|
|
b0314c5731 | ||
|
|
12f67e810f | ||
|
|
11b452d56f | ||
|
|
efd1e47642 | ||
|
|
410d08b2b5 | ||
|
|
a4892ad444 | ||
|
|
18870ce990 | ||
|
|
1f208ffd03 | ||
|
|
e51ca6e0d2 | ||
|
|
9182f47026 | ||
|
|
91040c0865 | ||
|
|
2202059259 | ||
|
|
8fb46301a0 | ||
|
|
0dcdbea083 | ||
|
|
74a051147a | ||
|
|
c3fb25898f | ||
|
|
fae5862c6e | ||
|
|
788e6d9fb8 | ||
|
|
14c67fbb52 | ||
|
|
096346350e | ||
|
|
61a827821d | ||
|
|
fbb5dfaaa9 | ||
|
|
d2d91f754c | ||
|
|
bcf43688ec | ||
|
|
b7942ee252 | ||
|
|
c8dd87918d | ||
|
|
652dc469ea | ||
|
|
87bf1dd9df | ||
|
|
f8367fdfdd | ||
|
|
9ba7354335 | ||
|
|
dab08b99b6 | ||
|
|
2a12f7bddf | ||
|
|
4278b4088d | ||
|
|
86168aa711 | ||
|
|
78f1c1446b | ||
|
|
9ec43eff1c | ||
|
|
4ee4e6d78a | ||
|
|
525479546a | ||
|
|
cf861232c7 | ||
|
|
dd5ca3a0c8 | ||
|
|
aeec16369b | ||
|
|
eb31617539 | ||
|
|
1ad629acd7 | ||
|
|
04e6ccd448 | ||
|
|
540fd2df03 | ||
|
|
984afd580b | ||
|
|
bbcfa55a88 | ||
|
|
1cbe1f52ab | ||
|
|
663bbde9c3 | ||
|
|
27e9216cea | ||
|
|
be4fdaa0c7 | ||
|
|
c1ed5c3637 | ||
|
|
4b8e54e811 | ||
|
|
5b7ad2ad82 | ||
|
|
ba20372c23 | ||
|
|
f383625f0e | ||
|
|
847bacf32e | ||
|
|
815757fcbb | ||
|
|
736605ec88 | ||
|
|
6e76729ce8 | ||
|
|
7f42ec9713 | ||
|
|
d7132011f9 | ||
|
|
d726e928d2 | ||
|
|
b80264de87 | ||
|
|
804c841fdc | ||
|
|
79ceb9f7b6 | ||
|
|
f780be31f3 | ||
|
|
eb1484b6db | ||
|
|
6ecaeed549 | ||
|
|
a489d85f2d | ||
|
|
065b6b05b7 | ||
|
|
1e755967c5 | ||
|
|
1d3fbc1b15 | ||
|
|
330a899eac | ||
|
|
405a125c82 | ||
|
|
b3a57f3dff | ||
|
|
2819eea82b | ||
|
|
41de8846fd | ||
|
|
68d5783a69 | ||
|
|
d49bbb2590 | ||
|
|
fd4703c086 | ||
|
|
0df935473f | ||
|
|
9df6cebb58 | ||
|
|
f299463847 | ||
|
|
4d262d21cb | ||
|
|
ae6a2b0007 | ||
|
|
f71bf18297 | ||
|
|
cddf4b2930 | ||
|
|
813f707892 | ||
|
|
48b0380cb3 | ||
|
|
c959bd2881 | ||
|
|
5131c813ce | ||
|
|
11e4f6a0ba | ||
|
|
cc6737b8bc | ||
|
|
3e7810a2ab | ||
|
|
fc0879f98d | ||
|
|
bb5444f618 | ||
|
|
e0aa6c5e1f | ||
|
|
1746a61659 | ||
|
|
7a8af8da6b | ||
|
|
dfd6db988f | ||
|
|
151db6ac7d | ||
|
|
de97a1f1da | ||
|
|
9a3815b66d | ||
|
|
425a65140b | ||
|
|
fe06f0c7b0 | ||
|
|
43b2b57191 | ||
|
|
50b81c9d4e | ||
|
|
2b4aa46a6a | ||
|
|
e1e85aa7af | ||
|
|
bf67850739 | ||
|
|
1561d64c80 | ||
|
|
ffd5fc79fc | ||
|
|
34648941d4 | ||
|
|
c69ca47922 | ||
|
|
f29c73fb1c | ||
|
|
f2eab71ccf | ||
|
|
6645d2e058 | ||
|
|
2faa879658 | ||
|
|
eb79256cee | ||
|
|
388aa467f1 | ||
|
|
8dd177a051 | ||
|
|
bbf2f906fb | ||
|
|
c24216cf30 | ||
|
|
5db895dcbf | ||
|
|
c50ff08a63 | ||
|
|
06141900b4 | ||
|
|
fab943b61a | ||
|
|
a67815e138 | ||
|
|
bd6b91c958 | ||
|
|
5aba988fac | ||
|
|
e64e194b6b | ||
|
|
bc274e2bd9 | ||
|
|
f13fd73d9e | ||
|
|
fe84141119 | ||
|
|
fb93db0730 | ||
|
|
23f6938498 | ||
|
|
98bcf1c0a5 | ||
|
|
803a72df27 | ||
|
|
0494ee52f1 | ||
|
|
6d15b2570f | ||
|
|
659460e19c | ||
|
|
ba036cd579 | ||
|
|
8724aeb9e7 | ||
|
|
da6c299804 | ||
|
|
50374b2456 | ||
|
|
49df5d4626 | ||
|
|
7ab12ed8ce | ||
|
|
b459228e26 | ||
|
|
8f56fabcdd | ||
|
|
a62632a947 | ||
|
|
f025d2bfa2 | ||
|
|
63645333d6 | ||
|
|
5d410c6895 | ||
|
|
8d77b734bb | ||
|
|
9574198958 | ||
|
|
ee54493163 | ||
|
|
c977293f14 | ||
|
|
b0ed658970 | ||
|
|
37c183636b | ||
|
|
e67d3c64e0 | ||
|
|
4f2db82a77 | ||
|
|
d6b851301e | ||
|
|
b7a479392e | ||
|
|
e1cc849554 | ||
|
|
7f5884829c | ||
|
|
a15c3b2660 | ||
|
|
41c44a4af6 |
@@ -2,6 +2,12 @@
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.rs]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -5,4 +5,4 @@
|
||||
# https://git-scm.com/docs/gitignore#_pattern_format
|
||||
|
||||
# Maintainers
|
||||
* @orhun @mindoodoo @sayanarijit @joshka @kdheepak @Valentin271
|
||||
* @orhun @mindoodoo @sayanarijit @joshka @kdheepak @Valentin271 @EdJoPaTo
|
||||
|
||||
52
.github/workflows/calculate-alpha-release.bash
vendored
Executable file
52
.github/workflows/calculate-alpha-release.bash
vendored
Executable file
@@ -0,0 +1,52 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Exit on error. Append "|| true" if you expect an error.
|
||||
set -o errexit
|
||||
# Exit on error inside any functions or subshells.
|
||||
set -o errtrace
|
||||
# Do not allow use of undefined vars. Use ${VAR:-} to use an undefined VAR
|
||||
set -o nounset
|
||||
# Catch the error in case mysqldump fails (but gzip succeeds) in `mysqldump |gzip`
|
||||
set -o pipefail
|
||||
# Turn on traces, useful while debugging but commented out by default
|
||||
# set -o xtrace
|
||||
|
||||
last_release="$(git tag --sort=committerdate | grep -P "v0+\.\d+\.\d+$" | tail -1)"
|
||||
echo "🐭 Last release: ${last_release}"
|
||||
|
||||
# detect breaking changes
|
||||
if [ -n "$(git log --oneline ${last_release}..HEAD | grep '!:')" ]; then
|
||||
echo "🐭 Breaking changes detected since ${last_release}"
|
||||
git log --oneline ${last_release}..HEAD | grep '!:'
|
||||
# increment the minor version
|
||||
minor="${last_release##v0.}"
|
||||
minor="${minor%.*}"
|
||||
next_minor="$((minor + 1))"
|
||||
next_release="v0.${next_minor}.0"
|
||||
else
|
||||
# increment the patch version
|
||||
patch="${last_release##*.}"
|
||||
next_patch="$((patch + 1))"
|
||||
next_release="${last_release/%${patch}/${next_patch}}"
|
||||
fi
|
||||
echo "🐭 Next release: ${next_release}"
|
||||
|
||||
suffix="alpha"
|
||||
last_tag="$(git tag --sort=committerdate | tail -1)"
|
||||
if [[ "${last_tag}" = "${next_release}-${suffix}"* ]]; then
|
||||
echo "🐭 Last alpha release: ${last_tag}"
|
||||
# increment the alpha version
|
||||
# e.g. v0.22.1-alpha.12 -> v0.22.1-alpha.13
|
||||
alpha="${last_tag##*-${suffix}.}"
|
||||
next_alpha="$((alpha + 1))"
|
||||
next_tag="${last_tag/%${alpha}/${next_alpha}}"
|
||||
else
|
||||
# increment the patch and start the alpha version from 0
|
||||
# e.g. v0.22.0 -> v0.22.1-alpha.0
|
||||
next_tag="${next_release}-${suffix}.0"
|
||||
fi
|
||||
# update the crate version
|
||||
msg="# crate version"
|
||||
sed -E -i "s/^version = .* ${msg}$/version = \"${next_tag#v}\" ${msg}/" Cargo.toml
|
||||
echo "NEXT_TAG=${next_tag}" >> $GITHUB_ENV
|
||||
echo "🐭 Next alpha release: ${next_tag}"
|
||||
24
.github/workflows/cd.yml
vendored
24
.github/workflows/cd.yml
vendored
@@ -28,27 +28,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Calculate the next release
|
||||
run: |
|
||||
suffix="alpha"
|
||||
last_tag="$(git tag --sort=committerdate | tail -1)"
|
||||
if [[ "${last_tag}" = *"-${suffix}"* ]]; then
|
||||
# increment the alpha version
|
||||
# e.g. v0.22.1-alpha.12 -> v0.22.1-alpha.13
|
||||
alpha="${last_tag##*-${suffix}.}"
|
||||
next_alpha="$((alpha + 1))"
|
||||
next_tag="${last_tag/%${alpha}/${next_alpha}}"
|
||||
else
|
||||
# increment the patch and start the alpha version from 0
|
||||
# e.g. v0.22.0 -> v0.22.1-alpha.0
|
||||
patch="${last_tag##*.}"
|
||||
next_patch="$((patch + 1))"
|
||||
next_tag="${last_tag/%${patch}/${next_patch}}-${suffix}.0"
|
||||
fi
|
||||
# update the crate version
|
||||
msg="# crate version"
|
||||
sed -E -i "s/^version = .* ${msg}$/version = \"${next_tag#v}\" ${msg}/" Cargo.toml
|
||||
echo "NEXT_TAG=${next_tag}" >> $GITHUB_ENV
|
||||
echo "Next alpha release: ${next_tag} 🐭"
|
||||
run: .github/workflows/calculate-alpha-release.bash
|
||||
|
||||
- name: Publish on crates.io
|
||||
uses: actions-rs/cargo@v1
|
||||
@@ -57,7 +37,7 @@ jobs:
|
||||
args: --allow-dirty --token ${{ secrets.CARGO_TOKEN }}
|
||||
|
||||
- name: Generate a changelog
|
||||
uses: orhun/git-cliff-action@v2
|
||||
uses: orhun/git-cliff-action@v3
|
||||
with:
|
||||
config: cliff.toml
|
||||
args: --unreleased --tag ${{ env.NEXT_TAG }} --strip header
|
||||
|
||||
1
.github/workflows/check-pr.yml
vendored
1
.github/workflows/check-pr.yml
vendored
@@ -84,4 +84,3 @@ jobs:
|
||||
echo "Pull request is labeled as 'do not merge'"
|
||||
echo "This workflow fails so that the pull request cannot be merged"
|
||||
exit 1
|
||||
|
||||
|
||||
39
.github/workflows/ci.yml
vendored
39
.github/workflows/ci.yml
vendored
@@ -29,28 +29,19 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: actions/checkout@v4
|
||||
- name: Checkout
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- name: Install Rust nightly
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
with:
|
||||
components: rustfmt
|
||||
- name: Install cargo-make
|
||||
uses: taiki-e/install-action@cargo-make
|
||||
- name: Cache Cargo dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
- name: Check formatting
|
||||
run: cargo make lint-format
|
||||
- name: Check documentation
|
||||
run: cargo make lint-docs
|
||||
- name: Check conventional commits
|
||||
uses: crate-ci/committed@master
|
||||
with:
|
||||
args: "-vv"
|
||||
commits: HEAD
|
||||
- name: Check typos
|
||||
uses: crate-ci/typos@master
|
||||
- name: Lint dependencies
|
||||
@@ -67,6 +58,8 @@ jobs:
|
||||
components: clippy
|
||||
- name: Install cargo-make
|
||||
uses: taiki-e/install-action@cargo-make
|
||||
- name: Cache Cargo dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
- name: Run cargo make clippy-all
|
||||
run: cargo make clippy
|
||||
|
||||
@@ -83,10 +76,12 @@ jobs:
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-llvm-cov,cargo-make
|
||||
- name: Cache Cargo dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
- name: Generate coverage
|
||||
run: cargo make coverage
|
||||
- name: Upload to codecov.io
|
||||
uses: codecov/codecov-action@v3
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: true
|
||||
@@ -95,8 +90,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ ubuntu-latest, windows-latest, macos-latest ]
|
||||
toolchain: [ "1.70.0", "stable" ]
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
toolchain: ["1.74.0", "stable"]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -107,6 +102,8 @@ jobs:
|
||||
toolchain: ${{ matrix.toolchain }}
|
||||
- name: Install cargo-make
|
||||
uses: taiki-e/install-action@cargo-make
|
||||
- name: Cache Cargo dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
- name: Run cargo make check
|
||||
run: cargo make check
|
||||
env:
|
||||
@@ -116,7 +113,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ ubuntu-latest, windows-latest, macos-latest ]
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -125,6 +122,8 @@ jobs:
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: Install cargo-make
|
||||
uses: taiki-e/install-action@cargo-make
|
||||
- name: Cache Cargo dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
- name: Test docs
|
||||
run: cargo make test-doc
|
||||
env:
|
||||
@@ -134,9 +133,9 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ ubuntu-latest, windows-latest, macos-latest ]
|
||||
toolchain: [ "1.70.0", "stable" ]
|
||||
backend: [ crossterm, termion, termwiz ]
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
toolchain: ["1.74.0", "stable"]
|
||||
backend: [crossterm, termion, termwiz]
|
||||
exclude:
|
||||
# termion is not supported on windows
|
||||
- os: windows-latest
|
||||
@@ -151,6 +150,10 @@ jobs:
|
||||
toolchain: ${{ matrix.toolchain }}
|
||||
- name: Install cargo-make
|
||||
uses: taiki-e/install-action@cargo-make
|
||||
- name: Install cargo-nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
- name: Cache Cargo dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
- name: Test ${{ matrix.backend }}
|
||||
run: cargo make test-backend ${{ matrix.backend }}
|
||||
env:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Breaking Changes
|
||||
|
||||
This document contains a list of breaking changes in each version and some notes to help migrate
|
||||
between versions. It is compile manually from the commit history and changelog. We also tag PRs on
|
||||
between versions. It is compiled manually from the commit history and changelog. We also tag PRs on
|
||||
github with a [breaking change] label.
|
||||
|
||||
[breaking change]: (https://github.com/ratatui-org/ratatui/issues?q=label%3A%22breaking+change%22)
|
||||
@@ -10,14 +10,21 @@ github with a [breaking change] label.
|
||||
|
||||
This is a quick summary of the sections below:
|
||||
|
||||
- Unreleased (0.24.1)
|
||||
- [v0.26.0](#v0260)
|
||||
- `Flex::Start` is the new default flex mode for `Layout`
|
||||
- `patch_style` & `reset_style` now consume and return `Self`
|
||||
- Removed deprecated `Block::title_on_bottom`
|
||||
- `Line` now has an extra `style` field which applies the style to the entire line
|
||||
- `Block` style methods cannot be created in a const context
|
||||
- `Tabs::new()` now accepts `IntoIterator<Item: Into<Line<'a>>>`
|
||||
- `Table::new` now accepts `IntoIterator<Item: Into<Row<'a>>>`.
|
||||
- [v0.25.0](#v0250)
|
||||
- Removed `Axis::title_style` and `Buffer::set_background`
|
||||
- `List::new()` now accepts `IntoIterator<Item = Into<ListItem<'a>>>`
|
||||
- `Table::new()` now requires specifying the widths
|
||||
- `Table::widths()` now accepts `IntoIterator<Item = AsRef<Constraint>>`
|
||||
- Layout::new() now accepts direction and constraint parameters
|
||||
- The default `Tabs::highlight_style` is now `Style::new().reversed()`
|
||||
|
||||
- [v0.24.0](#v0240)
|
||||
- MSRV is now 1.70.0
|
||||
- `ScrollbarState`: `position`, `content_length`, and `viewport_content_length` are now `usize`
|
||||
@@ -40,14 +47,144 @@ This is a quick summary of the sections below:
|
||||
- MSRV is now 1.63.0
|
||||
- `List` no longer ignores empty strings
|
||||
|
||||
## Unreleased (v0.24.1)
|
||||
## [v0.26.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.26.0)
|
||||
|
||||
### Removed `Axis::title_style` and `Buffer::set_background`
|
||||
### `Flex::Start` is the new default flex mode for `Layout`
|
||||
|
||||
[#881]: https://github.com/ratatui-org/ratatui/pull/881
|
||||
|
||||
Previously, constraints would stretch to fill all available space, violating constraints if
|
||||
necessary.
|
||||
|
||||
With v0.26.0, `Flex` modes are introduced and the default is `Flex::Start`, which will align
|
||||
areas associated with constraints to be beginning of the area. With v0.26.0, additionally,
|
||||
`Min` constraints grow to fill excess space. These changes will allow users to build layouts
|
||||
more easily.
|
||||
|
||||
With v0.26.0, users will most likely not need to change what constraints they use to create
|
||||
existing layouts with `Flex::Start`. However, to get old behavior, use `Flex::Legacy`.
|
||||
|
||||
```diff
|
||||
- let rects = Layout::horizontal([Length(1), Length(2)]).split(area);
|
||||
// becomes
|
||||
+ let rects = Layout::horizontal([Length(1), Length(2)]).flex(Flex::Legacy).split(area);
|
||||
```
|
||||
|
||||
### `Table::new()` now accepts `IntoIterator<Item: Into<Row<'a>>>` ([#774])
|
||||
|
||||
[#774]: https://github.com/ratatui-org/ratatui/pull/774
|
||||
|
||||
Previously, `Table::new()` accepted `IntoIterator<Item=Row<'a>>`. The argument change to
|
||||
`IntoIterator<Item: Into<Row<'a>>>`, This allows more flexible types from calling scopes, though it
|
||||
can some break type inference in the calling scope for empty containers.
|
||||
|
||||
This can be resolved either by providing an explicit type (e.g. `Vec::<Row>::new()`), or by using
|
||||
`Table::default()`.
|
||||
|
||||
```diff
|
||||
- let table = Table::new(vec![], widths);
|
||||
// becomes
|
||||
+ let table = Table::default().widths(widths);
|
||||
```
|
||||
|
||||
### `Tabs::new()` now accepts `IntoIterator<Item: Into<Line<'a>>>` ([#776])
|
||||
|
||||
[#776]: https://github.com/ratatui-org/ratatui/pull/776
|
||||
|
||||
Previously, `Tabs::new()` accepted `Vec<T>` where `T: Into<Line<'a>>`. This allows more flexible
|
||||
types from calling scopes, though it can break some type inference in the calling scope.
|
||||
|
||||
This typically occurs when collecting an iterator prior to calling `Tabs::new`, and can be resolved
|
||||
by removing the call to `.collect()`.
|
||||
|
||||
```diff
|
||||
- let tabs = Tabs::new((0.3).map(|i| format!("{i}")).collect());
|
||||
// becomes
|
||||
+ let tabs = Tabs::new((0.3).map(|i| format!("{i}")));
|
||||
```
|
||||
|
||||
### Table::default() now sets segment_size to None and column_spacing to ([#751])
|
||||
|
||||
[#751]: https://github.com/ratatui-org/ratatui/pull/751
|
||||
|
||||
The default() implementation of Table now sets the column_spacing field to 1 and the segment_size
|
||||
field to SegmentSize::None. This will affect the rendering of a small amount of apps.
|
||||
|
||||
To use the previous default values, call `table.segment_size(Default::default())` and
|
||||
`table.column_spacing(0)`.
|
||||
|
||||
### `patch_style` & `reset_style` now consumes and returns `Self` ([#754])
|
||||
|
||||
[#754]: https://github.com/ratatui-org/ratatui/pull/754
|
||||
|
||||
Previously, `patch_style` and `reset_style` in `Text`, `Line` and `Span` were using a mutable
|
||||
reference to `Self`. To be more consistent with the rest of `ratatui`, which is using fluent
|
||||
setters, these now take ownership of `Self` and return it.
|
||||
|
||||
The following example shows how to migrate for `Line`, but the same applies for `Text` and `Span`.
|
||||
|
||||
```diff
|
||||
- let mut line = Line::from("foobar");
|
||||
- line.patch_style(style);
|
||||
// becomes
|
||||
+ let line = Line::new("foobar").patch_style(style);
|
||||
```
|
||||
|
||||
### Remove deprecated `Block::title_on_bottom` ([#757])
|
||||
|
||||
[#757]: https://github.com/ratatui-org/ratatui/pull/757
|
||||
|
||||
`Block::title_on_bottom` was deprecated in v0.22. Use `Block::title` and `Title::position` instead.
|
||||
|
||||
```diff
|
||||
- block.title("foobar").title_on_bottom();
|
||||
+ block.title(Title::from("foobar").position(Position::Bottom));
|
||||
```
|
||||
|
||||
### `Block` style methods cannot be used in a const context ([#720])
|
||||
|
||||
[#720]: https://github.com/ratatui-org/ratatui/pull/720
|
||||
|
||||
Previously the `style()`, `border_style()` and `title_style()` methods could be used to create a
|
||||
`Block` in a constant context. These now accept `Into<Style>` instead of `Style`. These methods no
|
||||
longer can be called from a constant context.
|
||||
|
||||
### `Line` now has a `style` field that applies to the entire line ([#708])
|
||||
|
||||
[#708]: https://github.com/ratatui-org/ratatui/pull/708
|
||||
|
||||
Previously the style of a `Line` was stored in the `Span`s that make up the line. Now the `Line`
|
||||
itself has a `style` field, which can be set with the `Line::styled` method. Any code that creates
|
||||
`Line`s using the struct initializer instead of constructors will fail to compile due to the added
|
||||
field. This can be easily fixed by adding `..Default::default()` to the field list or by using a
|
||||
constructor method (`Line::styled()`, `Line::raw()`) or conversion method (`Line::from()`).
|
||||
|
||||
Each `Span` contained within the line will no longer have the style that is applied to the line in
|
||||
the `Span::style` field.
|
||||
|
||||
```diff
|
||||
let line = Line {
|
||||
spans: vec!["".into()],
|
||||
alignment: Alignment::Left,
|
||||
+ ..Default::default()
|
||||
};
|
||||
|
||||
// or
|
||||
|
||||
let line = Line::raw(vec!["".into()])
|
||||
.alignment(Alignment::Left);
|
||||
```
|
||||
|
||||
## [v0.25.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.25.0)
|
||||
|
||||
### Removed `Axis::title_style` and `Buffer::set_background` ([#691])
|
||||
|
||||
[#691]: https://github.com/ratatui-org/ratatui/pull/691
|
||||
|
||||
These items were deprecated since 0.10.
|
||||
|
||||
- You should use styling capabilities of [`text::Line`] given as argument of [`Axis::title`]
|
||||
instead of `Axis::title_style`
|
||||
instead of `Axis::title_style`
|
||||
- You should use styling capabilities of [`Buffer::set_style`] instead of `Buffer::set_background`
|
||||
|
||||
[`text::Line`]: https://docs.rs/ratatui/latest/ratatui/text/struct.Line.html
|
||||
@@ -58,7 +195,7 @@ instead of `Axis::title_style`
|
||||
|
||||
[#672]: https://github.com/ratatui-org/ratatui/pull/672
|
||||
|
||||
Previously `List::new()` took `Into<Vec<ListItem<'a>>>`. This change will throw a compilation
|
||||
Previously `List::new()` took `Into<Vec<ListItem<'a>>>`. This change will throw a compilation
|
||||
error for `IntoIterator`s with an indeterminate item (e.g. empty vecs).
|
||||
|
||||
E.g.
|
||||
@@ -71,18 +208,17 @@ E.g.
|
||||
|
||||
### The default `Tabs::highlight_style` is now `Style::new().reversed()` ([#635])
|
||||
|
||||
[#635]: https://github.com/ratatui-org/ratatui/pull/635
|
||||
|
||||
Previously the default highlight style for tabs was `Style::default()`, which meant that a `Tabs`
|
||||
widget in the default configuration would not show any indication of the selected tab.
|
||||
|
||||
[#635]: https://github.com/ratatui-org/ratatui/pull/635
|
||||
|
||||
### The default `Tabs::highlight_style` is now `Style::new().reversed()` ([#635])
|
||||
|
||||
Previously the default highlight style for tabs was `Style::default()`, which meant that a `Tabs`
|
||||
widget in the default configuration would not show any indication of the selected tab.
|
||||
|
||||
|
||||
### `Table::new()` now requires specifying the widths of the columns (#664)
|
||||
### `Table::new()` now requires specifying the widths of the columns ([#664])
|
||||
|
||||
[#664]: https://github.com/ratatui-org/ratatui/pull/664
|
||||
|
||||
@@ -121,7 +257,7 @@ E.g.
|
||||
```diff
|
||||
- let table = Table::new(rows).widths(&[Constraint::Length(1)]);
|
||||
// becomes
|
||||
+ let table = Table::new(rows).widths([Constraint::Length(1)]);
|
||||
+ let table = Table::new(rows, [Constraint::Length(1)]);
|
||||
```
|
||||
|
||||
### Layout::new() now accepts direction and constraint parameters ([#557])
|
||||
@@ -238,7 +374,7 @@ new module locations. E.g.:
|
||||
```diff
|
||||
- use ratatui::{widgets::scrollbar::{Scrollbar, Set}};
|
||||
// becomes
|
||||
+ use ratatui::{widgets::Scrollbar, symbols::scrollbar::Set}
|
||||
+ use ratatui::{widgets::Scrollbar, symbols::scrollbar::Set}
|
||||
```
|
||||
|
||||
### MSRV updated to 1.67 ([#361])
|
||||
@@ -259,7 +395,7 @@ changelog](https://github.com/bitflags/bitflags/blob/main/CHANGELOG.md#200-rc2).
|
||||
|
||||
## [v0.21.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.21.0)
|
||||
|
||||
### MSRV is 1.65.0 ([#171])
|
||||
### MSRV is 1.65.0 ([#171])
|
||||
|
||||
[#171]: https://github.com/ratatui-org/ratatui/issues/171
|
||||
|
||||
@@ -270,7 +406,7 @@ The minimum supported rust version is now 1.65.0.
|
||||
[#114]: https://github.com/ratatui-org/ratatui/issues/114
|
||||
|
||||
In order to support inline viewports, the unstable method `Terminal::with_options()` was stabilized
|
||||
and `ViewPort` was changed from a struct to an enum.
|
||||
and `ViewPort` was changed from a struct to an enum.
|
||||
|
||||
```diff
|
||||
let terminal = Terminal::with_options(backend, TerminalOptions {
|
||||
@@ -287,7 +423,7 @@ let terminal = Terminal::with_options(backend, TerminalOptions {
|
||||
[#168]: https://github.com/ratatui-org/ratatui/issues/168
|
||||
|
||||
A new type `Masked` was introduced that implements `From<Text<'a>>`. This causes any code that did
|
||||
previously did not need to use type annotations to fail to compile. To fix this, annotate or call
|
||||
previously did not need to use type annotations to fail to compile. To fix this, annotate or call
|
||||
to_string() / to_owned() / as_str() on the value. E.g.:
|
||||
|
||||
```diff
|
||||
|
||||
2303
CHANGELOG.md
2303
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -54,14 +54,6 @@ describes the nature of the problem that the commit is solving and any unintuiti
|
||||
change. It's rare that code changes can easily communicate intent, so make sure this is clearly
|
||||
documented.
|
||||
|
||||
### Clean up your commits
|
||||
|
||||
The final version of your PR that will be committed to the repository should be rebased and tested
|
||||
against main. Every commit will end up as a line in the changelog, so please squash commits that are
|
||||
only formatting or incremental fixes to things brought up as part of the PR review. Aim for a single
|
||||
commit (unless there is a strong reason to stack the commits). See [Git Best Practices - On Sausage
|
||||
Making](https://sethrobertson.github.io/GitBestPractices/#sausage) for more on this.
|
||||
|
||||
### Run CI tests before pushing a PR
|
||||
|
||||
We're using [cargo-husky](https://github.com/rhysd/cargo-husky) to automatically run git hooks,
|
||||
@@ -120,7 +112,8 @@ exist to show coverage directly in your editor. E.g.:
|
||||
|
||||
### Documentation
|
||||
|
||||
Here are some guidelines for writing documentation in Ratatui.
|
||||
Here are some guidelines for writing documentation in Ratatui.
|
||||
|
||||
Every public API **must** be documented.
|
||||
|
||||
Keep in mind that Ratatui tends to attract beginner Rust users that may not be familiar with Rust
|
||||
@@ -133,10 +126,9 @@ the concepts pointing to the various methods. Focus on interaction with various
|
||||
enough information that helps understand why you might want something.
|
||||
|
||||
Examples should help users understand a particular usage, not test a feature. They should be as
|
||||
simple as possible.
|
||||
Prefer hiding imports and using wildcards to keep things concise. Some imports may still be shown
|
||||
to demonstrate a particular non-obvious import (e.g. `Stylize` trait to use style methods).
|
||||
Speaking of `Stylize`, you should use it over the more verbose style setters:
|
||||
simple as possible. Prefer hiding imports and using wildcards to keep things concise. Some imports
|
||||
may still be shown to demonstrate a particular non-obvious import (e.g. `Stylize` trait to use style
|
||||
methods). Speaking of `Stylize`, you should use it over the more verbose style setters:
|
||||
|
||||
```rust
|
||||
let style = Style::new().red().bold();
|
||||
@@ -146,7 +138,7 @@ let style = Style::default().fg(Color::Red).add_modifier(Modifiers::BOLD);
|
||||
|
||||
#### Format
|
||||
|
||||
- First line is summary, second is blank, third onward is more detail
|
||||
- First line is summary, second is blank, third onward is more detail
|
||||
|
||||
```rust
|
||||
/// Summary
|
||||
@@ -156,10 +148,10 @@ let style = Style::default().fg(Color::Red).add_modifier(Modifiers::BOLD);
|
||||
fn foo() {}
|
||||
```
|
||||
|
||||
- Max line length is 100 characters
|
||||
- Max line length is 100 characters
|
||||
See [vscode rewrap extension](https://marketplace.visualstudio.com/items?itemName=stkb.rewrap)
|
||||
|
||||
- Doc comments are above macros
|
||||
- Doc comments are above macros
|
||||
i.e.
|
||||
|
||||
```rust
|
||||
@@ -168,9 +160,15 @@ i.e.
|
||||
struct Foo {}
|
||||
```
|
||||
|
||||
- Code items should be between backticks
|
||||
- Code items should be between backticks
|
||||
i.e. ``[`Block`]``, **NOT** ``[Block]``
|
||||
|
||||
### Deprecation notice
|
||||
|
||||
We generally want to wait at least two versions before removing deprecated items so users have
|
||||
time to update. However, if a deprecation is blocking for us to implement a new feature we may
|
||||
*consider* removing it in a one version notice.
|
||||
|
||||
### Use of unsafe for optimization purposes
|
||||
|
||||
We don't currently use any unsafe code in Ratatui, and would like to keep it that way. However there
|
||||
|
||||
80
Cargo.toml
80
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ratatui"
|
||||
version = "0.24.0" # crate version
|
||||
version = "0.26.1" # crate version
|
||||
authors = ["Florian Dehau <work@fdehau.com>", "The Ratatui Developers"]
|
||||
description = "A library that's all about cooking up terminal user interfaces"
|
||||
documentation = "https://docs.rs/ratatui/latest/ratatui/"
|
||||
@@ -18,14 +18,14 @@ exclude = [
|
||||
]
|
||||
autoexamples = true
|
||||
edition = "2021"
|
||||
rust-version = "1.70.0"
|
||||
rust-version = "1.74.0"
|
||||
|
||||
[badges]
|
||||
|
||||
[dependencies]
|
||||
crossterm = { version = "0.27", optional = true }
|
||||
termion = { version = "2.0", optional = true }
|
||||
termwiz = { version = "0.20.0", optional = true }
|
||||
termion = { version = "3.0", optional = true }
|
||||
termwiz = { version = "0.22.0", optional = true }
|
||||
|
||||
serde = { version = "1", optional = true, features = ["derive"] }
|
||||
bitflags = "2.3"
|
||||
@@ -33,13 +33,14 @@ cassowary = "0.3"
|
||||
indoc = "2.0"
|
||||
itertools = "0.12"
|
||||
paste = "1.0.2"
|
||||
strum = { version = "0.25", features = ["derive"] }
|
||||
strum = { version = "0.26", features = ["derive"] }
|
||||
time = { version = "0.3.11", optional = true, features = ["local-offset"] }
|
||||
unicode-segmentation = "1.10"
|
||||
unicode-width = "0.1"
|
||||
document-features = { version = "0.2.7", optional = true }
|
||||
lru = "0.12.0"
|
||||
stability = "0.1.1"
|
||||
compact_str = "0.7.1"
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = "1.0.71"
|
||||
@@ -50,10 +51,48 @@ cargo-husky = { version = "1.5.0", default-features = false, features = [
|
||||
] }
|
||||
color-eyre = "0.6.2"
|
||||
criterion = { version = "0.5.1", features = ["html_reports"] }
|
||||
derive_builder = "0.20.0"
|
||||
fakeit = "1.1"
|
||||
font8x8 = "0.3.1"
|
||||
palette = "0.7.3"
|
||||
pretty_assertions = "1.4.0"
|
||||
rand = "0.8.5"
|
||||
rand_chacha = "0.3.1"
|
||||
rstest = "0.18.2"
|
||||
serde_json = "1.0.109"
|
||||
|
||||
[lints.rust]
|
||||
unsafe_code = "forbid"
|
||||
[lints.clippy]
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
cast_possible_truncation = "allow"
|
||||
cast_possible_wrap = "allow"
|
||||
cast_precision_loss = "allow"
|
||||
cast_sign_loss = "allow"
|
||||
missing_errors_doc = "allow"
|
||||
missing_panics_doc = "allow"
|
||||
module_name_repetitions = "allow"
|
||||
must_use_candidate = "allow"
|
||||
wildcard_imports = "allow"
|
||||
|
||||
# nursery or restricted
|
||||
as_underscore = "warn"
|
||||
deref_by_slicing = "warn"
|
||||
else_if_without_else = "warn"
|
||||
empty_line_after_doc_comments = "warn"
|
||||
equatable_if_let = "warn"
|
||||
fn_to_numeric_cast_any = "warn"
|
||||
format_push_string = "warn"
|
||||
map_err_ignore = "warn"
|
||||
missing_const_for_fn = "warn"
|
||||
mixed_read_write_in_expression = "warn"
|
||||
mod_module_files = "warn"
|
||||
needless_raw_strings = "warn"
|
||||
redundant_type_annotations = "warn"
|
||||
rest_pat_in_fully_bound_structs = "warn"
|
||||
string_lit_chars_any = "warn"
|
||||
string_to_string = "warn"
|
||||
use_self = "warn"
|
||||
|
||||
[features]
|
||||
#! The crate provides a set of optional features that can be enabled in your `cargo.toml` file.
|
||||
@@ -73,7 +112,7 @@ termwiz = ["dep:termwiz"]
|
||||
#! The following optional features are available for all backends:
|
||||
## enables serialization and deserialization of style and color types using the [Serde crate].
|
||||
## This is useful if you want to save themes to a file.
|
||||
serde = ["dep:serde", "bitflags/serde"]
|
||||
serde = ["dep:serde", "bitflags/serde", "compact_str/serde"]
|
||||
|
||||
## enables the [`border!`] macro.
|
||||
macros = []
|
||||
@@ -94,11 +133,7 @@ underline-color = ["dep:crossterm"]
|
||||
#! The following features are unstable and may change in the future:
|
||||
|
||||
## Enable all unstable features.
|
||||
unstable = ["unstable-segment-size", "unstable-rendered-line-info"]
|
||||
|
||||
## Enables the [`Layout::segment_size`](crate::layout::Layout::segment_size) method which is experimental and may change in the
|
||||
## future. See [Issue #536](https://github.com/ratatui-org/ratatui/issues/536) for more details.
|
||||
unstable-segment-size = []
|
||||
unstable = ["unstable-rendered-line-info", "unstable-widget-ref"]
|
||||
|
||||
## Enables the [`Paragraph::line_count`](crate::widgets::Paragraph::line_count)
|
||||
## [`Paragraph::line_width`](crate::widgets::Paragraph::line_width) methods
|
||||
@@ -106,6 +141,10 @@ unstable-segment-size = []
|
||||
## See [Issue 293](https://github.com/ratatui-org/ratatui/issues/293) for more details.
|
||||
unstable-rendered-line-info = []
|
||||
|
||||
## Enables the `WidgetRef` and `StatefulWidgetRef` traits which are experimental and may change in
|
||||
## the future.
|
||||
unstable-widget-ref = []
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
# see https://doc.rust-lang.org/nightly/rustdoc/scraped-examples.html
|
||||
@@ -207,6 +246,21 @@ name = "layout"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "constraints"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = false
|
||||
|
||||
[[example]]
|
||||
name = "flex"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "constraint-explorer"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "list"
|
||||
required-features = ["crossterm"]
|
||||
@@ -267,3 +321,7 @@ doc-scrape-examples = true
|
||||
name = "inline"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[test]]
|
||||
name = "state_serde"
|
||||
required-features = ["serde"]
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,7 +1,7 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016-2022 Florian Dehau
|
||||
Copyright (c) 2023 The Ratatui Developers
|
||||
Copyright (c) 2023-2024 The Ratatui Developers
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -90,12 +90,21 @@ args = [
|
||||
"warnings",
|
||||
]
|
||||
|
||||
[tasks.install-nextest]
|
||||
description = "Install cargo-nextest"
|
||||
install_crate = { crate_name = "cargo-nextest", binary = "cargo-nextest", test_arg = "--help" }
|
||||
|
||||
[tasks.test]
|
||||
description = "Run tests"
|
||||
dependencies = ["test-doc"]
|
||||
run_task = { name = ["test-lib", "test-doc"] }
|
||||
|
||||
[tasks.test-lib]
|
||||
description = "Run default tests"
|
||||
dependencies = ["install-nextest"]
|
||||
command = "cargo"
|
||||
args = [
|
||||
"test",
|
||||
"nextest",
|
||||
"run",
|
||||
"--all-targets",
|
||||
"--no-default-features",
|
||||
"${ALL_FEATURES_FLAG}",
|
||||
@@ -109,9 +118,11 @@ args = ["test", "--doc", "--no-default-features", "${ALL_FEATURES_FLAG}"]
|
||||
[tasks.test-backend]
|
||||
# takes a command line parameter to specify the backend to test (e.g. "crossterm")
|
||||
description = "Run backend-specific tests"
|
||||
dependencies = ["install-nextest"]
|
||||
command = "cargo"
|
||||
args = [
|
||||
"test",
|
||||
"nextest",
|
||||
"run",
|
||||
"--all-targets",
|
||||
"--no-default-features",
|
||||
"--features",
|
||||
|
||||
98
README.md
98
README.md
@@ -21,33 +21,25 @@
|
||||
|
||||
<!-- cargo-rdme start -->
|
||||
|
||||

|
||||

|
||||
|
||||
<div align="center">
|
||||
|
||||
[![Crate Badge]](https://crates.io/crates/ratatui)
|
||||
[![License Badge]](./LICENSE)
|
||||
[![CI Badge]](https://github.com/ratatui-org/ratatui/actions?query=workflow%3ACI+)
|
||||
[![Docs Badge]](https://docs.rs/crate/ratatui/)<br>
|
||||
[![Dependencies Badge]](https://deps.rs/repo/github/ratatui-org/ratatui)
|
||||
[![Codecov Badge]](https://app.codecov.io/gh/ratatui-org/ratatui)
|
||||
[![Discord Badge]](https://discord.gg/pMCEU9hNEj)
|
||||
[![Matrix Badge]](https://matrix.to/#/#ratatui:matrix.org)<br>
|
||||
[![Crate Badge]][Crate] [![Docs Badge]][API Docs] [![CI Badge]][CI Workflow] [![License
|
||||
Badge]](./LICENSE) [![Sponsors Badge]][GitHub Sponsors]<br>
|
||||
[![Codecov Badge]][Codecov] [![Deps.rs Badge]][Deps.rs] [![Discord Badge]][Discord Server]
|
||||
[![Matrix Badge]][Matrix]<br>
|
||||
|
||||
[Documentation](https://docs.rs/ratatui)
|
||||
· [Ratatui Website](https://ratatui.rs)
|
||||
· [Examples](https://github.com/ratatui-org/ratatui/tree/main/examples)
|
||||
· [Report a bug](https://github.com/ratatui-org/ratatui/issues/new?labels=bug&projects=&template=bug_report.md)
|
||||
· [Request a Feature](https://github.com/ratatui-org/ratatui/issues/new?labels=enhancement&projects=&template=feature_request.md)
|
||||
· [Send a Pull Request](https://github.com/ratatui-org/ratatui/compare)
|
||||
[Ratatui Website] · [API Docs] · [Examples] · [Changelog] · [Breaking Changes]<br>
|
||||
[Contributing] · [Report a bug] · [Request a Feature] · [Create a Pull Request]
|
||||
|
||||
</div>
|
||||
|
||||
# Ratatui
|
||||
|
||||
[Ratatui] is a crate for cooking up terminal user interfaces in Rust. It is a lightweight
|
||||
library that provides a set of widgets and utilities to build complex Rust TUIs. Ratatui was
|
||||
forked from the [tui-rs] crate in 2023 in order to continue its development.
|
||||
[Ratatui][Ratatui Website] is a crate for cooking up terminal user interfaces in Rust. It is a
|
||||
lightweight library that provides a set of widgets and utilities to build complex Rust TUIs.
|
||||
Ratatui was forked from the [tui-rs] crate in 2023 in order to continue its development.
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -66,28 +58,29 @@ section of the [Ratatui Website] for more details on how to use other backends (
|
||||
Ratatui is based on the principle of immediate rendering with intermediate buffers. This means
|
||||
that for each frame, your app must render all widgets that are supposed to be part of the UI.
|
||||
This is in contrast to the retained mode style of rendering where widgets are updated and then
|
||||
automatically redrawn on the next frame. See the [Rendering] section of the [Ratatui Website] for
|
||||
more info.
|
||||
automatically redrawn on the next frame. See the [Rendering] section of the [Ratatui Website]
|
||||
for more info.
|
||||
|
||||
You can also watch the [FOSDEM 2024 talk] about Ratatui which gives a brief introduction to
|
||||
terminal user interfaces and showcases the features of Ratatui, along with a hello world demo.
|
||||
|
||||
## Other documentation
|
||||
|
||||
- [Ratatui Website] - explains the library's concepts and provides step-by-step tutorials
|
||||
- [API Docs] - the full API documentation for the library on docs.rs.
|
||||
- [Examples] - a collection of examples that demonstrate how to use the library.
|
||||
- [API Documentation] - the full API documentation for the library on docs.rs.
|
||||
- [Changelog] - generated by [git-cliff] utilizing [Conventional Commits].
|
||||
- [Contributing] - Please read this if you are interested in contributing to the project.
|
||||
- [Changelog] - generated by [git-cliff] utilizing [Conventional Commits].
|
||||
- [Breaking Changes] - a list of breaking changes in the library.
|
||||
|
||||
## Quickstart
|
||||
|
||||
The following example demonstrates the minimal amount of code necessary to setup a terminal and
|
||||
render "Hello World!". The full code for this example which contains a little more detail is in
|
||||
[hello_world.rs]. For more guidance on different ways to structure your application see the
|
||||
[Application Patterns] and [Hello World tutorial] sections in the [Ratatui Website] and the various
|
||||
[Examples]. There are also several starter templates available:
|
||||
|
||||
- [template]
|
||||
- [async-template] (book and template)
|
||||
the [Examples] directory. For more guidance on different ways to structure your application see
|
||||
the [Application Patterns] and [Hello World tutorial] sections in the [Ratatui Website] and the
|
||||
various [Examples]. There are also several starter templates available in the [templates]
|
||||
repository.
|
||||
|
||||
Every application built with `ratatui` needs to implement the following steps:
|
||||
|
||||
@@ -117,8 +110,8 @@ module] and the [Backends] section of the [Ratatui Website] for more info.
|
||||
|
||||
The drawing logic is delegated to a closure that takes a [`Frame`] instance as argument. The
|
||||
[`Frame`] provides the size of the area to draw to and allows the app to render any [`Widget`]
|
||||
using the provided [`render_widget`] method. See the [Widgets] section of the [Ratatui Website] for
|
||||
more info.
|
||||
using the provided [`render_widget`] method. See the [Widgets] section of the [Ratatui Website]
|
||||
for more info.
|
||||
|
||||
### Handling events
|
||||
|
||||
@@ -131,10 +124,11 @@ Website] for more info. For example, if you are using [Crossterm], you can use t
|
||||
|
||||
```rust
|
||||
use std::io::{self, stdout};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
@@ -160,7 +154,7 @@ fn handle_events() -> io::Result<bool> {
|
||||
if key.kind == event::KeyEventKind::Press && key.code == KeyCode::Char('q') {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
@@ -195,7 +189,7 @@ fn ui(frame: &mut Frame) {
|
||||
Constraint::Length(1),
|
||||
Constraint::Min(0),
|
||||
Constraint::Length(1),
|
||||
]
|
||||
],
|
||||
)
|
||||
.split(frame.size());
|
||||
frame.render_widget(
|
||||
@@ -209,7 +203,7 @@ fn ui(frame: &mut Frame) {
|
||||
|
||||
let inner_layout = Layout::new(
|
||||
Direction::Horizontal,
|
||||
[Constraint::Percentage(50), Constraint::Percentage(50)]
|
||||
[Constraint::Percentage(50), Constraint::Percentage(50)],
|
||||
)
|
||||
.split(main_layout[1]);
|
||||
frame.render_widget(
|
||||
@@ -251,7 +245,7 @@ fn ui(frame: &mut Frame) {
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
Constraint::Min(0),
|
||||
]
|
||||
],
|
||||
)
|
||||
.split(frame.size());
|
||||
|
||||
@@ -299,15 +293,18 @@ Running this example produces the following output:
|
||||
[Handling Events]: https://ratatui.rs/concepts/event-handling/
|
||||
[Layout]: https://ratatui.rs/how-to/layout/
|
||||
[Styling Text]: https://ratatui.rs/how-to/render/style-text/
|
||||
[template]: https://github.com/ratatui-org/template
|
||||
[async-template]: https://ratatui-org.github.io/async-template
|
||||
[Examples]: https://github.com/ratatui-org/ratatui/tree/main/examples
|
||||
[templates]: https://github.com/ratatui-org/templates/
|
||||
[Examples]: https://github.com/ratatui-org/ratatui/tree/main/examples/README.md
|
||||
[Report a bug]: https://github.com/ratatui-org/ratatui/issues/new?labels=bug&projects=&template=bug_report.md
|
||||
[Request a Feature]: https://github.com/ratatui-org/ratatui/issues/new?labels=enhancement&projects=&template=feature_request.md
|
||||
[Create a Pull Request]: https://github.com/ratatui-org/ratatui/compare
|
||||
[git-cliff]: https://git-cliff.org
|
||||
[Conventional Commits]: https://www.conventionalcommits.org
|
||||
[API Documentation]: https://docs.rs/ratatui
|
||||
[API Docs]: https://docs.rs/ratatui
|
||||
[Changelog]: https://github.com/ratatui-org/ratatui/blob/main/CHANGELOG.md
|
||||
[Contributing]: https://github.com/ratatui-org/ratatui/blob/main/CONTRIBUTING.md
|
||||
[Breaking Changes]: https://github.com/ratatui-org/ratatui/blob/main/BREAKING-CHANGES.md
|
||||
[FOSDEM 2024 talk]: https://www.youtube.com/watch?v=NU0q6NOLJ20
|
||||
[docsrs-hello]: https://github.com/ratatui-org/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-hello.png?raw=true
|
||||
[docsrs-layout]: https://github.com/ratatui-org/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-layout.png?raw=true
|
||||
[docsrs-styling]: https://github.com/ratatui-org/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-styling.png?raw=true
|
||||
@@ -324,24 +321,30 @@ Running this example produces the following output:
|
||||
[`Backend`]: backend::Backend
|
||||
[`backend` module]: backend
|
||||
[`crossterm::event`]: https://docs.rs/crossterm/latest/crossterm/event/index.html
|
||||
[Ratatui]: https://ratatui.rs
|
||||
[Crate]: https://crates.io/crates/ratatui
|
||||
[Crossterm]: https://crates.io/crates/crossterm
|
||||
[Termion]: https://crates.io/crates/termion
|
||||
[Termwiz]: https://crates.io/crates/termwiz
|
||||
[tui-rs]: https://crates.io/crates/tui
|
||||
[hello_world.rs]: https://github.com/ratatui-org/ratatui/blob/main/examples/hello_world.rs
|
||||
[GitHub Sponsors]: https://github.com/sponsors/ratatui-org
|
||||
[Crate Badge]: https://img.shields.io/crates/v/ratatui?logo=rust&style=flat-square
|
||||
[License Badge]: https://img.shields.io/crates/l/ratatui?style=flat-square
|
||||
[CI Badge]:
|
||||
https://img.shields.io/github/actions/workflow/status/ratatui-org/ratatui/ci.yml?style=flat-square&logo=github
|
||||
[CI Workflow]: https://github.com/ratatui-org/ratatui/actions/workflows/ci.yml
|
||||
[Codecov Badge]:
|
||||
https://img.shields.io/codecov/c/github/ratatui-org/ratatui?logo=codecov&style=flat-square&token=BAQ8SOKEST
|
||||
[Dependencies Badge]: https://deps.rs/repo/github/ratatui-org/ratatui/status.svg?style=flat-square
|
||||
[Codecov]: https://app.codecov.io/gh/ratatui-org/ratatui
|
||||
[Deps.rs Badge]: https://deps.rs/repo/github/ratatui-org/ratatui/status.svg?style=flat-square
|
||||
[Deps.rs]: https://deps.rs/repo/github/ratatui-org/ratatui
|
||||
[Discord Badge]:
|
||||
https://img.shields.io/discord/1070692720437383208?label=discord&logo=discord&style=flat-square
|
||||
[Discord Server]: https://discord.gg/pMCEU9hNEj
|
||||
[Docs Badge]: https://img.shields.io/docsrs/ratatui?logo=rust&style=flat-square
|
||||
[License Badge]: https://img.shields.io/crates/l/ratatui?style=flat-square
|
||||
[Matrix Badge]:
|
||||
https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix
|
||||
[Matrix]: https://matrix.to/#/#ratatui:matrix.org
|
||||
[Sponsors Badge]: https://img.shields.io/github/sponsors/ratatui-org?logo=github&style=flat-square
|
||||
|
||||
<!-- cargo-rdme end -->
|
||||
|
||||
@@ -390,9 +393,8 @@ The library comes with the following
|
||||
- [Table](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Table.html)
|
||||
- [Tabs](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Tabs.html)
|
||||
|
||||
Each widget has an associated example which can be found in the [examples](./examples/) folder. Run
|
||||
each examples with cargo (e.g. to run the gauge example `cargo run --example gauge`), and quit by
|
||||
pressing `q`.
|
||||
Each widget has an associated example which can be found in the [Examples] folder. Run each example
|
||||
with cargo (e.g. to run the gauge example `cargo run --example gauge`), and quit by pressing `q`.
|
||||
|
||||
You can also run all examples by running `cargo make run-examples` (requires `cargo-make` that can
|
||||
be installed with `cargo install cargo-make`).
|
||||
@@ -403,8 +405,8 @@ be installed with `cargo install cargo-make`).
|
||||
`ratatui::text::Text`
|
||||
- [color-to-tui](https://github.com/uttarayan21/color-to-tui) — Parse hex colors to
|
||||
`ratatui::style::Color`
|
||||
- [rust-tui-template](https://github.com/ratatui-org/rust-tui-template) — A template for
|
||||
bootstrapping a Rust TUI application with Tui-rs & crossterm
|
||||
- [templates](https://github.com/ratatui-org/templates) — Starter templates for
|
||||
bootstrapping a Rust TUI application with Ratatui & crossterm
|
||||
- [tui-builder](https://github.com/jkelleyrtp/tui-builder) — Batteries-included MVC framework for
|
||||
Tui-rs + Crossterm apps
|
||||
- [tui-clap](https://github.com/kegesch/tui-clap-rs) — Use clap-rs together with Tui-rs
|
||||
|
||||
16
bacon.toml
16
bacon.toml
@@ -44,6 +44,16 @@ command = [
|
||||
]
|
||||
need_stdout = true
|
||||
|
||||
[jobs.test-unit]
|
||||
command = [
|
||||
"cargo", "test",
|
||||
"--lib",
|
||||
"--all-features",
|
||||
"--color", "always",
|
||||
"--", "--color", "always", # see https://github.com/Canop/bacon/issues/124
|
||||
]
|
||||
need_stdout = true
|
||||
|
||||
[jobs.doc]
|
||||
command = [
|
||||
"cargo", "+nightly", "doc",
|
||||
@@ -74,7 +84,7 @@ on_success = "job:doc" # so that we don't open the browser at each change
|
||||
command = [
|
||||
"cargo", "llvm-cov",
|
||||
"--lcov", "--output-path", "target/lcov.info",
|
||||
"--all-features",
|
||||
"--all-features",
|
||||
"--color", "always",
|
||||
]
|
||||
|
||||
@@ -97,4 +107,6 @@ ctrl-c = "job:check-crossterm"
|
||||
ctrl-t = "job:check-termion"
|
||||
ctrl-w = "job:check-termwiz"
|
||||
v = "job:coverage"
|
||||
u = "job:coverage-unit-tests-only"
|
||||
ctrl-v = "job:coverage-unit-tests-only"
|
||||
u = "job:test-unit"
|
||||
n = "job:nextest"
|
||||
|
||||
@@ -8,7 +8,7 @@ use ratatui::{
|
||||
};
|
||||
|
||||
/// Benchmark for rendering a barchart.
|
||||
pub fn barchart(c: &mut Criterion) {
|
||||
fn barchart(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("barchart");
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
@@ -66,7 +66,7 @@ fn render(bencher: &mut Bencher, barchart: &BarChart) {
|
||||
bench_barchart.render(buffer.area, &mut buffer);
|
||||
},
|
||||
criterion::BatchSize::LargeInput,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
criterion_group!(benches, barchart);
|
||||
|
||||
@@ -10,10 +10,10 @@ use ratatui::{
|
||||
};
|
||||
|
||||
/// Benchmark for rendering a block.
|
||||
pub fn block(c: &mut Criterion) {
|
||||
fn block(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("block");
|
||||
|
||||
for buffer_size in &[
|
||||
for buffer_size in [
|
||||
Rect::new(0, 0, 100, 50), // vertically split screen
|
||||
Rect::new(0, 0, 200, 50), // 1080p fullscreen with medium font
|
||||
Rect::new(0, 0, 256, 256), // Max sized area
|
||||
@@ -47,8 +47,8 @@ pub fn block(c: &mut Criterion) {
|
||||
}
|
||||
|
||||
/// 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);
|
||||
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.
|
||||
// See https://github.com/ratatui-org/ratatui/pull/377.
|
||||
bencher.iter_batched(
|
||||
@@ -57,7 +57,7 @@ fn render(bencher: &mut Bencher, block: &Block, size: &Rect) {
|
||||
bench_block.render(buffer.area, &mut buffer);
|
||||
},
|
||||
BatchSize::SmallInput,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
criterion_group!(benches, block);
|
||||
|
||||
@@ -7,7 +7,7 @@ use ratatui::{
|
||||
|
||||
/// Benchmark for rendering a list.
|
||||
/// It only benchmarks the render with a different amount of items.
|
||||
pub fn list(c: &mut Criterion) {
|
||||
fn list(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("list");
|
||||
|
||||
for line_count in [64, 2048, 16384] {
|
||||
@@ -33,7 +33,7 @@ pub fn list(c: &mut Criterion) {
|
||||
ListState::default()
|
||||
.with_offset(line_count / 2)
|
||||
.with_selected(Some(line_count / 2)),
|
||||
)
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -52,7 +52,7 @@ fn render(bencher: &mut Bencher, list: &List) {
|
||||
Widget::render(bench_list, buffer.area, &mut buffer);
|
||||
},
|
||||
BatchSize::LargeInput,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/// render the list into a common size buffer with a state
|
||||
@@ -66,7 +66,7 @@ fn render_stateful(bencher: &mut Bencher, list: &List, mut state: ListState) {
|
||||
StatefulWidget::render(bench_list, buffer.area, &mut buffer, &mut state);
|
||||
},
|
||||
BatchSize::LargeInput,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
criterion_group!(benches, list);
|
||||
|
||||
@@ -17,15 +17,15 @@ const WRAP_WIDTH: u16 = 100;
|
||||
/// Benchmark for rendering a paragraph with a given number of lines. The design of this benchmark
|
||||
/// allows comparison of the performance of rendering a paragraph with different numbers of lines.
|
||||
/// as well as comparing with the various settings on the scroll and wrap features.
|
||||
pub fn paragraph(c: &mut Criterion) {
|
||||
fn paragraph(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("paragraph");
|
||||
for &line_count in [64, 2048, MAX_SCROLL_OFFSET].iter() {
|
||||
for line_count in [64, 2048, MAX_SCROLL_OFFSET] {
|
||||
let lines = random_lines(line_count);
|
||||
let lines = lines.as_str();
|
||||
|
||||
// benchmark that measures the overhead of creating a paragraph separately from rendering
|
||||
group.bench_with_input(BenchmarkId::new("new", line_count), lines, |b, lines| {
|
||||
b.iter(|| Paragraph::new(black_box(lines)))
|
||||
b.iter(|| Paragraph::new(black_box(lines)));
|
||||
});
|
||||
|
||||
// render the paragraph with no scroll
|
||||
@@ -38,14 +38,14 @@ pub fn paragraph(c: &mut Criterion) {
|
||||
// scroll the paragraph by half the number of lines and render
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("render_scroll_half", line_count),
|
||||
&Paragraph::new(lines).scroll((0u16, line_count / 2)),
|
||||
&Paragraph::new(lines).scroll((0, line_count / 2)),
|
||||
|bencher, paragraph| render(bencher, paragraph, NO_WRAP_WIDTH),
|
||||
);
|
||||
|
||||
// scroll the paragraph by the full number of lines and render
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("render_scroll_full", line_count),
|
||||
&Paragraph::new(lines).scroll((0u16, line_count)),
|
||||
&Paragraph::new(lines).scroll((0, line_count)),
|
||||
|bencher, paragraph| render(bencher, paragraph, NO_WRAP_WIDTH),
|
||||
);
|
||||
|
||||
@@ -61,7 +61,7 @@ pub fn paragraph(c: &mut Criterion) {
|
||||
BenchmarkId::new("render_wrap_scroll_full", line_count),
|
||||
&Paragraph::new(lines)
|
||||
.wrap(Wrap { trim: false })
|
||||
.scroll((0u16, line_count)),
|
||||
.scroll((0, line_count)),
|
||||
|bencher, paragraph| render(bencher, paragraph, WRAP_WIDTH),
|
||||
);
|
||||
}
|
||||
@@ -79,7 +79,7 @@ fn render(bencher: &mut Bencher, paragraph: &Paragraph, width: u16) {
|
||||
bench_paragraph.render(buffer.area, &mut buffer);
|
||||
},
|
||||
BatchSize::LargeInput,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/// Create a string with the given number of lines filled with nonsense words
|
||||
@@ -87,7 +87,7 @@ fn render(bencher: &mut Bencher, paragraph: &Paragraph, width: u16) {
|
||||
/// English language has about 5.1 average characters per word so including the space between words
|
||||
/// this should emit around 200 characters per paragraph on average.
|
||||
fn random_lines(count: u16) -> String {
|
||||
let count = count as i64;
|
||||
let count = i64::from(count);
|
||||
let sentence_count = 3;
|
||||
let word_count = 11;
|
||||
fakeit::words::paragraph(count, sentence_count, word_count, "\n".into())
|
||||
|
||||
@@ -7,7 +7,7 @@ use ratatui::{
|
||||
};
|
||||
|
||||
/// Benchmark for rendering a sparkline.
|
||||
pub fn sparkline(c: &mut Criterion) {
|
||||
fn sparkline(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("sparkline");
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
@@ -38,7 +38,7 @@ fn render(bencher: &mut Bencher, sparkline: &Sparkline) {
|
||||
bench_sparkline.render(buffer.area, &mut buffer);
|
||||
},
|
||||
criterion::BatchSize::LargeInput,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
criterion_group!(benches, sparkline);
|
||||
|
||||
@@ -30,6 +30,12 @@ body = """
|
||||
{{commit.body | indent(prefix=" ") }}
|
||||
````
|
||||
{%- endif %}
|
||||
{%- for footer in commit.footers %}
|
||||
{%- if footer.token != "Signed-off-by" and footer.token != "Co-authored-by" %}
|
||||
|
||||
{{ footer.token | indent(prefix=" ") }}{{ footer.separator }}{{ footer.value }}
|
||||
{%- endif %}
|
||||
{%- endfor %}
|
||||
{% endmacro -%}
|
||||
|
||||
{% for group, commits in commits | group_by(attribute="group") %}
|
||||
|
||||
@@ -1,8 +1,27 @@
|
||||
# Examples
|
||||
|
||||
These gifs were created using [VHS](https://github.com/charmbracelet/vhs). Each example has a
|
||||
corresponding `.tape` file that holds instructions for how to generate the images. Note that the
|
||||
images themselves are stored in a separate git branch to avoid bloating the main repository.
|
||||
This folder contains unreleased code. View the [examples for the latest release
|
||||
(0.25.0)](https://github.com/ratatui-org/ratatui/tree/v0.25.0/examples) instead.
|
||||
|
||||
> [!WARNING]
|
||||
>
|
||||
> There are backwards incompatible changes in these examples, as they are designed to compile
|
||||
> against the `main` branch.
|
||||
>
|
||||
> There are a few workaround for this problem:
|
||||
>
|
||||
> - View the examples as they were when the latest version was release by selecting the tag that
|
||||
> matches that version. E.g. <https://github.com/ratatui-org/ratatui/tree/v0.25.0/examples>. There
|
||||
> is a combo box at the top of this page which allows you to select any previous tagged version.
|
||||
> - To view the code locally, checkout the tag using `git switch --detach v0.25.0`.
|
||||
> - Use the latest [alpha version of Ratatui]. These are released weekly on Saturdays.
|
||||
> - Compile your code against the main branch either locally by adding e.g. `path = "../ratatui"` to
|
||||
> the dependency, or remotely by adding `git = "https://github.com/ratatui-org/ratatui"`
|
||||
>
|
||||
> For a list of unreleased breaking changes, see [BREAKING-CHANGES.md].
|
||||
>
|
||||
> We don't keep the CHANGELOG updated with unreleased changes, check the git commit history or run
|
||||
> `git-cliff -u` against a cloned version of this repository.
|
||||
|
||||
## Demo2
|
||||
|
||||
@@ -286,7 +305,8 @@ cargo run --example=tabs --features=crossterm
|
||||
|
||||
Demonstrates one approach to accepting user input. Source [user_input.rs](./user_input.rs).
|
||||
|
||||
> [!NOTE] Consider using [`tui-textarea`](https://crates.io/crates/tui-textarea) or
|
||||
> [!NOTE]
|
||||
> Consider using [`tui-textarea`](https://crates.io/crates/tui-textarea) or
|
||||
> [`tui-input`](https://crates.io/crates/tui-input) crates for more functional text entry UIs.
|
||||
|
||||
```shell
|
||||
@@ -295,13 +315,20 @@ cargo run --example=user_input --features=crossterm
|
||||
|
||||
![User Input][user_input.gif]
|
||||
|
||||
<!--
|
||||
links to images to make it easier to update in bulk
|
||||
These are generated with `vhs publish examples/xxx.gif`
|
||||
## How to update these examples
|
||||
|
||||
These gifs were created using [VHS](https://github.com/charmbracelet/vhs). Each example has a
|
||||
corresponding `.tape` file that holds instructions for how to generate the images. Note that the
|
||||
images themselves are stored in a separate `images` git branch to avoid bloating the main
|
||||
repository.
|
||||
|
||||
<!--
|
||||
|
||||
Links to images to make them easier to update in bulk. Use the following script to update and upload
|
||||
the examples to the images branch. (Requires push access to the branch).
|
||||
|
||||
To update these examples in bulk:
|
||||
```shell
|
||||
examples/generate.bash
|
||||
examples/vhs/generate.bash
|
||||
```
|
||||
-->
|
||||
|
||||
@@ -326,6 +353,9 @@ examples/generate.bash
|
||||
[ratatui-logo.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/ratatui-logo.gif?raw=true
|
||||
[scrollbar.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/scrollbar.gif?raw=true
|
||||
[sparkline.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/sparkline.gif?raw=true
|
||||
[table.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/table.gif?raw=true
|
||||
[table.gif]: https://vhs.charm.sh/vhs-6njXBytDf0rwPufUtmSSpI.gif
|
||||
[tabs.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/tabs.gif?raw=true
|
||||
[user_input.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/user_input.gif?raw=true
|
||||
|
||||
[alpha version of Ratatui]: https://crates.io/crates/ratatui/versions
|
||||
[BREAKING-CHANGES.md]: https://github.com/ratatui-org/ratatui/blob/main/BREAKING-CHANGES.md
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
//! # [Ratatui] `BarChart` example
|
||||
//!
|
||||
//! The latest version of this example is available in the [examples] folder in the repository.
|
||||
//!
|
||||
//! Please note that the examples are designed to be run against the `main` branch of the Github
|
||||
//! repository. This means that you may not be able to compile with the latest release version on
|
||||
//! crates.io, or the one that you have installed locally.
|
||||
//!
|
||||
//! See the [examples readme] for more information on finding examples that match the version of the
|
||||
//! library you are using.
|
||||
//!
|
||||
//! [Ratatui]: https://github.com/ratatui-org/ratatui
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
@@ -9,7 +24,10 @@ use crossterm::{
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{Bar, BarChart, BarGroup, Block, Borders, Paragraph},
|
||||
};
|
||||
|
||||
struct Company<'a> {
|
||||
revenue: [u64; 4],
|
||||
@@ -26,7 +44,7 @@ struct App<'a> {
|
||||
const TOTAL_REVENUE: &str = "Total Revenue";
|
||||
|
||||
impl<'a> App<'a> {
|
||||
fn new() -> App<'a> {
|
||||
fn new() -> Self {
|
||||
App {
|
||||
data: vec![
|
||||
("B1", 9),
|
||||
@@ -122,7 +140,7 @@ fn run_app<B: Backend>(
|
||||
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
if key.code == KeyCode::Char('q') {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
@@ -134,11 +152,11 @@ fn run_app<B: Backend>(
|
||||
}
|
||||
}
|
||||
|
||||
fn ui(f: &mut Frame, app: &App) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)])
|
||||
.split(f.size());
|
||||
fn ui(frame: &mut Frame, app: &App) {
|
||||
let vertical = Layout::vertical([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)]);
|
||||
let horizontal = Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]);
|
||||
let [top, bottom] = vertical.areas(frame.size());
|
||||
let [left, right] = horizontal.areas(bottom);
|
||||
|
||||
let barchart = BarChart::default()
|
||||
.block(Block::default().title("Data1").borders(Borders::ALL))
|
||||
@@ -146,17 +164,13 @@ fn ui(f: &mut Frame, app: &App) {
|
||||
.bar_width(9)
|
||||
.bar_style(Style::default().fg(Color::Yellow))
|
||||
.value_style(Style::default().fg(Color::Black).bg(Color::Yellow));
|
||||
f.render_widget(barchart, chunks[0]);
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
.split(chunks[1]);
|
||||
|
||||
draw_bar_with_group_labels(f, app, chunks[0]);
|
||||
draw_horizontal_bars(f, app, chunks[1]);
|
||||
frame.render_widget(barchart, top);
|
||||
draw_bar_with_group_labels(frame, app, left);
|
||||
draw_horizontal_bars(frame, app, right);
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
fn create_groups<'a>(app: &'a App, combine_values_and_labels: bool) -> Vec<BarGroup<'a>> {
|
||||
app.months
|
||||
.iter()
|
||||
@@ -190,13 +204,16 @@ fn create_groups<'a>(app: &'a App, combine_values_and_labels: bool) -> Vec<BarGr
|
||||
})
|
||||
.collect();
|
||||
BarGroup::default()
|
||||
.label(Line::from(month).alignment(Alignment::Center))
|
||||
.label(Line::from(month).centered())
|
||||
.bars(&bars)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
fn draw_bar_with_group_labels(f: &mut Frame, app: &App, area: Rect) {
|
||||
const LEGEND_HEIGHT: u16 = 6;
|
||||
|
||||
let groups = create_groups(app, false);
|
||||
|
||||
let mut barchart = BarChart::default()
|
||||
@@ -205,12 +222,11 @@ fn draw_bar_with_group_labels(f: &mut Frame, app: &App, area: Rect) {
|
||||
.group_gap(3);
|
||||
|
||||
for group in groups {
|
||||
barchart = barchart.data(group)
|
||||
barchart = barchart.data(group);
|
||||
}
|
||||
|
||||
f.render_widget(barchart, area);
|
||||
|
||||
const LEGEND_HEIGHT: u16 = 6;
|
||||
if area.height >= LEGEND_HEIGHT && area.width >= TOTAL_REVENUE.len() as u16 + 2 {
|
||||
let legend_width = TOTAL_REVENUE.len() as u16 + 2;
|
||||
let legend_area = Rect {
|
||||
@@ -223,7 +239,10 @@ fn draw_bar_with_group_labels(f: &mut Frame, app: &App, area: Rect) {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
fn draw_horizontal_bars(f: &mut Frame, app: &App, area: Rect) {
|
||||
const LEGEND_HEIGHT: u16 = 6;
|
||||
|
||||
let groups = create_groups(app, true);
|
||||
|
||||
let mut barchart = BarChart::default()
|
||||
@@ -234,12 +253,11 @@ fn draw_horizontal_bars(f: &mut Frame, app: &App, area: Rect) {
|
||||
.direction(Direction::Horizontal);
|
||||
|
||||
for group in groups {
|
||||
barchart = barchart.data(group)
|
||||
barchart = barchart.data(group);
|
||||
}
|
||||
|
||||
f.render_widget(barchart, area);
|
||||
|
||||
const LEGEND_HEIGHT: u16 = 6;
|
||||
if area.height >= LEGEND_HEIGHT && area.width >= TOTAL_REVENUE.len() as u16 + 2 {
|
||||
let legend_width = TOTAL_REVENUE.len() as u16 + 2;
|
||||
let legend_area = Rect {
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
//! # [Ratatui] Block example
|
||||
//!
|
||||
//! The latest version of this example is available in the [examples] folder in the repository.
|
||||
//!
|
||||
//! Please note that the examples are designed to be run against the `main` branch of the Github
|
||||
//! repository. This means that you may not be able to compile with the latest release version on
|
||||
//! crates.io, or the one that you have installed locally.
|
||||
//!
|
||||
//! See the [examples readme] for more information on finding examples that match the version of the
|
||||
//! library you are using.
|
||||
//!
|
||||
//! [Ratatui]: https://github.com/ratatui-org/ratatui
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
use std::{
|
||||
error::Error,
|
||||
io::{stdout, Stdout},
|
||||
@@ -62,7 +77,7 @@ fn run(terminal: &mut Terminal) -> Result<()> {
|
||||
fn handle_events() -> Result<ControlFlow<()>> {
|
||||
if event::poll(Duration::from_millis(100))? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
if key.code == KeyCode::Char('q') {
|
||||
return Ok(ControlFlow::Break(()));
|
||||
}
|
||||
}
|
||||
@@ -103,20 +118,14 @@ fn ui(frame: &mut Frame) {
|
||||
///
|
||||
/// Returns a tuple of the title area and the main areas.
|
||||
fn calculate_layout(area: Rect) -> (Rect, Vec<Vec<Rect>>) {
|
||||
let layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Length(1), Constraint::Min(0)])
|
||||
.split(area);
|
||||
let title_area = layout[0];
|
||||
let main_areas = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Max(4); 9])
|
||||
.split(layout[1])
|
||||
let main_layout = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
|
||||
let block_layout = Layout::vertical([Constraint::Max(4); 9]);
|
||||
let [title_area, main_area] = main_layout.areas(area);
|
||||
let main_areas = block_layout
|
||||
.split(main_area)
|
||||
.iter()
|
||||
.map(|&area| {
|
||||
Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
.split(area)
|
||||
.to_vec()
|
||||
})
|
||||
@@ -141,7 +150,7 @@ fn placeholder_paragraph() -> Paragraph<'static> {
|
||||
fn render_borders(paragraph: &Paragraph, border: Borders, frame: &mut Frame, area: Rect) {
|
||||
let block = Block::new()
|
||||
.borders(border)
|
||||
.title(format!("Borders::{border:#?}", border = border));
|
||||
.title(format!("Borders::{border:#?}"));
|
||||
frame.render_widget(paragraph.clone().block(block), area);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,21 @@
|
||||
use std::{error::Error, io, rc::Rc};
|
||||
//! # [Ratatui] Calendar example
|
||||
//!
|
||||
//! The latest version of this example is available in the [examples] folder in the repository.
|
||||
//!
|
||||
//! Please note that the examples are designed to be run against the `main` branch of the Github
|
||||
//! repository. This means that you may not be able to compile with the latest release version on
|
||||
//! crates.io, or the one that you have installed locally.
|
||||
//!
|
||||
//! See the [examples readme] for more information on finding examples that match the version of the
|
||||
//! library you are using.
|
||||
//!
|
||||
//! [Ratatui]: https://github.com/ratatui-org/ratatui
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
#![allow(clippy::wildcard_imports)]
|
||||
|
||||
use std::{error::Error, io};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
@@ -55,43 +72,19 @@ fn draw(f: &mut Frame) {
|
||||
|
||||
let list = make_dates(start.year());
|
||||
|
||||
for chunk in split_rows(&calarea)
|
||||
.iter()
|
||||
.flat_map(|row| split_cols(row).to_vec())
|
||||
{
|
||||
let rows = Layout::vertical([Constraint::Ratio(1, 3); 3]).split(calarea);
|
||||
let cols = rows.iter().flat_map(|row| {
|
||||
Layout::horizontal([Constraint::Ratio(1, 4); 4])
|
||||
.split(*row)
|
||||
.to_vec()
|
||||
});
|
||||
for col in cols {
|
||||
let cal = cals::get_cal(start.month(), start.year(), &list);
|
||||
f.render_widget(cal, chunk);
|
||||
f.render_widget(cal, col);
|
||||
start = start.replace_month(start.month().next()).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn split_rows(area: &Rect) -> Rc<[Rect]> {
|
||||
let list_layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(0)
|
||||
.constraints([
|
||||
Constraint::Percentage(33),
|
||||
Constraint::Percentage(33),
|
||||
Constraint::Percentage(33),
|
||||
]);
|
||||
|
||||
list_layout.split(*area)
|
||||
}
|
||||
|
||||
fn split_cols(area: &Rect) -> Rc<[Rect]> {
|
||||
let list_layout = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.margin(0)
|
||||
.constraints([
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
]);
|
||||
|
||||
list_layout.split(*area)
|
||||
}
|
||||
|
||||
fn make_dates(current_year: i32) -> CalendarEventStore {
|
||||
let mut list = CalendarEventStore::today(
|
||||
Style::default()
|
||||
@@ -175,20 +168,18 @@ fn make_dates(current_year: i32) -> CalendarEventStore {
|
||||
mod cals {
|
||||
use super::*;
|
||||
|
||||
pub(super) fn get_cal<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
|
||||
use Month::*;
|
||||
pub fn get_cal<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
|
||||
match m {
|
||||
May => example1(m, y, es),
|
||||
June => example2(m, y, es),
|
||||
July => example3(m, y, es),
|
||||
December => example3(m, y, es),
|
||||
February => example4(m, y, es),
|
||||
November => example5(m, y, es),
|
||||
Month::May => example1(m, y, es),
|
||||
Month::June => example2(m, y, es),
|
||||
Month::July | Month::December => example3(m, y, es),
|
||||
Month::February => example4(m, y, es),
|
||||
Month::November => example5(m, y, es),
|
||||
_ => default(m, y, es),
|
||||
}
|
||||
}
|
||||
|
||||
fn default<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
|
||||
fn default<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
|
||||
let default_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.bg(Color::Rgb(50, 50, 50));
|
||||
@@ -198,7 +189,7 @@ mod cals {
|
||||
.default_style(default_style)
|
||||
}
|
||||
|
||||
fn example1<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
|
||||
fn example1<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
|
||||
let default_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.bg(Color::Rgb(50, 50, 50));
|
||||
@@ -209,7 +200,7 @@ mod cals {
|
||||
.show_month_header(Style::default())
|
||||
}
|
||||
|
||||
fn example2<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
|
||||
fn example2<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
|
||||
let header_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.add_modifier(Modifier::DIM)
|
||||
@@ -225,7 +216,7 @@ mod cals {
|
||||
.show_month_header(Style::default())
|
||||
}
|
||||
|
||||
fn example3<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
|
||||
fn example3<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
|
||||
let header_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.fg(Color::Green);
|
||||
@@ -241,7 +232,7 @@ mod cals {
|
||||
.show_month_header(Style::default())
|
||||
}
|
||||
|
||||
fn example4<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
|
||||
fn example4<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
|
||||
let header_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.fg(Color::Green);
|
||||
@@ -255,7 +246,7 @@ mod cals {
|
||||
.default_style(default_style)
|
||||
}
|
||||
|
||||
fn example5<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
|
||||
fn example5<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
|
||||
let header_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.fg(Color::Green);
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
//! # [Ratatui] Canvas example
|
||||
//!
|
||||
//! The latest version of this example is available in the [examples] folder in the repository.
|
||||
//!
|
||||
//! Please note that the examples are designed to be run against the `main` branch of the Github
|
||||
//! repository. This means that you may not be able to compile with the latest release version on
|
||||
//! crates.io, or the one that you have installed locally.
|
||||
//!
|
||||
//! See the [examples readme] for more information on finding examples that match the version of the
|
||||
//! library you are using.
|
||||
//!
|
||||
//! [Ratatui]: https://github.com/ratatui-org/ratatui
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
#![allow(clippy::wildcard_imports)]
|
||||
|
||||
use std::{
|
||||
io::{self, stdout, Stdout},
|
||||
time::{Duration, Instant},
|
||||
@@ -29,8 +46,8 @@ struct App {
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn new() -> App {
|
||||
App {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
ball: Circle {
|
||||
@@ -49,7 +66,7 @@ impl App {
|
||||
|
||||
pub fn run() -> io::Result<()> {
|
||||
let mut terminal = init_terminal()?;
|
||||
let mut app = App::new();
|
||||
let mut app = Self::new();
|
||||
let mut last_tick = Instant::now();
|
||||
let tick_rate = Duration::from_millis(16);
|
||||
loop {
|
||||
@@ -91,13 +108,13 @@ impl App {
|
||||
// bounce the ball by flipping the velocity vector
|
||||
let ball = &self.ball;
|
||||
let playground = self.playground;
|
||||
if ball.x - ball.radius < playground.left() as f64
|
||||
|| ball.x + ball.radius > playground.right() as f64
|
||||
if ball.x - ball.radius < f64::from(playground.left())
|
||||
|| ball.x + ball.radius > f64::from(playground.right())
|
||||
{
|
||||
self.vx = -self.vx;
|
||||
}
|
||||
if ball.y - ball.radius < playground.top() as f64
|
||||
|| ball.y + ball.radius > playground.bottom() as f64
|
||||
if ball.y - ball.radius < f64::from(playground.top())
|
||||
|| ball.y + ball.radius > f64::from(playground.bottom())
|
||||
{
|
||||
self.vy = -self.vy;
|
||||
}
|
||||
@@ -107,19 +124,15 @@ impl App {
|
||||
}
|
||||
|
||||
fn ui(&self, frame: &mut Frame) {
|
||||
let main_layout = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
.split(frame.size());
|
||||
let horizontal =
|
||||
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]);
|
||||
let vertical = Layout::vertical([Constraint::Percentage(50), Constraint::Percentage(50)]);
|
||||
let [map, right] = horizontal.areas(frame.size());
|
||||
let [pong, boxes] = vertical.areas(right);
|
||||
|
||||
let right_layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
.split(main_layout[1]);
|
||||
|
||||
frame.render_widget(self.map_canvas(), main_layout[0]);
|
||||
frame.render_widget(self.pong_canvas(), right_layout[0]);
|
||||
frame.render_widget(self.boxes_canvas(right_layout[1]), right_layout[1]);
|
||||
frame.render_widget(self.map_canvas(), map);
|
||||
frame.render_widget(self.pong_canvas(), pong);
|
||||
frame.render_widget(self.boxes_canvas(boxes), boxes);
|
||||
}
|
||||
|
||||
fn map_canvas(&self) -> impl Widget + '_ {
|
||||
@@ -149,8 +162,10 @@ impl App {
|
||||
}
|
||||
|
||||
fn boxes_canvas(&self, area: Rect) -> impl Widget {
|
||||
let (left, right, bottom, top) =
|
||||
(0.0, area.width as f64, 0.0, area.height as f64 * 2.0 - 4.0);
|
||||
let left = 0.0;
|
||||
let right = f64::from(area.width);
|
||||
let bottom = 0.0;
|
||||
let top = f64::from(area.height).mul_add(2.0, -4.0);
|
||||
Canvas::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("Rects"))
|
||||
.marker(self.marker)
|
||||
@@ -159,26 +174,26 @@ impl App {
|
||||
.paint(|ctx| {
|
||||
for i in 0..=11 {
|
||||
ctx.draw(&Rectangle {
|
||||
x: (i * i + 3 * i) as f64 / 2.0 + 2.0,
|
||||
x: f64::from(i * i + 3 * i) / 2.0 + 2.0,
|
||||
y: 2.0,
|
||||
width: i as f64,
|
||||
height: i as f64,
|
||||
width: f64::from(i),
|
||||
height: f64::from(i),
|
||||
color: Color::Red,
|
||||
});
|
||||
ctx.draw(&Rectangle {
|
||||
x: (i * i + 3 * i) as f64 / 2.0 + 2.0,
|
||||
x: f64::from(i * i + 3 * i) / 2.0 + 2.0,
|
||||
y: 21.0,
|
||||
width: i as f64,
|
||||
height: i as f64,
|
||||
width: f64::from(i),
|
||||
height: f64::from(i),
|
||||
color: Color::Blue,
|
||||
});
|
||||
}
|
||||
for i in 0..100 {
|
||||
if i % 10 != 0 {
|
||||
ctx.print(i as f64 + 1.0, 0.0, format!("{i}", i = i % 10));
|
||||
ctx.print(f64::from(i) + 1.0, 0.0, format!("{i}", i = i % 10));
|
||||
}
|
||||
if i % 2 == 0 && i % 10 != 0 {
|
||||
ctx.print(0.0, i as f64, format!("{i}", i = i % 10));
|
||||
ctx.print(0.0, f64::from(i), format!("{i}", i = i % 10));
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
//! # [Ratatui] Chart example
|
||||
//!
|
||||
//! The latest version of this example is available in the [examples] folder in the repository.
|
||||
//!
|
||||
//! Please note that the examples are designed to be run against the `main` branch of the Github
|
||||
//! repository. This means that you may not be able to compile with the latest release version on
|
||||
//! crates.io, or the one that you have installed locally.
|
||||
//!
|
||||
//! See the [examples readme] for more information on finding examples that match the version of the
|
||||
//! library you are using.
|
||||
//!
|
||||
//! [Ratatui]: https://github.com/ratatui-org/ratatui
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
@@ -9,21 +24,13 @@ use crossterm::{
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
const DATA: [(f64, f64); 5] = [(0.0, 0.0), (1.0, 1.0), (2.0, 2.0), (3.0, 3.0), (4.0, 4.0)];
|
||||
const DATA2: [(f64, f64); 7] = [
|
||||
(0.0, 0.0),
|
||||
(10.0, 1.0),
|
||||
(20.0, 0.5),
|
||||
(30.0, 1.5),
|
||||
(40.0, 1.0),
|
||||
(50.0, 2.5),
|
||||
(60.0, 3.0),
|
||||
];
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{block::Title, Axis, Block, Borders, Chart, Dataset, GraphType, LegendPosition},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SinSignal {
|
||||
struct SinSignal {
|
||||
x: f64,
|
||||
interval: f64,
|
||||
period: f64,
|
||||
@@ -31,8 +38,8 @@ pub struct SinSignal {
|
||||
}
|
||||
|
||||
impl SinSignal {
|
||||
pub fn new(interval: f64, period: f64, scale: f64) -> SinSignal {
|
||||
SinSignal {
|
||||
const fn new(interval: f64, period: f64, scale: f64) -> Self {
|
||||
Self {
|
||||
x: 0.0,
|
||||
interval,
|
||||
period,
|
||||
@@ -59,12 +66,12 @@ struct App {
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn new() -> App {
|
||||
fn new() -> Self {
|
||||
let mut signal1 = SinSignal::new(0.2, 3.0, 18.0);
|
||||
let mut signal2 = SinSignal::new(0.1, 2.0, 10.0);
|
||||
let data1 = signal1.by_ref().take(200).collect::<Vec<(f64, f64)>>();
|
||||
let data2 = signal2.by_ref().take(200).collect::<Vec<(f64, f64)>>();
|
||||
App {
|
||||
Self {
|
||||
signal1,
|
||||
data1,
|
||||
signal2,
|
||||
@@ -74,14 +81,12 @@ impl App {
|
||||
}
|
||||
|
||||
fn on_tick(&mut self) {
|
||||
for _ in 0..5 {
|
||||
self.data1.remove(0);
|
||||
}
|
||||
self.data1.drain(0..5);
|
||||
self.data1.extend(self.signal1.by_ref().take(5));
|
||||
for _ in 0..10 {
|
||||
self.data2.remove(0);
|
||||
}
|
||||
|
||||
self.data2.drain(0..10);
|
||||
self.data2.extend(self.signal2.by_ref().take(10));
|
||||
|
||||
self.window[0] += 1.0;
|
||||
self.window[1] += 1.0;
|
||||
}
|
||||
@@ -128,7 +133,7 @@ fn run_app<B: Backend>(
|
||||
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
if key.code == KeyCode::Char('q') {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
@@ -140,16 +145,20 @@ fn run_app<B: Backend>(
|
||||
}
|
||||
}
|
||||
|
||||
fn ui(f: &mut Frame, app: &App) {
|
||||
let size = f.size();
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Ratio(1, 3),
|
||||
Constraint::Ratio(1, 3),
|
||||
Constraint::Ratio(1, 3),
|
||||
])
|
||||
.split(size);
|
||||
fn ui(frame: &mut Frame, app: &App) {
|
||||
let area = frame.size();
|
||||
|
||||
let vertical = Layout::vertical([Constraint::Percentage(40), Constraint::Percentage(60)]);
|
||||
let horizontal = Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]);
|
||||
let [chart1, bottom] = vertical.areas(area);
|
||||
let [line_chart, scatter] = horizontal.areas(bottom);
|
||||
|
||||
render_chart1(frame, chart1, app);
|
||||
render_line_chart(frame, line_chart);
|
||||
render_scatter(frame, scatter);
|
||||
}
|
||||
|
||||
fn render_chart1(f: &mut Frame, area: Rect, app: &App) {
|
||||
let x_labels = vec![
|
||||
Span::styled(
|
||||
format!("{}", app.window[0]),
|
||||
@@ -194,64 +203,164 @@ fn ui(f: &mut Frame, app: &App) {
|
||||
.labels(vec!["-20".bold(), "0".into(), "20".bold()])
|
||||
.bounds([-20.0, 20.0]),
|
||||
);
|
||||
f.render_widget(chart, chunks[0]);
|
||||
|
||||
f.render_widget(chart, area);
|
||||
}
|
||||
|
||||
fn render_line_chart(f: &mut Frame, area: Rect) {
|
||||
let datasets = vec![Dataset::default()
|
||||
.name("data")
|
||||
.name("Line from only 2 points".italic())
|
||||
.marker(symbols::Marker::Braille)
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.graph_type(GraphType::Line)
|
||||
.data(&DATA)];
|
||||
.data(&[(1., 1.), (4., 4.)])];
|
||||
|
||||
let chart = Chart::new(datasets)
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Chart 2".cyan().bold())
|
||||
.title(
|
||||
Title::default()
|
||||
.content("Line chart".cyan().bold())
|
||||
.alignment(Alignment::Center),
|
||||
)
|
||||
.borders(Borders::ALL),
|
||||
)
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
.title("X Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.style(Style::default().gray())
|
||||
.bounds([0.0, 5.0])
|
||||
.labels(vec!["0".bold(), "2.5".into(), "5.0".bold()]),
|
||||
)
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.title("Y Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.style(Style::default().gray())
|
||||
.bounds([0.0, 5.0])
|
||||
.labels(vec!["0".bold(), "2.5".into(), "5.0".bold()]),
|
||||
)
|
||||
.hidden_legend_constraints((Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)));
|
||||
f.render_widget(chart, chunks[1]);
|
||||
|
||||
let datasets = vec![Dataset::default()
|
||||
.name("data")
|
||||
.marker(symbols::Marker::Braille)
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.graph_type(GraphType::Line)
|
||||
.data(&DATA2)];
|
||||
let chart = Chart::new(datasets)
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Chart 3".cyan().bold())
|
||||
.borders(Borders::ALL),
|
||||
)
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
.title("X Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.bounds([0.0, 50.0])
|
||||
.labels(vec!["0".bold(), "25".into(), "50".bold()]),
|
||||
)
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.title("Y Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.bounds([0.0, 5.0])
|
||||
.labels(vec!["0".bold(), "2.5".into(), "5".bold()]),
|
||||
)
|
||||
.legend_position(Some(LegendPosition::TopLeft))
|
||||
.hidden_legend_constraints((Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)));
|
||||
f.render_widget(chart, chunks[2]);
|
||||
|
||||
f.render_widget(chart, area);
|
||||
}
|
||||
|
||||
fn render_scatter(f: &mut Frame, area: Rect) {
|
||||
let datasets = vec![
|
||||
Dataset::default()
|
||||
.name("Heavy")
|
||||
.marker(Marker::Dot)
|
||||
.graph_type(GraphType::Scatter)
|
||||
.style(Style::new().yellow())
|
||||
.data(&HEAVY_PAYLOAD_DATA),
|
||||
Dataset::default()
|
||||
.name("Medium".underlined())
|
||||
.marker(Marker::Braille)
|
||||
.graph_type(GraphType::Scatter)
|
||||
.style(Style::new().magenta())
|
||||
.data(&MEDIUM_PAYLOAD_DATA),
|
||||
Dataset::default()
|
||||
.name("Small")
|
||||
.marker(Marker::Dot)
|
||||
.graph_type(GraphType::Scatter)
|
||||
.style(Style::new().cyan())
|
||||
.data(&SMALL_PAYLOAD_DATA),
|
||||
];
|
||||
|
||||
let chart = Chart::new(datasets)
|
||||
.block(
|
||||
Block::new().borders(Borders::all()).title(
|
||||
Title::default()
|
||||
.content("Scatter chart".cyan().bold())
|
||||
.alignment(Alignment::Center),
|
||||
),
|
||||
)
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
.title("Year")
|
||||
.bounds([1960., 2020.])
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.labels(vec!["1960".into(), "1990".into(), "2020".into()]),
|
||||
)
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.title("Cost")
|
||||
.bounds([0., 75000.])
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.labels(vec!["0".into(), "37 500".into(), "75 000".into()]),
|
||||
)
|
||||
.hidden_legend_constraints((Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)));
|
||||
|
||||
f.render_widget(chart, area);
|
||||
}
|
||||
|
||||
// Data from https://ourworldindata.org/space-exploration-satellites
|
||||
const HEAVY_PAYLOAD_DATA: [(f64, f64); 9] = [
|
||||
(1965., 8200.),
|
||||
(1967., 5400.),
|
||||
(1981., 65400.),
|
||||
(1989., 30800.),
|
||||
(1997., 10200.),
|
||||
(2004., 11600.),
|
||||
(2014., 4500.),
|
||||
(2016., 7900.),
|
||||
(2018., 1500.),
|
||||
];
|
||||
|
||||
const MEDIUM_PAYLOAD_DATA: [(f64, f64); 29] = [
|
||||
(1963., 29500.),
|
||||
(1964., 30600.),
|
||||
(1965., 177_900.),
|
||||
(1965., 21000.),
|
||||
(1966., 17900.),
|
||||
(1966., 8400.),
|
||||
(1975., 17500.),
|
||||
(1982., 8300.),
|
||||
(1985., 5100.),
|
||||
(1988., 18300.),
|
||||
(1990., 38800.),
|
||||
(1990., 9900.),
|
||||
(1991., 18700.),
|
||||
(1992., 9100.),
|
||||
(1994., 10500.),
|
||||
(1994., 8500.),
|
||||
(1994., 8700.),
|
||||
(1997., 6200.),
|
||||
(1999., 18000.),
|
||||
(1999., 7600.),
|
||||
(1999., 8900.),
|
||||
(1999., 9600.),
|
||||
(2000., 16000.),
|
||||
(2001., 10000.),
|
||||
(2002., 10400.),
|
||||
(2002., 8100.),
|
||||
(2010., 2600.),
|
||||
(2013., 13600.),
|
||||
(2017., 8000.),
|
||||
];
|
||||
|
||||
const SMALL_PAYLOAD_DATA: [(f64, f64); 23] = [
|
||||
(1961., 118_500.),
|
||||
(1962., 14900.),
|
||||
(1975., 21400.),
|
||||
(1980., 32800.),
|
||||
(1988., 31100.),
|
||||
(1990., 41100.),
|
||||
(1993., 23600.),
|
||||
(1994., 20600.),
|
||||
(1994., 34600.),
|
||||
(1996., 50600.),
|
||||
(1997., 19200.),
|
||||
(1997., 45800.),
|
||||
(1998., 19100.),
|
||||
(2000., 73100.),
|
||||
(2003., 11200.),
|
||||
(2008., 12600.),
|
||||
(2010., 30500.),
|
||||
(2012., 20000.),
|
||||
(2013., 10600.),
|
||||
(2013., 34500.),
|
||||
(2015., 10600.),
|
||||
(2018., 23100.),
|
||||
(2019., 17300.),
|
||||
];
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
/// This example shows all the colors supported by ratatui. It will render a grid of foreground
|
||||
/// and background colors with their names and indexes.
|
||||
//! # [Ratatui] Colors example
|
||||
//!
|
||||
//! The latest version of this example is available in the [examples] folder in the repository.
|
||||
//!
|
||||
//! Please note that the examples are designed to be run against the `main` branch of the Github
|
||||
//! repository. This means that you may not be able to compile with the latest release version on
|
||||
//! crates.io, or the one that you have installed locally.
|
||||
//!
|
||||
//! See the [examples readme] for more information on finding examples that match the version of the
|
||||
//! library you are using.
|
||||
//!
|
||||
//! [Ratatui]: https://github.com/ratatui-org/ratatui
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
// This example shows all the colors supported by ratatui. It will render a grid of foreground
|
||||
// and background colors with their names and indexes.
|
||||
|
||||
use std::{
|
||||
error::Error,
|
||||
io::{self, Stdout},
|
||||
@@ -13,7 +29,10 @@ use crossterm::{
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{Block, Borders, Paragraph},
|
||||
};
|
||||
|
||||
type Result<T> = result::Result<T, Box<dyn Error>>;
|
||||
|
||||
@@ -33,7 +52,7 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
|
||||
|
||||
if event::poll(Duration::from_millis(250))? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
if key.code == KeyCode::Char('q') {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
@@ -42,14 +61,12 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
|
||||
}
|
||||
|
||||
fn ui(frame: &mut Frame) {
|
||||
let layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(30),
|
||||
Constraint::Length(17),
|
||||
Constraint::Length(2),
|
||||
])
|
||||
.split(frame.size());
|
||||
let layout = Layout::vertical([
|
||||
Constraint::Length(30),
|
||||
Constraint::Length(17),
|
||||
Constraint::Length(2),
|
||||
])
|
||||
.split(frame.size());
|
||||
|
||||
render_named_colors(frame, layout[0]);
|
||||
render_indexed_colors(frame, layout[1]);
|
||||
@@ -76,10 +93,7 @@ const NAMED_COLORS: [Color; 16] = [
|
||||
];
|
||||
|
||||
fn render_named_colors(frame: &mut Frame, area: Rect) {
|
||||
let layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Length(3); 10])
|
||||
.split(area);
|
||||
let layout = Layout::vertical([Constraint::Length(3); 10]).split(area);
|
||||
|
||||
render_fg_named_colors(frame, Color::Reset, layout[0]);
|
||||
render_fg_named_colors(frame, Color::Black, layout[1]);
|
||||
@@ -99,15 +113,11 @@ fn render_fg_named_colors(frame: &mut Frame, bg: Color, area: Rect) {
|
||||
let inner = block.inner(area);
|
||||
frame.render_widget(block, area);
|
||||
|
||||
let layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Length(1); 2])
|
||||
let layout = Layout::vertical([Constraint::Length(1); 2])
|
||||
.split(inner)
|
||||
.iter()
|
||||
.flat_map(|area| {
|
||||
Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Ratio(1, 8); 8])
|
||||
Layout::horizontal([Constraint::Ratio(1, 8); 8])
|
||||
.split(*area)
|
||||
.to_vec()
|
||||
})
|
||||
@@ -124,15 +134,11 @@ fn render_bg_named_colors(frame: &mut Frame, fg: Color, area: Rect) {
|
||||
let inner = block.inner(area);
|
||||
frame.render_widget(block, area);
|
||||
|
||||
let layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Length(1); 2])
|
||||
let layout = Layout::vertical([Constraint::Length(1); 2])
|
||||
.split(inner)
|
||||
.iter()
|
||||
.flat_map(|area| {
|
||||
Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Ratio(1, 8); 8])
|
||||
Layout::horizontal([Constraint::Ratio(1, 8); 8])
|
||||
.split(*area)
|
||||
.to_vec()
|
||||
})
|
||||
@@ -149,23 +155,18 @@ fn render_indexed_colors(frame: &mut Frame, area: Rect) {
|
||||
let inner = block.inner(area);
|
||||
frame.render_widget(block, area);
|
||||
|
||||
let layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(1), // 0 - 15
|
||||
Constraint::Length(1), // blank
|
||||
Constraint::Min(6), // 16 - 123
|
||||
Constraint::Length(1), // blank
|
||||
Constraint::Min(6), // 124 - 231
|
||||
Constraint::Length(1), // blank
|
||||
])
|
||||
.split(inner);
|
||||
let layout = Layout::vertical([
|
||||
Constraint::Length(1), // 0 - 15
|
||||
Constraint::Length(1), // blank
|
||||
Constraint::Min(6), // 16 - 123
|
||||
Constraint::Length(1), // blank
|
||||
Constraint::Min(6), // 124 - 231
|
||||
Constraint::Length(1), // blank
|
||||
])
|
||||
.split(inner);
|
||||
|
||||
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
||||
let color_layout = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Length(5); 16])
|
||||
.split(layout[0]);
|
||||
let color_layout = Layout::horizontal([Constraint::Length(5); 16]).split(layout[0]);
|
||||
for i in 0..16 {
|
||||
let color = Color::Indexed(i);
|
||||
let color_index = format!("{i:0>2}");
|
||||
@@ -196,25 +197,19 @@ fn render_indexed_colors(frame: &mut Frame, area: Rect) {
|
||||
.iter()
|
||||
// two rows of 3 columns
|
||||
.flat_map(|area| {
|
||||
Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Length(27); 3])
|
||||
Layout::horizontal([Constraint::Length(27); 3])
|
||||
.split(*area)
|
||||
.to_vec()
|
||||
})
|
||||
// each with 6 rows
|
||||
.flat_map(|area| {
|
||||
Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Length(1); 6])
|
||||
Layout::vertical([Constraint::Length(1); 6])
|
||||
.split(area)
|
||||
.to_vec()
|
||||
})
|
||||
// each with 6 columns
|
||||
.flat_map(|area| {
|
||||
Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Min(4); 6])
|
||||
Layout::horizontal([Constraint::Min(4); 6])
|
||||
.split(area)
|
||||
.to_vec()
|
||||
})
|
||||
@@ -244,22 +239,18 @@ fn title_block(title: String) -> Block<'static> {
|
||||
}
|
||||
|
||||
fn render_indexed_grayscale(frame: &mut Frame, area: Rect) {
|
||||
let layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(1), // 232 - 243
|
||||
Constraint::Length(1), // 244 - 255
|
||||
])
|
||||
.split(area)
|
||||
.iter()
|
||||
.flat_map(|area| {
|
||||
Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Length(6); 12])
|
||||
.split(*area)
|
||||
.to_vec()
|
||||
})
|
||||
.collect_vec();
|
||||
let layout = Layout::vertical([
|
||||
Constraint::Length(1), // 232 - 243
|
||||
Constraint::Length(1), // 244 - 255
|
||||
])
|
||||
.split(area)
|
||||
.iter()
|
||||
.flat_map(|area| {
|
||||
Layout::horizontal([Constraint::Length(6); 12])
|
||||
.split(*area)
|
||||
.to_vec()
|
||||
})
|
||||
.collect_vec();
|
||||
|
||||
for i in 232..=255 {
|
||||
let color = Color::Indexed(i);
|
||||
|
||||
@@ -1,107 +1,247 @@
|
||||
/// This example shows the full range of RGB colors that can be displayed in the terminal.
|
||||
///
|
||||
/// Requires a terminal that supports 24-bit color (true color) and unicode.
|
||||
//! # [Ratatui] `Colors_RGB` example
|
||||
//!
|
||||
//! The latest version of this example is available in the [examples] folder in the repository.
|
||||
//!
|
||||
//! Please note that the examples are designed to be run against the `main` branch of the Github
|
||||
//! repository. This means that you may not be able to compile with the latest release version on
|
||||
//! crates.io, or the one that you have installed locally.
|
||||
//!
|
||||
//! See the [examples readme] for more information on finding examples that match the version of the
|
||||
//! library you are using.
|
||||
//!
|
||||
//! [Ratatui]: https://github.com/ratatui-org/ratatui
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
// This example shows the full range of RGB colors that can be displayed in the terminal.
|
||||
//
|
||||
// Requires a terminal that supports 24-bit color (true color) and unicode.
|
||||
//
|
||||
// This example also demonstrates how implementing the Widget trait on a mutable reference
|
||||
// allows the widget to update its state while it is being rendered. This allows the fps
|
||||
// widget to update the fps calculation and the colors widget to update a cached version of
|
||||
// the colors to render instead of recalculating them every frame.
|
||||
//
|
||||
// This is an alternative to using the `StatefulWidget` trait and a separate state struct. It
|
||||
// is useful when the state is only used by the widget and doesn't need to be shared with
|
||||
// other widgets.
|
||||
|
||||
use std::{
|
||||
io::stdout,
|
||||
panic,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use color_eyre::config::HookBuilder;
|
||||
use color_eyre::{config::HookBuilder, eyre, Result};
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode, KeyEventKind},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
};
|
||||
use palette::{convert::FromColorUnclamped, Okhsv, Srgb};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
fn main() -> color_eyre::Result<()> {
|
||||
App::run()
|
||||
}
|
||||
use ratatui::prelude::*;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct App {
|
||||
should_quit: bool,
|
||||
// a 2d vec of the colors to render, calculated when the size changes as this is expensive
|
||||
// to calculate every frame
|
||||
colors: Vec<Vec<Color>>,
|
||||
last_size: Rect,
|
||||
fps: Fps,
|
||||
frame_count: usize,
|
||||
/// The current state of the app (running or quit)
|
||||
state: AppState,
|
||||
|
||||
/// A widget that displays the current frames per second
|
||||
fps_widget: FpsWidget,
|
||||
|
||||
/// A widget that displays the full range of RGB colors that can be displayed in the terminal.
|
||||
colors_widget: ColorsWidget,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq)]
|
||||
enum AppState {
|
||||
/// The app is running
|
||||
#[default]
|
||||
Running,
|
||||
|
||||
/// The user has requested the app to quit
|
||||
Quit,
|
||||
}
|
||||
|
||||
/// A widget that displays the current frames per second
|
||||
#[derive(Debug)]
|
||||
struct Fps {
|
||||
struct FpsWidget {
|
||||
/// The number of elapsed frames that have passed - used to calculate the fps
|
||||
frame_count: usize,
|
||||
|
||||
/// The last instant that the fps was calculated
|
||||
last_instant: Instant,
|
||||
|
||||
/// The current frames per second
|
||||
fps: Option<f32>,
|
||||
}
|
||||
|
||||
struct AppWidget<'a> {
|
||||
title: Paragraph<'a>,
|
||||
fps_widget: FpsWidget<'a>,
|
||||
rgb_colors_widget: RgbColorsWidget<'a>,
|
||||
}
|
||||
/// A widget that displays the full range of RGB colors that can be displayed in the terminal.
|
||||
///
|
||||
/// This widget is animated and will change colors over time.
|
||||
#[derive(Debug, Default)]
|
||||
struct ColorsWidget {
|
||||
/// The colors to render - should be double the height of the area as we render two rows of
|
||||
/// pixels for each row of the widget using the half block character. This is computed any time
|
||||
/// the size of the widget changes.
|
||||
colors: Vec<Vec<Color>>,
|
||||
|
||||
struct FpsWidget<'a> {
|
||||
fps: &'a Fps,
|
||||
}
|
||||
|
||||
struct RgbColorsWidget<'a> {
|
||||
/// The colors to render - should be double the height of the area
|
||||
colors: &'a Vec<Vec<Color>>,
|
||||
/// the number of elapsed frames that have passed - used to animate the colors
|
||||
/// the number of elapsed frames that have passed - used to animate the colors by shifting the
|
||||
/// x index by the frame number
|
||||
frame_count: usize,
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
install_error_hooks()?;
|
||||
let terminal = init_terminal()?;
|
||||
App::default().run(terminal)?;
|
||||
restore_terminal()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn run() -> color_eyre::Result<()> {
|
||||
install_panic_hook()?;
|
||||
|
||||
let mut terminal = init_terminal()?;
|
||||
let mut app = Self::default();
|
||||
|
||||
while !app.should_quit {
|
||||
app.tick();
|
||||
terminal.draw(|frame| {
|
||||
let size = frame.size();
|
||||
app.setup_colors(size);
|
||||
frame.render_widget(AppWidget::new(&app), size);
|
||||
})?;
|
||||
app.handle_events()?;
|
||||
/// Run the app
|
||||
///
|
||||
/// This is the main event loop for the app.
|
||||
pub fn run(mut self, mut terminal: Terminal<impl Backend>) -> Result<()> {
|
||||
while self.is_running() {
|
||||
terminal.draw(|frame| frame.render_widget(&mut self, frame.size()))?;
|
||||
self.handle_events()?;
|
||||
}
|
||||
restore_terminal()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn tick(&mut self) {
|
||||
self.frame_count += 1;
|
||||
self.fps.tick();
|
||||
const fn is_running(&self) -> bool {
|
||||
matches!(self.state, AppState::Running)
|
||||
}
|
||||
|
||||
fn handle_events(&mut self) -> color_eyre::Result<()> {
|
||||
if event::poll(Duration::from_secs_f32(1.0 / 60.0))? {
|
||||
/// Handle any events that have occurred since the last time the app was rendered.
|
||||
///
|
||||
/// Currently, this only handles the q key to quit the app.
|
||||
fn handle_events(&mut self) -> Result<()> {
|
||||
// Ensure that the app only blocks for a period that allows the app to render at
|
||||
// approximately 60 FPS (this doesn't account for the time to render the frame, and will
|
||||
// also update the app immediately any time an event occurs)
|
||||
let timeout = Duration::from_secs_f32(1.0 / 60.0);
|
||||
if event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
|
||||
self.should_quit = true;
|
||||
self.state = AppState::Quit;
|
||||
};
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_colors(&mut self, size: Rect) {
|
||||
// only update the colors if the size has changed since the last time we rendered
|
||||
if self.last_size.width == size.width && self.last_size.height == size.height {
|
||||
return;
|
||||
/// Implement the Widget trait for &mut App so that it can be rendered
|
||||
///
|
||||
/// This is implemented on a mutable reference so that the app can update its state while it is
|
||||
/// being rendered. This allows the fps widget to update the fps calculation and the colors widget
|
||||
/// to update the colors to render.
|
||||
impl Widget for &mut App {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
#[allow(clippy::enum_glob_use)]
|
||||
use Constraint::*;
|
||||
let [top, colors] = Layout::vertical([Length(1), Min(0)]).areas(area);
|
||||
let [title, fps] = Layout::horizontal([Min(0), Length(8)]).areas(top);
|
||||
Text::from("colors_rgb example. Press q to quit")
|
||||
.centered()
|
||||
.render(title, buf);
|
||||
self.fps_widget.render(fps, buf);
|
||||
self.colors_widget.render(colors, buf);
|
||||
}
|
||||
}
|
||||
|
||||
/// Default impl for `FpsWidget`
|
||||
///
|
||||
/// Manual impl is required because we need to initialize the `last_instant` field to the current
|
||||
/// instant.
|
||||
impl Default for FpsWidget {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
frame_count: 0,
|
||||
last_instant: Instant::now(),
|
||||
fps: None,
|
||||
}
|
||||
self.last_size = size;
|
||||
}
|
||||
}
|
||||
|
||||
/// Widget impl for `FpsWidget`
|
||||
///
|
||||
/// This is implemented on a mutable reference so that we can update the frame count and fps
|
||||
/// calculation while rendering.
|
||||
impl Widget for &mut FpsWidget {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.calculate_fps();
|
||||
if let Some(fps) = self.fps {
|
||||
let text = format!("{fps:.1} fps");
|
||||
Text::from(text).render(area, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FpsWidget {
|
||||
/// Update the fps calculation.
|
||||
///
|
||||
/// This updates the fps once a second, but only if the widget has rendered at least 2 frames
|
||||
/// since the last calculation. This avoids noise in the fps calculation when rendering on slow
|
||||
/// machines that can't render at least 2 frames per second.
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
fn calculate_fps(&mut self) {
|
||||
self.frame_count += 1;
|
||||
let elapsed = self.last_instant.elapsed();
|
||||
if elapsed > Duration::from_secs(1) && self.frame_count > 2 {
|
||||
self.fps = Some(self.frame_count as f32 / elapsed.as_secs_f32());
|
||||
self.frame_count = 0;
|
||||
self.last_instant = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Widget impl for `ColorsWidget`
|
||||
///
|
||||
/// This is implemented on a mutable reference so that we can update the frame count and store a
|
||||
/// cached version of the colors to render instead of recalculating them every frame.
|
||||
impl Widget for &mut ColorsWidget {
|
||||
/// Render the widget
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.setup_colors(area);
|
||||
let colors = &self.colors;
|
||||
for (xi, x) in (area.left()..area.right()).enumerate() {
|
||||
// animate the colors by shifting the x index by the frame number
|
||||
let xi = (xi + self.frame_count) % (area.width as usize);
|
||||
for (yi, y) in (area.top()..area.bottom()).enumerate() {
|
||||
// render a half block character for each row of pixels with the foreground color
|
||||
// set to the color of the pixel and the background color set to the color of the
|
||||
// pixel below it
|
||||
let fg = colors[yi * 2][xi];
|
||||
let bg = colors[yi * 2 + 1][xi];
|
||||
buf.get_mut(x, y).set_char('▀').set_fg(fg).set_bg(bg);
|
||||
}
|
||||
}
|
||||
self.frame_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
impl ColorsWidget {
|
||||
/// Setup the colors to render.
|
||||
///
|
||||
/// This is called once per frame to setup the colors to render. It caches the colors so that
|
||||
/// they don't need to be recalculated every frame.
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
fn setup_colors(&mut self, size: Rect) {
|
||||
let Rect { width, height, .. } = size;
|
||||
// double the height because each screen row has two rows of half block pixels
|
||||
let height = height * 2;
|
||||
self.colors.clear();
|
||||
let height = height as usize * 2;
|
||||
let width = width as usize;
|
||||
// only update the colors if the size has changed since the last time we rendered
|
||||
if self.colors.len() == height && self.colors[0].len() == width {
|
||||
return;
|
||||
}
|
||||
self.colors = Vec::with_capacity(height);
|
||||
for y in 0..height {
|
||||
let mut row = Vec::new();
|
||||
let mut row = Vec::with_capacity(width);
|
||||
for x in 0..width {
|
||||
let hue = x as f32 * 360.0 / width as f32;
|
||||
let value = (height - y) as f32 / height as f32;
|
||||
@@ -117,103 +257,25 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
impl Fps {
|
||||
fn tick(&mut self) {
|
||||
self.frame_count += 1;
|
||||
let elapsed = self.last_instant.elapsed();
|
||||
// update the fps every second, but only if we've rendered at least 2 frames (to avoid
|
||||
// noise in the fps calculation)
|
||||
if elapsed > Duration::from_secs(1) && self.frame_count > 2 {
|
||||
self.fps = Some(self.frame_count as f32 / elapsed.as_secs_f32());
|
||||
self.frame_count = 0;
|
||||
self.last_instant = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Fps {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
frame_count: 0,
|
||||
last_instant: Instant::now(),
|
||||
fps: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> AppWidget<'a> {
|
||||
fn new(app: &'a App) -> Self {
|
||||
let title =
|
||||
Paragraph::new("colors_rgb example. Press q to quit").alignment(Alignment::Center);
|
||||
Self {
|
||||
title,
|
||||
fps_widget: FpsWidget { fps: &app.fps },
|
||||
rgb_colors_widget: RgbColorsWidget {
|
||||
colors: &app.colors,
|
||||
frame_count: app.frame_count,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for AppWidget<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let main_layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Length(1), Constraint::Min(0)])
|
||||
.split(area);
|
||||
let title_layout = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Min(0), Constraint::Length(8)])
|
||||
.split(main_layout[0]);
|
||||
|
||||
self.title.render(title_layout[0], buf);
|
||||
self.fps_widget.render(title_layout[1], buf);
|
||||
self.rgb_colors_widget.render(main_layout[1], buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for RgbColorsWidget<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let colors = self.colors;
|
||||
for (xi, x) in (area.left()..area.right()).enumerate() {
|
||||
// animate the colors by shifting the x index by the frame number
|
||||
let xi = (xi + self.frame_count) % (area.width as usize);
|
||||
for (yi, y) in (area.top()..area.bottom()).enumerate() {
|
||||
let fg = colors[yi * 2][xi];
|
||||
let bg = colors[yi * 2 + 1][xi];
|
||||
buf.get_mut(x, y).set_char('▀').set_fg(fg).set_bg(bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for FpsWidget<'a> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
if let Some(fps) = self.fps.fps {
|
||||
let text = format!("{:.1} fps", fps);
|
||||
Paragraph::new(text).render(area, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Install a panic hook that restores the terminal before panicking.
|
||||
fn install_panic_hook() -> color_eyre::Result<()> {
|
||||
/// Install `color_eyre` panic and error hooks
|
||||
///
|
||||
/// The hooks restore the terminal to a usable state before printing the error message.
|
||||
fn install_error_hooks() -> Result<()> {
|
||||
let (panic, error) = HookBuilder::default().into_hooks();
|
||||
let panic = panic.into_panic_hook();
|
||||
let error = error.into_eyre_hook();
|
||||
color_eyre::eyre::set_hook(Box::new(move |e| {
|
||||
eyre::set_hook(Box::new(move |e| {
|
||||
let _ = restore_terminal();
|
||||
error(e)
|
||||
}))?;
|
||||
std::panic::set_hook(Box::new(move |info| {
|
||||
panic::set_hook(Box::new(move |info| {
|
||||
let _ = restore_terminal();
|
||||
panic(info)
|
||||
panic(info);
|
||||
}));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn init_terminal() -> color_eyre::Result<Terminal<impl Backend>> {
|
||||
fn init_terminal() -> Result<Terminal<impl Backend>> {
|
||||
enable_raw_mode()?;
|
||||
stdout().execute(EnterAlternateScreen)?;
|
||||
let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
|
||||
@@ -222,7 +284,7 @@ fn init_terminal() -> color_eyre::Result<Terminal<impl Backend>> {
|
||||
Ok(terminal)
|
||||
}
|
||||
|
||||
fn restore_terminal() -> color_eyre::Result<()> {
|
||||
fn restore_terminal() -> Result<()> {
|
||||
disable_raw_mode()?;
|
||||
stdout().execute(LeaveAlternateScreen)?;
|
||||
Ok(())
|
||||
|
||||
646
examples/constraint-explorer.rs
Normal file
646
examples/constraint-explorer.rs
Normal file
@@ -0,0 +1,646 @@
|
||||
//! # [Ratatui] Constraint explorer example
|
||||
//!
|
||||
//! The latest version of this example is available in the [examples] folder in the repository.
|
||||
//!
|
||||
//! Please note that the examples are designed to be run against the `main` branch of the Github
|
||||
//! repository. This means that you may not be able to compile with the latest release version on
|
||||
//! crates.io, or the one that you have installed locally.
|
||||
//!
|
||||
//! See the [examples readme] for more information on finding examples that match the version of the
|
||||
//! library you are using.
|
||||
//!
|
||||
//! [Ratatui]: https://github.com/ratatui-org/ratatui
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
#![allow(clippy::enum_glob_use, clippy::wildcard_imports)]
|
||||
|
||||
use std::io::{self, stdout};
|
||||
|
||||
use color_eyre::{config::HookBuilder, Result};
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode, KeyEventKind},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use ratatui::{
|
||||
layout::{Constraint::*, Flex},
|
||||
prelude::*,
|
||||
style::palette::tailwind::*,
|
||||
symbols::line,
|
||||
widgets::{Block, Paragraph, Wrap},
|
||||
};
|
||||
use strum::{Display, EnumIter, FromRepr};
|
||||
|
||||
#[derive(Default)]
|
||||
struct App {
|
||||
mode: AppMode,
|
||||
spacing: u16,
|
||||
constraints: Vec<Constraint>,
|
||||
selected_index: usize,
|
||||
value: u16,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq)]
|
||||
enum AppMode {
|
||||
#[default]
|
||||
Running,
|
||||
Quit,
|
||||
}
|
||||
|
||||
/// A variant of [`Constraint`] that can be rendered as a tab.
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, EnumIter, FromRepr, Display)]
|
||||
enum ConstraintName {
|
||||
#[default]
|
||||
Length,
|
||||
Percentage,
|
||||
Ratio,
|
||||
Min,
|
||||
Max,
|
||||
Fill,
|
||||
}
|
||||
|
||||
/// A widget that renders a [`Constraint`] as a block. E.g.:
|
||||
/// ```plain
|
||||
/// ┌──────────────┐
|
||||
/// │ Length(16) │
|
||||
/// │ 16px │
|
||||
/// └──────────────┘
|
||||
/// ```
|
||||
struct ConstraintBlock {
|
||||
constraint: Constraint,
|
||||
legend: bool,
|
||||
selected: bool,
|
||||
}
|
||||
|
||||
/// A widget that renders a spacer with a label indicating the width of the spacer. E.g.:
|
||||
///
|
||||
/// ```plain
|
||||
/// ┌ ┐
|
||||
/// 8 px
|
||||
/// └ ┘
|
||||
/// ```
|
||||
struct SpacerBlock;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
init_error_hooks()?;
|
||||
let terminal = init_terminal()?;
|
||||
App::default().run(terminal)?;
|
||||
restore_terminal()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// App behaviour
|
||||
impl App {
|
||||
fn run(&mut self, mut terminal: Terminal<impl Backend>) -> Result<()> {
|
||||
self.insert_test_defaults();
|
||||
|
||||
while self.is_running() {
|
||||
self.draw(&mut terminal)?;
|
||||
self.handle_events()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// TODO remove these - these are just for testing
|
||||
fn insert_test_defaults(&mut self) {
|
||||
self.constraints = vec![
|
||||
Constraint::Length(20),
|
||||
Constraint::Length(20),
|
||||
Constraint::Length(20),
|
||||
];
|
||||
self.value = 20;
|
||||
}
|
||||
|
||||
fn is_running(&self) -> bool {
|
||||
self.mode == AppMode::Running
|
||||
}
|
||||
|
||||
fn draw(&self, terminal: &mut Terminal<impl Backend>) -> io::Result<()> {
|
||||
terminal.draw(|frame| frame.render_widget(self, frame.size()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_events(&mut self) -> Result<()> {
|
||||
use KeyCode::*;
|
||||
match event::read()? {
|
||||
Event::Key(key) if key.kind == KeyEventKind::Press => match key.code {
|
||||
Char('q') | Esc => self.exit(),
|
||||
Char('1') => self.swap_constraint(ConstraintName::Min),
|
||||
Char('2') => self.swap_constraint(ConstraintName::Max),
|
||||
Char('3') => self.swap_constraint(ConstraintName::Length),
|
||||
Char('4') => self.swap_constraint(ConstraintName::Percentage),
|
||||
Char('5') => self.swap_constraint(ConstraintName::Ratio),
|
||||
Char('6') => self.swap_constraint(ConstraintName::Fill),
|
||||
Char('+') => self.increment_spacing(),
|
||||
Char('-') => self.decrement_spacing(),
|
||||
Char('x') => self.delete_block(),
|
||||
Char('a') => self.insert_block(),
|
||||
Char('k') | Up => self.increment_value(),
|
||||
Char('j') | Down => self.decrement_value(),
|
||||
Char('h') | Left => self.prev_block(),
|
||||
Char('l') | Right => self.next_block(),
|
||||
_ => {}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn increment_value(&mut self) {
|
||||
let Some(constraint) = self.constraints.get_mut(self.selected_index) else {
|
||||
return;
|
||||
};
|
||||
match constraint {
|
||||
Constraint::Length(v)
|
||||
| Constraint::Min(v)
|
||||
| Constraint::Max(v)
|
||||
| Constraint::Fill(v)
|
||||
| Constraint::Percentage(v) => *v = v.saturating_add(1),
|
||||
Constraint::Ratio(_n, d) => *d = d.saturating_add(1),
|
||||
};
|
||||
}
|
||||
|
||||
fn decrement_value(&mut self) {
|
||||
let Some(constraint) = self.constraints.get_mut(self.selected_index) else {
|
||||
return;
|
||||
};
|
||||
match constraint {
|
||||
Constraint::Length(v)
|
||||
| Constraint::Min(v)
|
||||
| Constraint::Max(v)
|
||||
| Constraint::Fill(v)
|
||||
| Constraint::Percentage(v) => *v = v.saturating_sub(1),
|
||||
Constraint::Ratio(_n, d) => *d = d.saturating_sub(1),
|
||||
};
|
||||
}
|
||||
|
||||
/// select the next block with wrap around
|
||||
fn next_block(&mut self) {
|
||||
if self.constraints.is_empty() {
|
||||
return;
|
||||
}
|
||||
let len = self.constraints.len();
|
||||
self.selected_index = (self.selected_index + 1) % len;
|
||||
}
|
||||
|
||||
/// select the previous block with wrap around
|
||||
fn prev_block(&mut self) {
|
||||
if self.constraints.is_empty() {
|
||||
return;
|
||||
}
|
||||
let len = self.constraints.len();
|
||||
self.selected_index = (self.selected_index + self.constraints.len() - 1) % len;
|
||||
}
|
||||
|
||||
/// delete the selected block
|
||||
fn delete_block(&mut self) {
|
||||
if self.constraints.is_empty() {
|
||||
return;
|
||||
}
|
||||
self.constraints.remove(self.selected_index);
|
||||
self.selected_index = self.selected_index.saturating_sub(1);
|
||||
}
|
||||
|
||||
/// insert a block after the selected block
|
||||
fn insert_block(&mut self) {
|
||||
let index = self
|
||||
.selected_index
|
||||
.saturating_add(1)
|
||||
.min(self.constraints.len());
|
||||
let constraint = Constraint::Length(self.value);
|
||||
self.constraints.insert(index, constraint);
|
||||
self.selected_index = index;
|
||||
}
|
||||
|
||||
fn increment_spacing(&mut self) {
|
||||
self.spacing = self.spacing.saturating_add(1);
|
||||
}
|
||||
|
||||
fn decrement_spacing(&mut self) {
|
||||
self.spacing = self.spacing.saturating_sub(1);
|
||||
}
|
||||
|
||||
fn exit(&mut self) {
|
||||
self.mode = AppMode::Quit;
|
||||
}
|
||||
|
||||
fn swap_constraint(&mut self, name: ConstraintName) {
|
||||
if self.constraints.is_empty() {
|
||||
return;
|
||||
}
|
||||
let constraint = match name {
|
||||
ConstraintName::Length => Length(self.value),
|
||||
ConstraintName::Percentage => Percentage(self.value),
|
||||
ConstraintName::Min => Min(self.value),
|
||||
ConstraintName::Max => Max(self.value),
|
||||
ConstraintName::Fill => Fill(self.value),
|
||||
ConstraintName::Ratio => Ratio(1, u32::from(self.value) / 4), // for balance
|
||||
};
|
||||
self.constraints[self.selected_index] = constraint;
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Constraint> for ConstraintName {
|
||||
fn from(constraint: Constraint) -> Self {
|
||||
match constraint {
|
||||
Length(_) => Self::Length,
|
||||
Percentage(_) => Self::Percentage,
|
||||
Ratio(_, _) => Self::Ratio,
|
||||
Min(_) => Self::Min,
|
||||
Max(_) => Self::Max,
|
||||
Fill(_) => Self::Fill,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for &App {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let [header_area, instructions_area, swap_legend_area, _, blocks_area] =
|
||||
Layout::vertical([
|
||||
Length(2), // header
|
||||
Length(2), // instructions
|
||||
Length(1), // swap key legend
|
||||
Length(1), // gap
|
||||
Fill(1), // blocks
|
||||
])
|
||||
.areas(area);
|
||||
|
||||
App::header().render(header_area, buf);
|
||||
App::instructions().render(instructions_area, buf);
|
||||
App::swap_legend().render(swap_legend_area, buf);
|
||||
self.render_layout_blocks(blocks_area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
// App rendering
|
||||
impl App {
|
||||
const HEADER_COLOR: Color = SLATE.c200;
|
||||
const TEXT_COLOR: Color = SLATE.c400;
|
||||
const AXIS_COLOR: Color = SLATE.c500;
|
||||
|
||||
fn header() -> impl Widget {
|
||||
let text = "Constraint Explorer";
|
||||
text.bold().fg(Self::HEADER_COLOR).into_centered_line()
|
||||
}
|
||||
|
||||
fn instructions() -> impl Widget {
|
||||
let text = "◄ ►: select, ▲ ▼: edit, 1-6: swap, a: add, x: delete, q: quit, + -: spacing";
|
||||
Paragraph::new(text)
|
||||
.fg(Self::TEXT_COLOR)
|
||||
.centered()
|
||||
.wrap(Wrap { trim: false })
|
||||
}
|
||||
|
||||
fn swap_legend() -> impl Widget {
|
||||
#[allow(unstable_name_collisions)]
|
||||
Paragraph::new(
|
||||
Line::from(
|
||||
[
|
||||
ConstraintName::Min,
|
||||
ConstraintName::Max,
|
||||
ConstraintName::Length,
|
||||
ConstraintName::Percentage,
|
||||
ConstraintName::Ratio,
|
||||
ConstraintName::Fill,
|
||||
]
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, name)| {
|
||||
format!(" {i}: {name} ", i = i + 1)
|
||||
.fg(SLATE.c200)
|
||||
.bg(name.color())
|
||||
})
|
||||
.intersperse(Span::from(" "))
|
||||
.collect_vec(),
|
||||
)
|
||||
.centered(),
|
||||
)
|
||||
.wrap(Wrap { trim: false })
|
||||
}
|
||||
|
||||
/// A bar like `<----- 80 px (gap: 2 px) ----->`
|
||||
///
|
||||
/// Only shows the gap when spacing is not zero
|
||||
fn axis(&self, width: u16) -> impl Widget {
|
||||
let label = if self.spacing != 0 {
|
||||
format!("{} px (gap: {} px)", width, self.spacing)
|
||||
} else {
|
||||
format!("{width} px")
|
||||
};
|
||||
let bar_width = width.saturating_sub(2) as usize; // we want to `<` and `>` at the ends
|
||||
let width_bar = format!("<{label:-^bar_width$}>");
|
||||
Paragraph::new(width_bar).fg(Self::AXIS_COLOR).centered()
|
||||
}
|
||||
|
||||
fn render_layout_blocks(&self, area: Rect, buf: &mut Buffer) {
|
||||
let [user_constraints, area] = Layout::vertical([Length(3), Fill(1)])
|
||||
.spacing(1)
|
||||
.areas(area);
|
||||
|
||||
self.render_user_constraints_legend(user_constraints, buf);
|
||||
|
||||
let [start, center, end, space_around, space_between] =
|
||||
Layout::vertical([Length(7); 5]).areas(area);
|
||||
|
||||
self.render_layout_block(Flex::Start, start, buf);
|
||||
self.render_layout_block(Flex::Center, center, buf);
|
||||
self.render_layout_block(Flex::End, end, buf);
|
||||
self.render_layout_block(Flex::SpaceAround, space_around, buf);
|
||||
self.render_layout_block(Flex::SpaceBetween, space_between, buf);
|
||||
}
|
||||
|
||||
fn render_user_constraints_legend(&self, area: Rect, buf: &mut Buffer) {
|
||||
let blocks = Layout::horizontal(
|
||||
self.constraints
|
||||
.iter()
|
||||
.map(|_| Constraint::Fill(1))
|
||||
.collect_vec(),
|
||||
)
|
||||
.split(area);
|
||||
|
||||
for (i, (area, constraint)) in blocks.iter().zip(self.constraints.iter()).enumerate() {
|
||||
let selected = self.selected_index == i;
|
||||
ConstraintBlock::new(*constraint, selected, true).render(*area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_layout_block(&self, flex: Flex, area: Rect, buf: &mut Buffer) {
|
||||
let [label_area, axis_area, blocks_area] =
|
||||
Layout::vertical([Length(1), Max(1), Length(4)]).areas(area);
|
||||
|
||||
if label_area.height > 0 {
|
||||
format!("Flex::{flex:?}").bold().render(label_area, buf);
|
||||
}
|
||||
|
||||
self.axis(area.width).render(axis_area, buf);
|
||||
|
||||
let (blocks, spacers) = Layout::horizontal(&self.constraints)
|
||||
.flex(flex)
|
||||
.spacing(self.spacing)
|
||||
.split_with_spacers(blocks_area);
|
||||
|
||||
for (i, (area, constraint)) in blocks.iter().zip(self.constraints.iter()).enumerate() {
|
||||
let selected = self.selected_index == i;
|
||||
ConstraintBlock::new(*constraint, selected, false).render(*area, buf);
|
||||
}
|
||||
|
||||
for area in spacers.iter() {
|
||||
SpacerBlock.render(*area, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for ConstraintBlock {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
match area.height {
|
||||
1 => self.render_1px(area, buf),
|
||||
2 => self.render_2px(area, buf),
|
||||
_ => self.render_4px(area, buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ConstraintBlock {
|
||||
const TEXT_COLOR: Color = SLATE.c200;
|
||||
|
||||
const fn new(constraint: Constraint, selected: bool, legend: bool) -> Self {
|
||||
Self {
|
||||
constraint,
|
||||
legend,
|
||||
selected,
|
||||
}
|
||||
}
|
||||
|
||||
fn label(&self, width: u16) -> String {
|
||||
let long_width = format!("{width} px");
|
||||
let short_width = format!("{width}");
|
||||
// border takes up 2 columns
|
||||
let available_space = width.saturating_sub(2) as usize;
|
||||
let width_label = if long_width.len() < available_space {
|
||||
long_width
|
||||
} else if short_width.len() < available_space {
|
||||
short_width
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
format!("{}\n{}", self.constraint, width_label)
|
||||
}
|
||||
|
||||
fn render_1px(&self, area: Rect, buf: &mut Buffer) {
|
||||
let lighter_color = ConstraintName::from(self.constraint).lighter_color();
|
||||
let main_color = ConstraintName::from(self.constraint).color();
|
||||
let selected_color = if self.selected {
|
||||
lighter_color
|
||||
} else {
|
||||
main_color
|
||||
};
|
||||
Block::default()
|
||||
.fg(Self::TEXT_COLOR)
|
||||
.bg(selected_color)
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
fn render_2px(&self, area: Rect, buf: &mut Buffer) {
|
||||
let lighter_color = ConstraintName::from(self.constraint).lighter_color();
|
||||
let main_color = ConstraintName::from(self.constraint).color();
|
||||
let selected_color = if self.selected {
|
||||
lighter_color
|
||||
} else {
|
||||
main_color
|
||||
};
|
||||
Block::bordered()
|
||||
.border_set(symbols::border::QUADRANT_OUTSIDE)
|
||||
.border_style(Style::reset().fg(selected_color).reversed())
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
fn render_4px(&self, area: Rect, buf: &mut Buffer) {
|
||||
let lighter_color = ConstraintName::from(self.constraint).lighter_color();
|
||||
let main_color = ConstraintName::from(self.constraint).color();
|
||||
let selected_color = if self.selected {
|
||||
lighter_color
|
||||
} else {
|
||||
main_color
|
||||
};
|
||||
let color = if self.legend {
|
||||
selected_color
|
||||
} else {
|
||||
main_color
|
||||
};
|
||||
let label = self.label(area.width);
|
||||
let block = Block::bordered()
|
||||
.border_set(symbols::border::QUADRANT_OUTSIDE)
|
||||
.border_style(Style::reset().fg(color).reversed())
|
||||
.fg(Self::TEXT_COLOR)
|
||||
.bg(color);
|
||||
Paragraph::new(label)
|
||||
.centered()
|
||||
.fg(Self::TEXT_COLOR)
|
||||
.bg(color)
|
||||
.block(block)
|
||||
.render(area, buf);
|
||||
|
||||
if !self.legend {
|
||||
let border_color = if self.selected {
|
||||
lighter_color
|
||||
} else {
|
||||
main_color
|
||||
};
|
||||
if let Some(last_row) = area.rows().last() {
|
||||
buf.set_style(last_row, border_color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for SpacerBlock {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
match area.height {
|
||||
1 => (),
|
||||
2 => Self::render_2px(area, buf),
|
||||
3 => Self::render_3px(area, buf),
|
||||
_ => Self::render_4px(area, buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SpacerBlock {
|
||||
const TEXT_COLOR: Color = SLATE.c500;
|
||||
const BORDER_COLOR: Color = SLATE.c600;
|
||||
|
||||
/// A block with a corner borders
|
||||
fn block() -> impl Widget {
|
||||
let corners_only = symbols::border::Set {
|
||||
top_left: line::NORMAL.top_left,
|
||||
top_right: line::NORMAL.top_right,
|
||||
bottom_left: line::NORMAL.bottom_left,
|
||||
bottom_right: line::NORMAL.bottom_right,
|
||||
vertical_left: " ",
|
||||
vertical_right: " ",
|
||||
horizontal_top: " ",
|
||||
horizontal_bottom: " ",
|
||||
};
|
||||
Block::bordered()
|
||||
.border_set(corners_only)
|
||||
.border_style(Self::BORDER_COLOR)
|
||||
}
|
||||
|
||||
/// A vertical line used if there is not enough space to render the block
|
||||
fn line() -> impl Widget {
|
||||
Paragraph::new(Text::from(vec![
|
||||
Line::from(""),
|
||||
Line::from("│"),
|
||||
Line::from("│"),
|
||||
Line::from(""),
|
||||
]))
|
||||
.style(Self::BORDER_COLOR)
|
||||
}
|
||||
|
||||
/// A label that says "Spacer" if there is enough space
|
||||
fn spacer_label(width: u16) -> impl Widget {
|
||||
let label = if width >= 6 { "Spacer" } else { "" };
|
||||
label.fg(Self::TEXT_COLOR).into_centered_line()
|
||||
}
|
||||
|
||||
/// A label that says "8 px" if there is enough space
|
||||
fn label(width: u16) -> impl Widget {
|
||||
let long_label = format!("{width} px");
|
||||
let short_label = format!("{width}");
|
||||
let label = if long_label.len() < width as usize {
|
||||
long_label
|
||||
} else if short_label.len() < width as usize {
|
||||
short_label
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
Line::styled(label, Self::TEXT_COLOR).centered()
|
||||
}
|
||||
|
||||
fn render_2px(area: Rect, buf: &mut Buffer) {
|
||||
if area.width > 1 {
|
||||
Self::block().render(area, buf);
|
||||
} else {
|
||||
Self::line().render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_3px(area: Rect, buf: &mut Buffer) {
|
||||
if area.width > 1 {
|
||||
Self::block().render(area, buf);
|
||||
} else {
|
||||
Self::line().render(area, buf);
|
||||
}
|
||||
|
||||
let row = area.rows().nth(1).unwrap_or_default();
|
||||
Self::spacer_label(area.width).render(row, buf);
|
||||
}
|
||||
|
||||
fn render_4px(area: Rect, buf: &mut Buffer) {
|
||||
if area.width > 1 {
|
||||
Self::block().render(area, buf);
|
||||
} else {
|
||||
Self::line().render(area, buf);
|
||||
}
|
||||
|
||||
let row = area.rows().nth(1).unwrap_or_default();
|
||||
Self::spacer_label(area.width).render(row, buf);
|
||||
|
||||
let row = area.rows().nth(2).unwrap_or_default();
|
||||
Self::label(area.width).render(row, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl ConstraintName {
|
||||
const fn color(self) -> Color {
|
||||
match self {
|
||||
Self::Length => SLATE.c700,
|
||||
Self::Percentage => SLATE.c800,
|
||||
Self::Ratio => SLATE.c900,
|
||||
Self::Fill => SLATE.c950,
|
||||
Self::Min => BLUE.c800,
|
||||
Self::Max => BLUE.c900,
|
||||
}
|
||||
}
|
||||
|
||||
const fn lighter_color(self) -> Color {
|
||||
match self {
|
||||
Self::Length => STONE.c500,
|
||||
Self::Percentage => STONE.c600,
|
||||
Self::Ratio => STONE.c700,
|
||||
Self::Fill => STONE.c800,
|
||||
Self::Min => SKY.c600,
|
||||
Self::Max => SKY.c700,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn init_error_hooks() -> Result<()> {
|
||||
let (panic, error) = HookBuilder::default().into_hooks();
|
||||
let panic = panic.into_panic_hook();
|
||||
let error = error.into_eyre_hook();
|
||||
color_eyre::eyre::set_hook(Box::new(move |e| {
|
||||
let _ = restore_terminal();
|
||||
error(e)
|
||||
}))?;
|
||||
std::panic::set_hook(Box::new(move |info| {
|
||||
let _ = restore_terminal();
|
||||
panic(info);
|
||||
}));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn init_terminal() -> Result<Terminal<impl Backend>> {
|
||||
enable_raw_mode()?;
|
||||
stdout().execute(EnterAlternateScreen)?;
|
||||
let backend = CrosstermBackend::new(stdout());
|
||||
let terminal = Terminal::new(backend)?;
|
||||
Ok(terminal)
|
||||
}
|
||||
|
||||
fn restore_terminal() -> Result<()> {
|
||||
disable_raw_mode()?;
|
||||
stdout().execute(LeaveAlternateScreen)?;
|
||||
Ok(())
|
||||
}
|
||||
437
examples/constraints.rs
Normal file
437
examples/constraints.rs
Normal file
@@ -0,0 +1,437 @@
|
||||
//! # [Ratatui] Constraints example
|
||||
//!
|
||||
//! The latest version of this example is available in the [examples] folder in the repository.
|
||||
//!
|
||||
//! Please note that the examples are designed to be run against the `main` branch of the Github
|
||||
//! repository. This means that you may not be able to compile with the latest release version on
|
||||
//! crates.io, or the one that you have installed locally.
|
||||
//!
|
||||
//! See the [examples readme] for more information on finding examples that match the version of the
|
||||
//! library you are using.
|
||||
//!
|
||||
//! [Ratatui]: https://github.com/ratatui-org/ratatui
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
#![allow(clippy::enum_glob_use, clippy::wildcard_imports)]
|
||||
|
||||
use std::io::{self, stdout};
|
||||
|
||||
use color_eyre::{config::HookBuilder, Result};
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
};
|
||||
use ratatui::{layout::Constraint::*, prelude::*, style::palette::tailwind, widgets::*};
|
||||
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
|
||||
|
||||
const SPACER_HEIGHT: u16 = 0;
|
||||
const ILLUSTRATION_HEIGHT: u16 = 4;
|
||||
const EXAMPLE_HEIGHT: u16 = ILLUSTRATION_HEIGHT + SPACER_HEIGHT;
|
||||
|
||||
// priority 2
|
||||
const MIN_COLOR: Color = tailwind::BLUE.c900;
|
||||
const MAX_COLOR: Color = tailwind::BLUE.c800;
|
||||
// priority 3
|
||||
const LENGTH_COLOR: Color = tailwind::SLATE.c700;
|
||||
const PERCENTAGE_COLOR: Color = tailwind::SLATE.c800;
|
||||
const RATIO_COLOR: Color = tailwind::SLATE.c900;
|
||||
// priority 4
|
||||
const FILL_COLOR: Color = tailwind::SLATE.c950;
|
||||
|
||||
#[derive(Default, Clone, Copy)]
|
||||
struct App {
|
||||
selected_tab: SelectedTab,
|
||||
scroll_offset: u16,
|
||||
max_scroll_offset: u16,
|
||||
state: AppState,
|
||||
}
|
||||
|
||||
/// Tabs for the different examples
|
||||
///
|
||||
/// The order of the variants is the order in which they are displayed.
|
||||
#[derive(Default, Debug, Copy, Clone, Display, FromRepr, EnumIter, PartialEq, Eq)]
|
||||
enum SelectedTab {
|
||||
#[default]
|
||||
Min,
|
||||
Max,
|
||||
Length,
|
||||
Percentage,
|
||||
Ratio,
|
||||
Fill,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
enum AppState {
|
||||
#[default]
|
||||
Running,
|
||||
Quit,
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
init_error_hooks()?;
|
||||
let terminal = init_terminal()?;
|
||||
|
||||
// increase the cache size to avoid flickering for indeterminate layouts
|
||||
Layout::init_cache(100);
|
||||
|
||||
App::default().run(terminal)?;
|
||||
|
||||
restore_terminal()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn run(&mut self, mut terminal: Terminal<impl Backend>) -> Result<()> {
|
||||
self.update_max_scroll_offset();
|
||||
while self.is_running() {
|
||||
self.draw(&mut terminal)?;
|
||||
self.handle_events()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_max_scroll_offset(&mut self) {
|
||||
self.max_scroll_offset = (self.selected_tab.get_example_count() - 1) * EXAMPLE_HEIGHT;
|
||||
}
|
||||
|
||||
fn is_running(self) -> bool {
|
||||
self.state == AppState::Running
|
||||
}
|
||||
|
||||
fn draw(self, terminal: &mut Terminal<impl Backend>) -> io::Result<()> {
|
||||
terminal.draw(|frame| frame.render_widget(self, frame.size()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_events(&mut self) -> Result<()> {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
use KeyCode::*;
|
||||
match key.code {
|
||||
Char('q') | Esc => self.quit(),
|
||||
Char('l') | Right => self.next(),
|
||||
Char('h') | Left => self.previous(),
|
||||
Char('j') | Down => self.down(),
|
||||
Char('k') | Up => self.up(),
|
||||
Char('g') | Home => self.top(),
|
||||
Char('G') | End => self.bottom(),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn quit(&mut self) {
|
||||
self.state = AppState::Quit;
|
||||
}
|
||||
|
||||
fn next(&mut self) {
|
||||
self.selected_tab = self.selected_tab.next();
|
||||
self.update_max_scroll_offset();
|
||||
self.scroll_offset = 0;
|
||||
}
|
||||
|
||||
fn previous(&mut self) {
|
||||
self.selected_tab = self.selected_tab.previous();
|
||||
self.update_max_scroll_offset();
|
||||
self.scroll_offset = 0;
|
||||
}
|
||||
|
||||
fn up(&mut self) {
|
||||
self.scroll_offset = self.scroll_offset.saturating_sub(1);
|
||||
}
|
||||
|
||||
fn down(&mut self) {
|
||||
self.scroll_offset = self
|
||||
.scroll_offset
|
||||
.saturating_add(1)
|
||||
.min(self.max_scroll_offset);
|
||||
}
|
||||
|
||||
fn top(&mut self) {
|
||||
self.scroll_offset = 0;
|
||||
}
|
||||
|
||||
fn bottom(&mut self) {
|
||||
self.scroll_offset = self.max_scroll_offset;
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for App {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let [tabs, axis, demo] = Layout::vertical([Length(3), Length(3), Fill(0)]).areas(area);
|
||||
|
||||
self.render_tabs(tabs, buf);
|
||||
Self::render_axis(axis, buf);
|
||||
self.render_demo(demo, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn render_tabs(self, area: Rect, buf: &mut Buffer) {
|
||||
let titles = SelectedTab::iter().map(SelectedTab::to_tab_title);
|
||||
let block = Block::new()
|
||||
.title("Constraints ".bold())
|
||||
.title(" Use h l or ◄ ► to change tab and j k or ▲ ▼ to scroll");
|
||||
Tabs::new(titles)
|
||||
.block(block)
|
||||
.highlight_style(Modifier::REVERSED)
|
||||
.select(self.selected_tab as usize)
|
||||
.padding("", "")
|
||||
.divider(" ")
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
fn render_axis(area: Rect, buf: &mut Buffer) {
|
||||
let width = area.width as usize;
|
||||
// a bar like `<----- 80 px ----->`
|
||||
let width_label = format!("{width} px");
|
||||
let width_bar = format!(
|
||||
"<{width_label:-^width$}>",
|
||||
width = width - width_label.len() / 2
|
||||
);
|
||||
Paragraph::new(width_bar.dark_gray())
|
||||
.centered()
|
||||
.block(Block::default().padding(Padding {
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 1,
|
||||
bottom: 0,
|
||||
}))
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
/// Render the demo content
|
||||
///
|
||||
/// This function renders the demo content into a separate buffer and then splices the buffer
|
||||
/// into the main buffer. This is done to make it possible to handle scrolling easily.
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
fn render_demo(self, area: Rect, buf: &mut Buffer) {
|
||||
// render demo content into a separate buffer so all examples fit we add an extra
|
||||
// area.height to make sure the last example is fully visible even when the scroll offset is
|
||||
// at the max
|
||||
let height = self.selected_tab.get_example_count() * EXAMPLE_HEIGHT;
|
||||
let demo_area = Rect::new(0, 0, area.width, height + area.height);
|
||||
let mut demo_buf = Buffer::empty(demo_area);
|
||||
|
||||
let scrollbar_needed = self.scroll_offset != 0 || height > area.height;
|
||||
let content_area = if scrollbar_needed {
|
||||
Rect {
|
||||
width: demo_area.width - 1,
|
||||
..demo_area
|
||||
}
|
||||
} else {
|
||||
demo_area
|
||||
};
|
||||
self.selected_tab.render(content_area, &mut demo_buf);
|
||||
|
||||
let visible_content = demo_buf
|
||||
.content
|
||||
.into_iter()
|
||||
.skip((demo_area.width * self.scroll_offset) as usize)
|
||||
.take(area.area() as usize);
|
||||
for (i, cell) in visible_content.enumerate() {
|
||||
let x = i as u16 % area.width;
|
||||
let y = i as u16 / area.width;
|
||||
*buf.get_mut(area.x + x, area.y + y) = cell;
|
||||
}
|
||||
|
||||
if scrollbar_needed {
|
||||
let mut state = ScrollbarState::new(self.max_scroll_offset as usize)
|
||||
.position(self.scroll_offset as usize);
|
||||
Scrollbar::new(ScrollbarOrientation::VerticalRight).render(area, buf, &mut state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SelectedTab {
|
||||
/// Get the previous tab, if there is no previous tab return the current tab.
|
||||
fn previous(self) -> Self {
|
||||
let current_index: usize = self as usize;
|
||||
let previous_index = current_index.saturating_sub(1);
|
||||
Self::from_repr(previous_index).unwrap_or(self)
|
||||
}
|
||||
|
||||
/// Get the next tab, if there is no next tab return the current tab.
|
||||
fn next(self) -> Self {
|
||||
let current_index = self as usize;
|
||||
let next_index = current_index.saturating_add(1);
|
||||
Self::from_repr(next_index).unwrap_or(self)
|
||||
}
|
||||
|
||||
const fn get_example_count(self) -> u16 {
|
||||
#[allow(clippy::match_same_arms)]
|
||||
match self {
|
||||
Self::Length => 4,
|
||||
Self::Percentage => 5,
|
||||
Self::Ratio => 4,
|
||||
Self::Fill => 2,
|
||||
Self::Min => 5,
|
||||
Self::Max => 5,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_tab_title(value: Self) -> Line<'static> {
|
||||
let text = format!(" {value} ");
|
||||
let color = match value {
|
||||
Self::Length => LENGTH_COLOR,
|
||||
Self::Percentage => PERCENTAGE_COLOR,
|
||||
Self::Ratio => RATIO_COLOR,
|
||||
Self::Fill => FILL_COLOR,
|
||||
Self::Min => MIN_COLOR,
|
||||
Self::Max => MAX_COLOR,
|
||||
};
|
||||
text.fg(tailwind::SLATE.c200).bg(color).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for SelectedTab {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
match self {
|
||||
Self::Length => Self::render_length_example(area, buf),
|
||||
Self::Percentage => Self::render_percentage_example(area, buf),
|
||||
Self::Ratio => Self::render_ratio_example(area, buf),
|
||||
Self::Fill => Self::render_fill_example(area, buf),
|
||||
Self::Min => Self::render_min_example(area, buf),
|
||||
Self::Max => Self::render_max_example(area, buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SelectedTab {
|
||||
fn render_length_example(area: Rect, buf: &mut Buffer) {
|
||||
let [example1, example2, example3, _] =
|
||||
Layout::vertical([Length(EXAMPLE_HEIGHT); 4]).areas(area);
|
||||
|
||||
Example::new(&[Length(20), Length(20)]).render(example1, buf);
|
||||
Example::new(&[Length(20), Min(20)]).render(example2, buf);
|
||||
Example::new(&[Length(20), Max(20)]).render(example3, buf);
|
||||
}
|
||||
|
||||
fn render_percentage_example(area: Rect, buf: &mut Buffer) {
|
||||
let [example1, example2, example3, example4, example5, _] =
|
||||
Layout::vertical([Length(EXAMPLE_HEIGHT); 6]).areas(area);
|
||||
|
||||
Example::new(&[Percentage(75), Fill(0)]).render(example1, buf);
|
||||
Example::new(&[Percentage(25), Fill(0)]).render(example2, buf);
|
||||
Example::new(&[Percentage(50), Min(20)]).render(example3, buf);
|
||||
Example::new(&[Percentage(0), Max(0)]).render(example4, buf);
|
||||
Example::new(&[Percentage(0), Fill(0)]).render(example5, buf);
|
||||
}
|
||||
|
||||
fn render_ratio_example(area: Rect, buf: &mut Buffer) {
|
||||
let [example1, example2, example3, example4, _] =
|
||||
Layout::vertical([Length(EXAMPLE_HEIGHT); 5]).areas(area);
|
||||
|
||||
Example::new(&[Ratio(1, 2); 2]).render(example1, buf);
|
||||
Example::new(&[Ratio(1, 4); 4]).render(example2, buf);
|
||||
Example::new(&[Ratio(1, 2), Ratio(1, 3), Ratio(1, 4)]).render(example3, buf);
|
||||
Example::new(&[Ratio(1, 2), Percentage(25), Length(10)]).render(example4, buf);
|
||||
}
|
||||
|
||||
fn render_fill_example(area: Rect, buf: &mut Buffer) {
|
||||
let [example1, example2, _] = Layout::vertical([Length(EXAMPLE_HEIGHT); 3]).areas(area);
|
||||
|
||||
Example::new(&[Fill(1), Fill(2), Fill(3)]).render(example1, buf);
|
||||
Example::new(&[Fill(1), Percentage(50), Fill(1)]).render(example2, buf);
|
||||
}
|
||||
|
||||
fn render_min_example(area: Rect, buf: &mut Buffer) {
|
||||
let [example1, example2, example3, example4, example5, _] =
|
||||
Layout::vertical([Length(EXAMPLE_HEIGHT); 6]).areas(area);
|
||||
|
||||
Example::new(&[Percentage(100), Min(0)]).render(example1, buf);
|
||||
Example::new(&[Percentage(100), Min(20)]).render(example2, buf);
|
||||
Example::new(&[Percentage(100), Min(40)]).render(example3, buf);
|
||||
Example::new(&[Percentage(100), Min(60)]).render(example4, buf);
|
||||
Example::new(&[Percentage(100), Min(80)]).render(example5, buf);
|
||||
}
|
||||
|
||||
fn render_max_example(area: Rect, buf: &mut Buffer) {
|
||||
let [example1, example2, example3, example4, example5, _] =
|
||||
Layout::vertical([Length(EXAMPLE_HEIGHT); 6]).areas(area);
|
||||
|
||||
Example::new(&[Percentage(0), Max(0)]).render(example1, buf);
|
||||
Example::new(&[Percentage(0), Max(20)]).render(example2, buf);
|
||||
Example::new(&[Percentage(0), Max(40)]).render(example3, buf);
|
||||
Example::new(&[Percentage(0), Max(60)]).render(example4, buf);
|
||||
Example::new(&[Percentage(0), Max(80)]).render(example5, buf);
|
||||
}
|
||||
}
|
||||
|
||||
struct Example {
|
||||
constraints: Vec<Constraint>,
|
||||
}
|
||||
|
||||
impl Example {
|
||||
fn new(constraints: &[Constraint]) -> Self {
|
||||
Self {
|
||||
constraints: constraints.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Example {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let [area, _] =
|
||||
Layout::vertical([Length(ILLUSTRATION_HEIGHT), Length(SPACER_HEIGHT)]).areas(area);
|
||||
let blocks = Layout::horizontal(&self.constraints).split(area);
|
||||
|
||||
for (block, constraint) in blocks.iter().zip(&self.constraints) {
|
||||
Self::illustration(*constraint, block.width).render(*block, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Example {
|
||||
fn illustration(constraint: Constraint, width: u16) -> impl Widget {
|
||||
let color = match constraint {
|
||||
Constraint::Length(_) => LENGTH_COLOR,
|
||||
Constraint::Percentage(_) => PERCENTAGE_COLOR,
|
||||
Constraint::Ratio(_, _) => RATIO_COLOR,
|
||||
Constraint::Fill(_) => FILL_COLOR,
|
||||
Constraint::Min(_) => MIN_COLOR,
|
||||
Constraint::Max(_) => MAX_COLOR,
|
||||
};
|
||||
let fg = Color::White;
|
||||
let title = format!("{constraint}");
|
||||
let content = format!("{width} px");
|
||||
let text = format!("{title}\n{content}");
|
||||
let block = Block::bordered()
|
||||
.border_set(symbols::border::QUADRANT_OUTSIDE)
|
||||
.border_style(Style::reset().fg(color).reversed())
|
||||
.style(Style::default().fg(fg).bg(color));
|
||||
Paragraph::new(text).centered().block(block)
|
||||
}
|
||||
}
|
||||
|
||||
fn init_error_hooks() -> Result<()> {
|
||||
let (panic, error) = HookBuilder::default().into_hooks();
|
||||
let panic = panic.into_panic_hook();
|
||||
let error = error.into_eyre_hook();
|
||||
color_eyre::eyre::set_hook(Box::new(move |e| {
|
||||
let _ = restore_terminal();
|
||||
error(e)
|
||||
}))?;
|
||||
std::panic::set_hook(Box::new(move |info| {
|
||||
let _ = restore_terminal();
|
||||
panic(info);
|
||||
}));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn init_terminal() -> Result<Terminal<impl Backend>> {
|
||||
enable_raw_mode()?;
|
||||
stdout().execute(EnterAlternateScreen)?;
|
||||
let backend = CrosstermBackend::new(stdout());
|
||||
let terminal = Terminal::new(backend)?;
|
||||
Ok(terminal)
|
||||
}
|
||||
|
||||
fn restore_terminal() -> Result<()> {
|
||||
disable_raw_mode()?;
|
||||
stdout().execute(LeaveAlternateScreen)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,3 +1,18 @@
|
||||
//! # [Ratatui] Custom Widget example
|
||||
//!
|
||||
//! The latest version of this example is available in the [examples] folder in the repository.
|
||||
//!
|
||||
//! Please note that the examples are designed to be run against the `main` branch of the Github
|
||||
//! repository. This means that you may not be able to compile with the latest release version on
|
||||
//! crates.io, or the one that you have installed locally.
|
||||
//!
|
||||
//! See the [examples readme] for more information on finding examples that match the version of the
|
||||
//! library you are using.
|
||||
//!
|
||||
//! [Ratatui]: https://github.com/ratatui-org/ratatui
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
use std::{error::Error, io, ops::ControlFlow, time::Duration};
|
||||
|
||||
use crossterm::{
|
||||
@@ -8,7 +23,7 @@ use crossterm::{
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
use ratatui::{prelude::*, widgets::Paragraph};
|
||||
|
||||
/// A custom widget that renders a button with a label, theme and state.
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -56,7 +71,7 @@ const GREEN: Theme = Theme {
|
||||
|
||||
/// A button with a label that can be themed.
|
||||
impl<'a> Button<'a> {
|
||||
pub fn new<T: Into<Line<'a>>>(label: T) -> Button<'a> {
|
||||
pub fn new<T: Into<Line<'a>>>(label: T) -> Self {
|
||||
Button {
|
||||
label: label.into(),
|
||||
theme: BLUE,
|
||||
@@ -64,18 +79,19 @@ impl<'a> Button<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn theme(mut self, theme: Theme) -> Button<'a> {
|
||||
pub const fn theme(mut self, theme: Theme) -> Self {
|
||||
self.theme = theme;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn state(mut self, state: State) -> Button<'a> {
|
||||
pub const fn state(mut self, state: State) -> Self {
|
||||
self.state = state;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for Button<'a> {
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let (background, text, shadow, highlight) = self.colors();
|
||||
buf.set_style(area, Style::new().bg(background).fg(text));
|
||||
@@ -109,7 +125,7 @@ impl<'a> Widget for Button<'a> {
|
||||
}
|
||||
|
||||
impl Button<'_> {
|
||||
fn colors(&self) -> (Color, Color, Color, Color) {
|
||||
const fn colors(&self) -> (Color, Color, Color, Color) {
|
||||
let theme = self.theme;
|
||||
match self.state {
|
||||
State::Normal => (theme.background, theme.text, theme.shadow, theme.highlight),
|
||||
@@ -148,7 +164,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
|
||||
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
|
||||
let mut selected_button: usize = 0;
|
||||
let button_states = &mut [State::Selected, State::Normal, State::Normal];
|
||||
let mut button_states = [State::Selected, State::Normal, State::Normal];
|
||||
loop {
|
||||
terminal.draw(|frame| ui(frame, button_states))?;
|
||||
if !event::poll(Duration::from_millis(100))? {
|
||||
@@ -159,54 +175,48 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
|
||||
if key.kind != event::KeyEventKind::Press {
|
||||
continue;
|
||||
}
|
||||
if handle_key_event(key, button_states, &mut selected_button).is_break() {
|
||||
if handle_key_event(key, &mut button_states, &mut selected_button).is_break() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse) => handle_mouse_event(mouse, button_states, &mut selected_button),
|
||||
Event::Mouse(mouse) => {
|
||||
handle_mouse_event(mouse, &mut button_states, &mut selected_button);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ui(frame: &mut Frame, states: &[State; 3]) {
|
||||
let layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(1),
|
||||
Constraint::Max(3),
|
||||
Constraint::Length(1),
|
||||
Constraint::Min(0), // ignore remaining space
|
||||
])
|
||||
.split(frame.size());
|
||||
fn ui(frame: &mut Frame, states: [State; 3]) {
|
||||
let vertical = Layout::vertical([
|
||||
Constraint::Length(1),
|
||||
Constraint::Max(3),
|
||||
Constraint::Length(1),
|
||||
Constraint::Min(0), // ignore remaining space
|
||||
]);
|
||||
let [title, buttons, help, _] = vertical.areas(frame.size());
|
||||
|
||||
frame.render_widget(
|
||||
Paragraph::new("Custom Widget Example (mouse enabled)"),
|
||||
layout[0],
|
||||
);
|
||||
render_buttons(frame, layout[1], states);
|
||||
frame.render_widget(
|
||||
Paragraph::new("←/→: select, Space: toggle, q: quit"),
|
||||
layout[2],
|
||||
title,
|
||||
);
|
||||
render_buttons(frame, buttons, states);
|
||||
frame.render_widget(Paragraph::new("←/→: select, Space: toggle, q: quit"), help);
|
||||
}
|
||||
|
||||
fn render_buttons(frame: &mut Frame<'_>, area: Rect, states: &[State; 3]) {
|
||||
let layout = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([
|
||||
Constraint::Length(15),
|
||||
Constraint::Length(15),
|
||||
Constraint::Length(15),
|
||||
Constraint::Min(0), // ignore remaining space
|
||||
])
|
||||
.split(area);
|
||||
frame.render_widget(Button::new("Red").theme(RED).state(states[0]), layout[0]);
|
||||
frame.render_widget(
|
||||
Button::new("Green").theme(GREEN).state(states[1]),
|
||||
layout[1],
|
||||
);
|
||||
frame.render_widget(Button::new("Blue").theme(BLUE).state(states[2]), layout[2]);
|
||||
fn render_buttons(frame: &mut Frame<'_>, area: Rect, states: [State; 3]) {
|
||||
let horizontal = Layout::horizontal([
|
||||
Constraint::Length(15),
|
||||
Constraint::Length(15),
|
||||
Constraint::Length(15),
|
||||
Constraint::Min(0), // ignore remaining space
|
||||
]);
|
||||
let [red, green, blue, _] = horizontal.areas(area);
|
||||
|
||||
frame.render_widget(Button::new("Red").theme(RED).state(states[0]), red);
|
||||
frame.render_widget(Button::new("Green").theme(GREEN).state(states[1]), green);
|
||||
frame.render_widget(Button::new("Blue").theme(BLUE).state(states[2]), blue);
|
||||
}
|
||||
|
||||
fn handle_key_event(
|
||||
|
||||
@@ -2,7 +2,7 @@ use rand::{
|
||||
distributions::{Distribution, Uniform},
|
||||
rngs::ThreadRng,
|
||||
};
|
||||
use ratatui::widgets::*;
|
||||
use ratatui::widgets::ListState;
|
||||
|
||||
const TASKS: [&str; 24] = [
|
||||
"Item1", "Item2", "Item3", "Item4", "Item5", "Item6", "Item7", "Item8", "Item9", "Item10",
|
||||
@@ -73,8 +73,8 @@ pub struct RandomSignal {
|
||||
}
|
||||
|
||||
impl RandomSignal {
|
||||
pub fn new(lower: u64, upper: u64) -> RandomSignal {
|
||||
RandomSignal {
|
||||
pub fn new(lower: u64, upper: u64) -> Self {
|
||||
Self {
|
||||
distribution: Uniform::new(lower, upper),
|
||||
rng: rand::thread_rng(),
|
||||
}
|
||||
@@ -97,8 +97,8 @@ pub struct SinSignal {
|
||||
}
|
||||
|
||||
impl SinSignal {
|
||||
pub fn new(interval: f64, period: f64, scale: f64) -> SinSignal {
|
||||
SinSignal {
|
||||
pub const fn new(interval: f64, period: f64, scale: f64) -> Self {
|
||||
Self {
|
||||
x: 0.0,
|
||||
interval,
|
||||
period,
|
||||
@@ -144,8 +144,8 @@ pub struct StatefulList<T> {
|
||||
}
|
||||
|
||||
impl<T> StatefulList<T> {
|
||||
pub fn with_items(items: Vec<T>) -> StatefulList<T> {
|
||||
StatefulList {
|
||||
pub fn with_items(items: Vec<T>) -> Self {
|
||||
Self {
|
||||
state: ListState::default(),
|
||||
items,
|
||||
}
|
||||
@@ -191,9 +191,7 @@ where
|
||||
S: Iterator,
|
||||
{
|
||||
fn on_tick(&mut self) {
|
||||
for _ in 0..self.tick_rate {
|
||||
self.points.remove(0);
|
||||
}
|
||||
self.points.drain(0..self.tick_rate);
|
||||
self.points
|
||||
.extend(self.source.by_ref().take(self.tick_rate));
|
||||
}
|
||||
@@ -237,7 +235,7 @@ pub struct App<'a> {
|
||||
}
|
||||
|
||||
impl<'a> App<'a> {
|
||||
pub fn new(title: &'a str, enhanced_graphics: bool) -> App<'a> {
|
||||
pub fn new(title: &'a str, enhanced_graphics: bool) -> Self {
|
||||
let mut rand_signal = RandomSignal::new(0, 100);
|
||||
let sparkline_points = rand_signal.by_ref().take(300).collect();
|
||||
let mut sin_signal = SinSignal::new(0.2, 3.0, 18.0);
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
//! # [Ratatui] Original Demo example
|
||||
//!
|
||||
//! The latest version of this example is available in the [examples] folder in the repository.
|
||||
//!
|
||||
//! Please note that the examples are designed to be run against the `main` branch of the Github
|
||||
//! repository. This means that you may not be able to compile with the latest release version on
|
||||
//! crates.io, or the one that you have installed locally.
|
||||
//!
|
||||
//! See the [examples readme] for more information on finding examples that match the version of the
|
||||
//! library you are using.
|
||||
//!
|
||||
//! [Ratatui]: https://github.com/ratatui-org/ratatui
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
use std::{error::Error, time::Duration};
|
||||
|
||||
use argh::FromArgs;
|
||||
|
||||
@@ -4,7 +4,10 @@ use std::{
|
||||
};
|
||||
|
||||
use ratatui::prelude::*;
|
||||
use termwiz::{input::*, terminal::Terminal as TermwizTerminal};
|
||||
use termwiz::{
|
||||
input::{InputEvent, KeyCode},
|
||||
terminal::Terminal as TermwizTerminal,
|
||||
};
|
||||
|
||||
use crate::{app::App, ui};
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#[allow(clippy::wildcard_imports)]
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{canvas::*, *},
|
||||
@@ -6,16 +7,13 @@ use ratatui::{
|
||||
use crate::app::App;
|
||||
|
||||
pub fn draw(f: &mut Frame, app: &mut App) {
|
||||
let chunks = Layout::default()
|
||||
.constraints([Constraint::Length(3), Constraint::Min(0)])
|
||||
.split(f.size());
|
||||
let titles = app
|
||||
let chunks = Layout::vertical([Constraint::Length(3), Constraint::Min(0)]).split(f.size());
|
||||
let tabs = app
|
||||
.tabs
|
||||
.titles
|
||||
.iter()
|
||||
.map(|t| text::Line::from(Span::styled(*t, Style::default().fg(Color::Green))))
|
||||
.collect();
|
||||
let tabs = Tabs::new(titles)
|
||||
.collect::<Tabs>()
|
||||
.block(Block::default().borders(Borders::ALL).title(app.title))
|
||||
.highlight_style(Style::default().fg(Color::Yellow))
|
||||
.select(app.tabs.index);
|
||||
@@ -29,27 +27,25 @@ pub fn draw(f: &mut Frame, app: &mut App) {
|
||||
}
|
||||
|
||||
fn draw_first_tab(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
let chunks = Layout::default()
|
||||
.constraints([
|
||||
Constraint::Length(9),
|
||||
Constraint::Min(8),
|
||||
Constraint::Length(7),
|
||||
])
|
||||
.split(area);
|
||||
let chunks = Layout::vertical([
|
||||
Constraint::Length(9),
|
||||
Constraint::Min(8),
|
||||
Constraint::Length(7),
|
||||
])
|
||||
.split(area);
|
||||
draw_gauges(f, app, chunks[0]);
|
||||
draw_charts(f, app, chunks[1]);
|
||||
draw_text(f, chunks[2]);
|
||||
}
|
||||
|
||||
fn draw_gauges(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
let chunks = Layout::default()
|
||||
.constraints([
|
||||
Constraint::Length(2),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(1),
|
||||
])
|
||||
.margin(1)
|
||||
.split(area);
|
||||
let chunks = Layout::vertical([
|
||||
Constraint::Length(2),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(1),
|
||||
])
|
||||
.margin(1)
|
||||
.split(area);
|
||||
let block = Block::default().borders(Borders::ALL).title("Graphs");
|
||||
f.render_widget(block, area);
|
||||
|
||||
@@ -90,25 +86,21 @@ fn draw_gauges(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
f.render_widget(line_gauge, chunks[2]);
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn draw_charts(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
let constraints = if app.show_chart {
|
||||
vec![Constraint::Percentage(50), Constraint::Percentage(50)]
|
||||
} else {
|
||||
vec![Constraint::Percentage(100)]
|
||||
};
|
||||
let chunks = Layout::default()
|
||||
.constraints(constraints)
|
||||
.direction(Direction::Horizontal)
|
||||
.split(area);
|
||||
let chunks = Layout::horizontal(constraints).split(area);
|
||||
{
|
||||
let chunks = Layout::default()
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
let chunks = Layout::vertical([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
.split(chunks[0]);
|
||||
{
|
||||
let chunks = Layout::default()
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
.direction(Direction::Horizontal)
|
||||
.split(chunks[0]);
|
||||
let chunks =
|
||||
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
.split(chunks[0]);
|
||||
|
||||
// Draw tasks
|
||||
let tasks: Vec<ListItem> = app
|
||||
@@ -273,10 +265,8 @@ fn draw_text(f: &mut Frame, area: Rect) {
|
||||
}
|
||||
|
||||
fn draw_second_tab(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
let chunks = Layout::default()
|
||||
.constraints([Constraint::Percentage(30), Constraint::Percentage(70)])
|
||||
.direction(Direction::Horizontal)
|
||||
.split(area);
|
||||
let chunks =
|
||||
Layout::horizontal([Constraint::Percentage(30), Constraint::Percentage(70)]).split(area);
|
||||
let up_style = Style::default().fg(Color::Green);
|
||||
let failure_style = Style::default()
|
||||
.fg(Color::Red)
|
||||
@@ -361,10 +351,7 @@ fn draw_second_tab(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
}
|
||||
|
||||
fn draw_third_tab(f: &mut Frame, _app: &mut App, area: Rect) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)])
|
||||
.split(area);
|
||||
let chunks = Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]).split(area);
|
||||
let colors = [
|
||||
Color::Reset,
|
||||
Color::Black,
|
||||
|
||||
@@ -1,99 +1,222 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
|
||||
use ratatui::prelude::Rect;
|
||||
use color_eyre::{eyre::Context, Result};
|
||||
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind};
|
||||
use itertools::Itertools;
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
|
||||
|
||||
use crate::{Root, Term};
|
||||
use crate::{destroy, tabs::*, term, THEME};
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct App {
|
||||
term: Term,
|
||||
should_quit: bool,
|
||||
context: AppContext,
|
||||
mode: Mode,
|
||||
tab: Tab,
|
||||
about_tab: AboutTab,
|
||||
recipe_tab: RecipeTab,
|
||||
email_tab: EmailTab,
|
||||
traceroute_tab: TracerouteTab,
|
||||
weather_tab: WeatherTab,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub struct AppContext {
|
||||
pub tab_index: usize,
|
||||
pub row_index: usize,
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
enum Mode {
|
||||
#[default]
|
||||
Running,
|
||||
Destroy,
|
||||
Quit,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, Display, EnumIter, FromRepr, PartialEq, Eq)]
|
||||
enum Tab {
|
||||
#[default]
|
||||
About,
|
||||
Recipe,
|
||||
Email,
|
||||
Traceroute,
|
||||
Weather,
|
||||
}
|
||||
|
||||
pub fn run(terminal: &mut Terminal<impl Backend>) -> Result<()> {
|
||||
App::new().run(terminal)
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn new() -> Result<Self> {
|
||||
Ok(Self {
|
||||
term: Term::start()?,
|
||||
should_quit: false,
|
||||
context: AppContext::default(),
|
||||
})
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn run() -> Result<()> {
|
||||
install_panic_hook();
|
||||
let mut app = Self::new()?;
|
||||
while !app.should_quit {
|
||||
app.draw()?;
|
||||
app.handle_events()?;
|
||||
/// Run the app until the user quits.
|
||||
pub fn run(&mut self, terminal: &mut Terminal<impl Backend>) -> Result<()> {
|
||||
while self.is_running() {
|
||||
self.draw(terminal)?;
|
||||
self.handle_events()?;
|
||||
}
|
||||
Term::stop()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw(&mut self) -> Result<()> {
|
||||
self.term
|
||||
.draw(|frame| frame.render_widget(Root::new(&self.context), frame.size()))
|
||||
.context("terminal.draw")?;
|
||||
fn is_running(&self) -> bool {
|
||||
self.mode != Mode::Quit
|
||||
}
|
||||
|
||||
/// Draw a single frame of the app.
|
||||
fn draw(&self, terminal: &mut Terminal<impl Backend>) -> Result<()> {
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
frame.render_widget(self, frame.size());
|
||||
if self.mode == Mode::Destroy {
|
||||
destroy::destroy(frame);
|
||||
}
|
||||
})
|
||||
.wrap_err("terminal.draw")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle events from the terminal.
|
||||
///
|
||||
/// This function is called once per frame, The events are polled from the stdin with timeout of
|
||||
/// 1/50th of a second. This was chosen to try to match the default frame rate of a GIF in VHS.
|
||||
fn handle_events(&mut self) -> Result<()> {
|
||||
match Term::next_event(Duration::from_millis(16))? {
|
||||
Some(Event::Key(key)) => self.handle_key_event(key),
|
||||
Some(Event::Resize(width, height)) => {
|
||||
Ok(self.term.resize(Rect::new(0, 0, width, height))?)
|
||||
}
|
||||
_ => Ok(()),
|
||||
let timeout = Duration::from_secs_f64(1.0 / 50.0);
|
||||
match term::next_event(timeout)? {
|
||||
Some(Event::Key(key)) if key.kind == KeyEventKind::Press => self.handle_key_press(key),
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_key_event(&mut self, key: KeyEvent) -> Result<()> {
|
||||
if key.kind != KeyEventKind::Press {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let context = &mut self.context;
|
||||
const TAB_COUNT: usize = 5;
|
||||
fn handle_key_press(&mut self, key: KeyEvent) {
|
||||
use KeyCode::*;
|
||||
match key.code {
|
||||
KeyCode::Char('q') | KeyCode::Esc => {
|
||||
self.should_quit = true;
|
||||
}
|
||||
KeyCode::Tab | KeyCode::BackTab if key.modifiers.contains(KeyModifiers::SHIFT) => {
|
||||
let tab_index = context.tab_index + TAB_COUNT; // to wrap around properly
|
||||
context.tab_index = tab_index.saturating_sub(1) % TAB_COUNT;
|
||||
context.row_index = 0;
|
||||
}
|
||||
KeyCode::Tab | KeyCode::BackTab => {
|
||||
context.tab_index = context.tab_index.saturating_add(1) % TAB_COUNT;
|
||||
context.row_index = 0;
|
||||
}
|
||||
KeyCode::Up | KeyCode::Char('k') => {
|
||||
context.row_index = context.row_index.saturating_sub(1);
|
||||
}
|
||||
KeyCode::Down | KeyCode::Char('j') => {
|
||||
context.row_index = context.row_index.saturating_add(1);
|
||||
}
|
||||
Char('q') | Esc => self.mode = Mode::Quit,
|
||||
Char('h') | Left => self.prev_tab(),
|
||||
Char('l') | Right => self.next_tab(),
|
||||
Char('k') | Up => self.prev(),
|
||||
Char('j') | Down => self.next(),
|
||||
Char('d') | Delete => self.destroy(),
|
||||
_ => {}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prev(&mut self) {
|
||||
match self.tab {
|
||||
Tab::About => self.about_tab.prev_row(),
|
||||
Tab::Recipe => self.recipe_tab.prev(),
|
||||
Tab::Email => self.email_tab.prev(),
|
||||
Tab::Traceroute => self.traceroute_tab.prev_row(),
|
||||
Tab::Weather => self.weather_tab.prev(),
|
||||
}
|
||||
}
|
||||
|
||||
fn next(&mut self) {
|
||||
match self.tab {
|
||||
Tab::About => self.about_tab.next_row(),
|
||||
Tab::Recipe => self.recipe_tab.next(),
|
||||
Tab::Email => self.email_tab.next(),
|
||||
Tab::Traceroute => self.traceroute_tab.next_row(),
|
||||
Tab::Weather => self.weather_tab.next(),
|
||||
}
|
||||
}
|
||||
|
||||
fn prev_tab(&mut self) {
|
||||
self.tab = self.tab.prev();
|
||||
}
|
||||
|
||||
fn next_tab(&mut self) {
|
||||
self.tab = self.tab.next();
|
||||
}
|
||||
|
||||
fn destroy(&mut self) {
|
||||
self.mode = Mode::Destroy;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn install_panic_hook() {
|
||||
better_panic::install();
|
||||
let hook = std::panic::take_hook();
|
||||
std::panic::set_hook(Box::new(move |info| {
|
||||
let _ = Term::stop();
|
||||
hook(info);
|
||||
std::process::exit(1);
|
||||
}));
|
||||
/// Implement Widget for &App rather than for App as we would otherwise have to clone or copy the
|
||||
/// entire app state on every frame. For this example, the app state is small enough that it doesn't
|
||||
/// matter, but for larger apps this can be a significant performance improvement.
|
||||
impl Widget for &App {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let vertical = Layout::vertical([
|
||||
Constraint::Length(1),
|
||||
Constraint::Min(0),
|
||||
Constraint::Length(1),
|
||||
]);
|
||||
let [title_bar, tab, bottom_bar] = vertical.areas(area);
|
||||
|
||||
Block::new().style(THEME.root).render(area, buf);
|
||||
self.render_title_bar(title_bar, buf);
|
||||
self.render_selected_tab(tab, buf);
|
||||
App::render_bottom_bar(bottom_bar, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn render_title_bar(&self, area: Rect, buf: &mut Buffer) {
|
||||
let layout = Layout::horizontal([Constraint::Min(0), Constraint::Length(43)]);
|
||||
let [title, tabs] = layout.areas(area);
|
||||
|
||||
Span::styled("Ratatui", THEME.app_title).render(title, buf);
|
||||
let titles = Tab::iter().map(Tab::title);
|
||||
Tabs::new(titles)
|
||||
.style(THEME.tabs)
|
||||
.highlight_style(THEME.tabs_selected)
|
||||
.select(self.tab as usize)
|
||||
.divider("")
|
||||
.padding("", "")
|
||||
.render(tabs, buf);
|
||||
}
|
||||
|
||||
fn render_selected_tab(&self, area: Rect, buf: &mut Buffer) {
|
||||
match self.tab {
|
||||
Tab::About => self.about_tab.render(area, buf),
|
||||
Tab::Recipe => self.recipe_tab.render(area, buf),
|
||||
Tab::Email => self.email_tab.render(area, buf),
|
||||
Tab::Traceroute => self.traceroute_tab.render(area, buf),
|
||||
Tab::Weather => self.weather_tab.render(area, buf),
|
||||
};
|
||||
}
|
||||
|
||||
fn render_bottom_bar(area: Rect, buf: &mut Buffer) {
|
||||
let keys = [
|
||||
("H/←", "Left"),
|
||||
("L/→", "Right"),
|
||||
("K/↑", "Up"),
|
||||
("J/↓", "Down"),
|
||||
("D/Del", "Destroy"),
|
||||
("Q/Esc", "Quit"),
|
||||
];
|
||||
let spans = keys
|
||||
.iter()
|
||||
.flat_map(|(key, desc)| {
|
||||
let key = Span::styled(format!(" {key} "), THEME.key_binding.key);
|
||||
let desc = Span::styled(format!(" {desc} "), THEME.key_binding.description);
|
||||
[key, desc]
|
||||
})
|
||||
.collect_vec();
|
||||
Line::from(spans)
|
||||
.centered()
|
||||
.style((Color::Indexed(236), Color::Indexed(232)))
|
||||
.render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl Tab {
|
||||
fn next(self) -> Self {
|
||||
let current_index = self as usize;
|
||||
let next_index = current_index.saturating_add(1);
|
||||
Self::from_repr(next_index).unwrap_or(self)
|
||||
}
|
||||
|
||||
fn prev(self) -> Self {
|
||||
let current_index = self as usize;
|
||||
let prev_index = current_index.saturating_sub(1);
|
||||
Self::from_repr(prev_index).unwrap_or(self)
|
||||
}
|
||||
|
||||
fn title(self) -> String {
|
||||
match self {
|
||||
Self::About => String::new(),
|
||||
tab => format!(" {tab} "),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
821
examples/demo2/big_text.rs
Normal file
821
examples/demo2/big_text.rs
Normal file
@@ -0,0 +1,821 @@
|
||||
//! [tui-big-text] is a rust crate that renders large pixel text as a [Ratatui] widget using the
|
||||
//! glyphs from the [font8x8] crate.
|
||||
//!
|
||||
//! 
|
||||
//!
|
||||
//! # Installation
|
||||
//!
|
||||
//! ```shell
|
||||
//! cargo add ratatui tui-big-text
|
||||
//! ```
|
||||
//!
|
||||
//! # Usage
|
||||
//!
|
||||
//! Create a [`BigText`] widget using `BigTextBuilder` and pass it to [`Frame::render_widget`] to
|
||||
//! render be rendered. The builder allows you to customize the [`Style`] of the widget and the
|
||||
//! [`PixelSize`] of the glyphs. The [`PixelSize`] can be used to control how many character cells
|
||||
//! are used to represent a single pixel of the 8x8 font.
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```rust
|
||||
//! use anyhow::Result;
|
||||
//! use ratatui::prelude::*;
|
||||
//! use tui_big_text::{BigTextBuilder, PixelSize};
|
||||
//!
|
||||
//! fn render(frame: &mut Frame) -> Result<()> {
|
||||
//! let big_text = BigTextBuilder::default()
|
||||
//! .pixel_size(PixelSize::Full)
|
||||
//! .style(Style::new().blue())
|
||||
//! .lines(vec![
|
||||
//! "Hello".red().into(),
|
||||
//! "World".white().into(),
|
||||
//! "~~~~~".into(),
|
||||
//! ])
|
||||
//! .build()?;
|
||||
//! frame.render_widget(big_text, frame.size());
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! [tui-big-text]: https://crates.io/crates/tui-big-text
|
||||
//! [Ratatui]: https://crates.io/crates/ratatui
|
||||
//! [font8x8]: https://crates.io/crates/font8x8
|
||||
//! [`BigText`]: crate::BigText
|
||||
//! [`PixelSize`]: crate::PixelSize
|
||||
//! [`Frame::render_widget`]: ratatui::Frame::render_widget
|
||||
//! [`Style`]: ratatui::style::Style
|
||||
|
||||
use std::cmp::min;
|
||||
|
||||
use derive_builder::Builder;
|
||||
use font8x8::UnicodeFonts;
|
||||
use ratatui::{prelude::*, text::StyledGrapheme};
|
||||
|
||||
#[allow(unused)]
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Default)]
|
||||
pub enum PixelSize {
|
||||
#[default]
|
||||
/// A pixel from the 8x8 font is represented by a full character cell in the terminal.
|
||||
Full,
|
||||
/// A pixel from the 8x8 font is represented by a half (upper/lower) character cell in the
|
||||
/// terminal.
|
||||
HalfHeight,
|
||||
/// A pixel from the 8x8 font is represented by a half (left/right) character cell in the
|
||||
/// terminal.
|
||||
HalfWidth,
|
||||
/// A pixel from the 8x8 font is represented by a quadrant of a character cell in the terminal.
|
||||
Quadrant,
|
||||
}
|
||||
|
||||
/// Displays one or more lines of text using 8x8 pixel characters.
|
||||
///
|
||||
/// The text is rendered using the [font8x8](https://crates.io/crates/font8x8) crate.
|
||||
///
|
||||
/// Using the `pixel_size` method, you can also chose, how 'big' a pixel should be.
|
||||
/// Currently a pixel of the 8x8 font can be represented by one full or half
|
||||
/// (horizontal/vertical/both) character cell of the terminal.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::prelude::*;
|
||||
/// use tui_big_text::{BigTextBuilder, PixelSize};
|
||||
///
|
||||
/// BigText::builder()
|
||||
/// .pixel_size(PixelSize::Full)
|
||||
/// .style(Style::new().white())
|
||||
/// .lines(vec![
|
||||
/// "Hello".red().into(),
|
||||
/// "World".blue().into(),
|
||||
/// "=====".into(),
|
||||
/// ])
|
||||
/// .build();
|
||||
/// ```
|
||||
///
|
||||
/// Renders:
|
||||
///
|
||||
/// ```plain
|
||||
/// ██ ██ ███ ███
|
||||
/// ██ ██ ██ ██
|
||||
/// ██ ██ ████ ██ ██ ████
|
||||
/// ██████ ██ ██ ██ ██ ██ ██
|
||||
/// ██ ██ ██████ ██ ██ ██ ██
|
||||
/// ██ ██ ██ ██ ██ ██ ██
|
||||
/// ██ ██ ████ ████ ████ ████
|
||||
///
|
||||
/// ██ ██ ███ ███
|
||||
/// ██ ██ ██ ██
|
||||
/// ██ ██ ████ ██ ███ ██ ██
|
||||
/// ██ █ ██ ██ ██ ███ ██ ██ █████
|
||||
/// ███████ ██ ██ ██ ██ ██ ██ ██
|
||||
/// ███ ███ ██ ██ ██ ██ ██ ██
|
||||
/// ██ ██ ████ ████ ████ ███ ██
|
||||
///
|
||||
/// ███ ██ ███ ██ ███ ██ ███ ██ ███ ██
|
||||
/// ██ ███ ██ ███ ██ ███ ██ ███ ██ ███
|
||||
/// ```
|
||||
#[derive(Debug, Builder, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct BigText<'a> {
|
||||
/// The text to display
|
||||
#[builder(setter(into))]
|
||||
lines: Vec<Line<'a>>,
|
||||
|
||||
/// The style of the widget
|
||||
///
|
||||
/// Defaults to `Style::default()`
|
||||
#[builder(default)]
|
||||
style: Style,
|
||||
|
||||
/// The size of single glyphs
|
||||
///
|
||||
/// Defaults to `BigTextSize::default()` (=> `BigTextSize::Full`)
|
||||
#[builder(default)]
|
||||
pixel_size: PixelSize,
|
||||
}
|
||||
|
||||
impl Widget for BigText<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let layout = layout(area, self.pixel_size);
|
||||
for (line, line_layout) in self.lines.iter().zip(layout) {
|
||||
for (g, cell) in line.styled_graphemes(self.style).zip(line_layout) {
|
||||
render_symbol(&g, cell, buf, self.pixel_size);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns how many cells are needed to display a full 8x8 glyphe using the given font size
|
||||
const fn cells_per_glyph(size: PixelSize) -> (u16, u16) {
|
||||
match size {
|
||||
PixelSize::Full => (8, 8),
|
||||
PixelSize::HalfHeight => (8, 4),
|
||||
PixelSize::HalfWidth => (4, 8),
|
||||
PixelSize::Quadrant => (4, 4),
|
||||
}
|
||||
}
|
||||
|
||||
/// Chunk the area into as many x*y cells as possible returned as a 2D iterator of `Rect`s
|
||||
/// representing the rows of cells.
|
||||
/// The size of each cell depends on given font size
|
||||
fn layout(
|
||||
area: Rect,
|
||||
pixel_size: PixelSize,
|
||||
) -> impl IntoIterator<Item = impl IntoIterator<Item = Rect>> {
|
||||
let (width, height) = cells_per_glyph(pixel_size);
|
||||
(area.top()..area.bottom())
|
||||
.step_by(height as usize)
|
||||
.map(move |y| {
|
||||
(area.left()..area.right())
|
||||
.step_by(width as usize)
|
||||
.map(move |x| {
|
||||
let width = min(area.right() - x, width);
|
||||
let height = min(area.bottom() - y, height);
|
||||
Rect::new(x, y, width, height)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Render a single grapheme into a cell by looking up the corresponding 8x8 bitmap in the
|
||||
/// `BITMAPS` array and setting the corresponding cells in the buffer.
|
||||
fn render_symbol(grapheme: &StyledGrapheme, area: Rect, buf: &mut Buffer, pixel_size: PixelSize) {
|
||||
buf.set_style(area, grapheme.style);
|
||||
let c = grapheme.symbol.chars().next().unwrap(); // TODO: handle multi-char graphemes
|
||||
if let Some(glyph) = font8x8::BASIC_FONTS.get(c) {
|
||||
render_glyph(glyph, area, buf, pixel_size);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the correct unicode symbol for two vertical "pixels"
|
||||
const fn get_symbol_half_height(top: u8, bottom: u8) -> char {
|
||||
match top {
|
||||
0 => match bottom {
|
||||
0 => ' ',
|
||||
_ => '▄',
|
||||
},
|
||||
_ => match bottom {
|
||||
0 => '▀',
|
||||
_ => '█',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the correct unicode symbol for two horizontal "pixels"
|
||||
const fn get_symbol_half_width(left: u8, right: u8) -> char {
|
||||
match left {
|
||||
0 => match right {
|
||||
0 => ' ',
|
||||
_ => '▐',
|
||||
},
|
||||
_ => match right {
|
||||
0 => '▌',
|
||||
_ => '█',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the correct unicode symbol for 2x2 "pixels"
|
||||
const fn get_symbol_half_size(
|
||||
top_left: u8,
|
||||
top_right: u8,
|
||||
bottom_left: u8,
|
||||
bottom_right: u8,
|
||||
) -> char {
|
||||
const QUADRANT_SYMBOLS: [char; 16] = [
|
||||
' ', '▘', '▝', '▀', '▖', '▌', '▞', '▛', '▗', '▚', '▐', '▜', '▄', '▙', '▟', '█',
|
||||
];
|
||||
|
||||
let top_left = if top_left > 0 { 1 } else { 0 };
|
||||
let top_right = if top_right > 0 { 1 << 1 } else { 0 };
|
||||
let bottom_left = if bottom_left > 0 { 1 << 2 } else { 0 };
|
||||
let bottom_right = if bottom_right > 0 { 1 << 3 } else { 0 };
|
||||
|
||||
QUADRANT_SYMBOLS[top_left + top_right + bottom_left + bottom_right]
|
||||
}
|
||||
|
||||
/// Render a single 8x8 glyph into a cell by setting the corresponding cells in the buffer.
|
||||
fn render_glyph(glyph: [u8; 8], area: Rect, buf: &mut Buffer, pixel_size: PixelSize) {
|
||||
let (width, height) = cells_per_glyph(pixel_size);
|
||||
|
||||
let glyph_vertical_index = (0..glyph.len()).step_by(8 / height as usize);
|
||||
let glyph_horizontal_bit_selector = (0..8).step_by(8 / width as usize);
|
||||
|
||||
for (row, y) in glyph_vertical_index.zip(area.top()..area.bottom()) {
|
||||
for (col, x) in glyph_horizontal_bit_selector
|
||||
.clone()
|
||||
.zip(area.left()..area.right())
|
||||
{
|
||||
let cell = buf.get_mut(x, y);
|
||||
let symbol_character = match pixel_size {
|
||||
PixelSize::Full => match glyph[row] & (1 << col) {
|
||||
0 => ' ',
|
||||
_ => '█',
|
||||
},
|
||||
PixelSize::HalfHeight => {
|
||||
let top = glyph[row] & (1 << col);
|
||||
let bottom = glyph[row + 1] & (1 << col);
|
||||
get_symbol_half_height(top, bottom)
|
||||
}
|
||||
PixelSize::HalfWidth => {
|
||||
let left = glyph[row] & (1 << col);
|
||||
let right = glyph[row] & (1 << (col + 1));
|
||||
get_symbol_half_width(left, right)
|
||||
}
|
||||
PixelSize::Quadrant => {
|
||||
let top_left = glyph[row] & (1 << col);
|
||||
let top_right = glyph[row] & (1 << (col + 1));
|
||||
let bottom_left = glyph[row + 1] & (1 << col);
|
||||
let bottom_right = glyph[row + 1] & (1 << (col + 1));
|
||||
get_symbol_half_size(top_left, top_right, bottom_left, bottom_right)
|
||||
}
|
||||
};
|
||||
cell.set_char(symbol_character);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ratatui::assert_buffer_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||
|
||||
#[test]
|
||||
fn build() -> Result<()> {
|
||||
let lines = vec![Line::from(vec!["Hello".red(), "World".blue()])];
|
||||
let style = Style::new().green();
|
||||
let pixel_size = PixelSize::default();
|
||||
assert_eq!(
|
||||
BigTextBuilder::default()
|
||||
.lines(lines.clone())
|
||||
.style(style)
|
||||
.build()?,
|
||||
BigText {
|
||||
lines,
|
||||
style,
|
||||
pixel_size
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_single_line() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.lines(vec![Line::from("SingleLine")])
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 80, 8));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines(vec![
|
||||
" ████ ██ ███ ████ ██ ",
|
||||
"██ ██ ██ ██ ",
|
||||
"███ ███ █████ ███ ██ ██ ████ ██ ███ █████ ████ ",
|
||||
" ███ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ",
|
||||
" ███ ██ ██ ██ ██ ██ ██ ██████ ██ █ ██ ██ ██ ██████ ",
|
||||
"██ ██ ██ ██ ██ █████ ██ ██ ██ ██ ██ ██ ██ ██ ",
|
||||
" ████ ████ ██ ██ ██ ████ ████ ███████ ████ ██ ██ ████ ",
|
||||
" █████ ",
|
||||
]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_truncated() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.lines(vec![Line::from("Truncated")])
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 70, 6));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines(vec![
|
||||
"██████ █ ███",
|
||||
"█ ██ █ ██ ██",
|
||||
" ██ ██ ███ ██ ██ █████ ████ ████ █████ ████ ██",
|
||||
" ██ ███ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ █████",
|
||||
" ██ ██ ██ ██ ██ ██ ██ ██ █████ ██ ██████ ██ ██",
|
||||
" ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ █ ██ ██ ██",
|
||||
]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_multiple_lines() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.lines(vec![Line::from("Multi"), Line::from("Lines")])
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 40, 16));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines(vec![
|
||||
"██ ██ ███ █ ██ ",
|
||||
"███ ███ ██ ██ ",
|
||||
"███████ ██ ██ ██ █████ ███ ",
|
||||
"███████ ██ ██ ██ ██ ██ ",
|
||||
"██ █ ██ ██ ██ ██ ██ ██ ",
|
||||
"██ ██ ██ ██ ██ ██ █ ██ ",
|
||||
"██ ██ ███ ██ ████ ██ ████ ",
|
||||
" ",
|
||||
"████ ██ ",
|
||||
" ██ ",
|
||||
" ██ ███ █████ ████ █████ ",
|
||||
" ██ ██ ██ ██ ██ ██ ██ ",
|
||||
" ██ █ ██ ██ ██ ██████ ████ ",
|
||||
" ██ ██ ██ ██ ██ ██ ██ ",
|
||||
"███████ ████ ██ ██ ████ █████ ",
|
||||
" ",
|
||||
]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_widget_style() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.lines(vec![Line::from("Styled")])
|
||||
.style(Style::new().bold())
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 48, 8));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let mut expected = Buffer::with_lines(vec![
|
||||
" ████ █ ███ ███ ",
|
||||
"██ ██ ██ ██ ██ ",
|
||||
"███ █████ ██ ██ ██ ████ ██ ",
|
||||
" ███ ██ ██ ██ ██ ██ ██ █████ ",
|
||||
" ███ ██ ██ ██ ██ ██████ ██ ██ ",
|
||||
"██ ██ ██ █ █████ ██ ██ ██ ██ ",
|
||||
" ████ ██ ██ ████ ████ ███ ██ ",
|
||||
" █████ ",
|
||||
]);
|
||||
expected.set_style(Rect::new(0, 0, 48, 8), Style::new().bold());
|
||||
assert_buffer_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_line_style() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.lines(vec![
|
||||
Line::from("Red".red()),
|
||||
Line::from("Green".green()),
|
||||
Line::from("Blue".blue()),
|
||||
])
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 40, 24));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let mut expected = Buffer::with_lines(vec![
|
||||
"██████ ███ ",
|
||||
" ██ ██ ██ ",
|
||||
" ██ ██ ████ ██ ",
|
||||
" █████ ██ ██ █████ ",
|
||||
" ██ ██ ██████ ██ ██ ",
|
||||
" ██ ██ ██ ██ ██ ",
|
||||
"███ ██ ████ ███ ██ ",
|
||||
" ",
|
||||
" ████ ",
|
||||
" ██ ██ ",
|
||||
"██ ██ ███ ████ ████ █████ ",
|
||||
"██ ███ ██ ██ ██ ██ ██ ██ ██ ",
|
||||
"██ ███ ██ ██ ██████ ██████ ██ ██ ",
|
||||
" ██ ██ ██ ██ ██ ██ ██ ",
|
||||
" █████ ████ ████ ████ ██ ██ ",
|
||||
" ",
|
||||
"██████ ███ ",
|
||||
" ██ ██ ██ ",
|
||||
" ██ ██ ██ ██ ██ ████ ",
|
||||
" █████ ██ ██ ██ ██ ██ ",
|
||||
" ██ ██ ██ ██ ██ ██████ ",
|
||||
" ██ ██ ██ ██ ██ ██ ",
|
||||
"██████ ████ ███ ██ ████ ",
|
||||
" ",
|
||||
]);
|
||||
expected.set_style(Rect::new(0, 0, 24, 8), Style::new().red());
|
||||
expected.set_style(Rect::new(0, 8, 40, 8), Style::new().green());
|
||||
expected.set_style(Rect::new(0, 16, 32, 8), Style::new().blue());
|
||||
assert_buffer_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_half_height_single_line() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.pixel_size(PixelSize::HalfHeight)
|
||||
.lines(vec![Line::from("SingleLine")])
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 80, 4));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines(vec![
|
||||
"▄█▀▀█▄ ▀▀ ▀██ ▀██▀ ▀▀ ",
|
||||
"▀██▄ ▀██ ██▀▀█▄ ▄█▀▀▄█▀ ██ ▄█▀▀█▄ ██ ▀██ ██▀▀█▄ ▄█▀▀█▄ ",
|
||||
"▄▄ ▀██ ██ ██ ██ ▀█▄▄██ ██ ██▀▀▀▀ ██ ▄█ ██ ██ ██ ██▀▀▀▀ ",
|
||||
" ▀▀▀▀ ▀▀▀▀ ▀▀ ▀▀ ▄▄▄▄█▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀▀▀▀ ▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀ ",
|
||||
]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_half_height_truncated() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.pixel_size(PixelSize::HalfHeight)
|
||||
.lines(vec![Line::from("Truncated")])
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 70, 3));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines(vec![
|
||||
"█▀██▀█ ▄█ ▀██",
|
||||
" ██ ▀█▄█▀█▄ ██ ██ ██▀▀█▄ ▄█▀▀█▄ ▀▀▀█▄ ▀██▀▀ ▄█▀▀█▄ ▄▄▄██",
|
||||
" ██ ██ ▀▀ ██ ██ ██ ██ ██ ▄▄ ▄█▀▀██ ██ ▄ ██▀▀▀▀ ██ ██",
|
||||
]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_half_height_multiple_lines() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.pixel_size(PixelSize::HalfHeight)
|
||||
.lines(vec![Line::from("Multi"), Line::from("Lines")])
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 40, 8));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines(vec![
|
||||
"██▄ ▄██ ▀██ ▄█ ▀▀ ",
|
||||
"███████ ██ ██ ██ ▀██▀▀ ▀██ ",
|
||||
"██ ▀ ██ ██ ██ ██ ██ ▄ ██ ",
|
||||
"▀▀ ▀▀ ▀▀▀ ▀▀ ▀▀▀▀ ▀▀ ▀▀▀▀ ",
|
||||
"▀██▀ ▀▀ ",
|
||||
" ██ ▀██ ██▀▀█▄ ▄█▀▀█▄ ▄█▀▀▀▀ ",
|
||||
" ██ ▄█ ██ ██ ██ ██▀▀▀▀ ▀▀▀█▄ ",
|
||||
"▀▀▀▀▀▀▀ ▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀ ▀▀▀▀▀ ",
|
||||
]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_half_height_widget_style() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.pixel_size(PixelSize::HalfHeight)
|
||||
.lines(vec![Line::from("Styled")])
|
||||
.style(Style::new().bold())
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 48, 4));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let mut expected = Buffer::with_lines(vec![
|
||||
"▄█▀▀█▄ ▄█ ▀██ ▀██ ",
|
||||
"▀██▄ ▀██▀▀ ██ ██ ██ ▄█▀▀█▄ ▄▄▄██ ",
|
||||
"▄▄ ▀██ ██ ▄ ▀█▄▄██ ██ ██▀▀▀▀ ██ ██ ",
|
||||
" ▀▀▀▀ ▀▀ ▄▄▄▄█▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀ ▀▀ ",
|
||||
]);
|
||||
expected.set_style(Rect::new(0, 0, 48, 4), Style::new().bold());
|
||||
assert_buffer_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_half_height_line_style() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.pixel_size(PixelSize::HalfHeight)
|
||||
.lines(vec![
|
||||
Line::from("Red".red()),
|
||||
Line::from("Green".green()),
|
||||
Line::from("Blue".blue()),
|
||||
])
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 40, 12));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let mut expected = Buffer::with_lines(vec![
|
||||
"▀██▀▀█▄ ▀██ ",
|
||||
" ██▄▄█▀ ▄█▀▀█▄ ▄▄▄██ ",
|
||||
" ██ ▀█▄ ██▀▀▀▀ ██ ██ ",
|
||||
"▀▀▀ ▀▀ ▀▀▀▀ ▀▀▀ ▀▀ ",
|
||||
" ▄█▀▀█▄ ",
|
||||
"██ ▀█▄█▀█▄ ▄█▀▀█▄ ▄█▀▀█▄ ██▀▀█▄ ",
|
||||
"▀█▄ ▀██ ██ ▀▀ ██▀▀▀▀ ██▀▀▀▀ ██ ██ ",
|
||||
" ▀▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀ ▀▀ ",
|
||||
"▀██▀▀█▄ ▀██ ",
|
||||
" ██▄▄█▀ ██ ██ ██ ▄█▀▀█▄ ",
|
||||
" ██ ██ ██ ██ ██ ██▀▀▀▀ ",
|
||||
"▀▀▀▀▀▀ ▀▀▀▀ ▀▀▀ ▀▀ ▀▀▀▀ ",
|
||||
]);
|
||||
expected.set_style(Rect::new(0, 0, 24, 4), Style::new().red());
|
||||
expected.set_style(Rect::new(0, 4, 40, 4), Style::new().green());
|
||||
expected.set_style(Rect::new(0, 8, 32, 4), Style::new().blue());
|
||||
assert_buffer_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_half_width_single_line() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.pixel_size(PixelSize::HalfWidth)
|
||||
.lines(vec![Line::from("SingleLine")])
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 40, 8));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines(vec![
|
||||
"▐█▌ █ ▐█ ██ █ ",
|
||||
"█ █ █ ▐▌ ",
|
||||
"█▌ ▐█ ██▌ ▐█▐▌ █ ▐█▌ ▐▌ ▐█ ██▌ ▐█▌ ",
|
||||
"▐█ █ █ █ █ █ █ █ █ ▐▌ █ █ █ █ █ ",
|
||||
" ▐█ █ █ █ █ █ █ ███ ▐▌ ▌ █ █ █ ███ ",
|
||||
"█ █ █ █ █ ▐██ █ █ ▐▌▐▌ █ █ █ █ ",
|
||||
"▐█▌ ▐█▌ █ █ █ ▐█▌ ▐█▌ ███▌▐█▌ █ █ ▐█▌ ",
|
||||
" ██▌ ",
|
||||
]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_half_width_truncated() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.pixel_size(PixelSize::HalfWidth)
|
||||
.lines(vec![Line::from("Truncated")])
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 35, 6));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines(vec![
|
||||
"███ ▐ ▐█",
|
||||
"▌█▐ █ █",
|
||||
" █ █▐█ █ █ ██▌ ▐█▌ ▐█▌ ▐██ ▐█▌ █",
|
||||
" █ ▐█▐▌█ █ █ █ █ █ █ █ █ █ ▐██",
|
||||
" █ ▐▌▐▌█ █ █ █ █ ▐██ █ ███ █ █",
|
||||
" █ ▐▌ █ █ █ █ █ █ █ █ █▐ █ █ █",
|
||||
]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_half_width_multiple_lines() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.pixel_size(PixelSize::HalfWidth)
|
||||
.lines(vec![Line::from("Multi"), Line::from("Lines")])
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 16));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines(vec![
|
||||
"█ ▐▌ ▐█ ▐ █ ",
|
||||
"█▌█▌ █ █ ",
|
||||
"███▌█ █ █ ▐██ ▐█ ",
|
||||
"███▌█ █ █ █ █ ",
|
||||
"█▐▐▌█ █ █ █ █ ",
|
||||
"█ ▐▌█ █ █ █▐ █ ",
|
||||
"█ ▐▌▐█▐▌▐█▌ ▐▌ ▐█▌ ",
|
||||
" ",
|
||||
"██ █ ",
|
||||
"▐▌ ",
|
||||
"▐▌ ▐█ ██▌ ▐█▌ ▐██ ",
|
||||
"▐▌ █ █ █ █ █ █ ",
|
||||
"▐▌ ▌ █ █ █ ███ ▐█▌ ",
|
||||
"▐▌▐▌ █ █ █ █ █ ",
|
||||
"███▌▐█▌ █ █ ▐█▌ ██▌ ",
|
||||
" ",
|
||||
]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_half_width_widget_style() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.pixel_size(PixelSize::HalfWidth)
|
||||
.lines(vec![Line::from("Styled")])
|
||||
.style(Style::new().bold())
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 24, 8));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let mut expected = Buffer::with_lines(vec![
|
||||
"▐█▌ ▐ ▐█ ▐█ ",
|
||||
"█ █ █ █ █ ",
|
||||
"█▌ ▐██ █ █ █ ▐█▌ █ ",
|
||||
"▐█ █ █ █ █ █ █ ▐██ ",
|
||||
" ▐█ █ █ █ █ ███ █ █ ",
|
||||
"█ █ █▐ ▐██ █ █ █ █ ",
|
||||
"▐█▌ ▐▌ █ ▐█▌ ▐█▌ ▐█▐▌",
|
||||
" ██▌ ",
|
||||
]);
|
||||
expected.set_style(Rect::new(0, 0, 24, 8), Style::new().bold());
|
||||
assert_buffer_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_half_width_line_style() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.pixel_size(PixelSize::HalfWidth)
|
||||
.lines(vec![
|
||||
Line::from("Red".red()),
|
||||
Line::from("Green".green()),
|
||||
Line::from("Blue".blue()),
|
||||
])
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 24));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let mut expected = Buffer::with_lines(vec![
|
||||
"███ ▐█ ",
|
||||
"▐▌▐▌ █ ",
|
||||
"▐▌▐▌▐█▌ █ ",
|
||||
"▐██ █ █ ▐██ ",
|
||||
"▐▌█ ███ █ █ ",
|
||||
"▐▌▐▌█ █ █ ",
|
||||
"█▌▐▌▐█▌ ▐█▐▌ ",
|
||||
" ",
|
||||
" ██ ",
|
||||
"▐▌▐▌ ",
|
||||
"█ █▐█ ▐█▌ ▐█▌ ██▌ ",
|
||||
"█ ▐█▐▌█ █ █ █ █ █ ",
|
||||
"█ █▌▐▌▐▌███ ███ █ █ ",
|
||||
"▐▌▐▌▐▌ █ █ █ █ ",
|
||||
" ██▌██ ▐█▌ ▐█▌ █ █ ",
|
||||
" ",
|
||||
"███ ▐█ ",
|
||||
"▐▌▐▌ █ ",
|
||||
"▐▌▐▌ █ █ █ ▐█▌ ",
|
||||
"▐██ █ █ █ █ █ ",
|
||||
"▐▌▐▌ █ █ █ ███ ",
|
||||
"▐▌▐▌ █ █ █ █ ",
|
||||
"███ ▐█▌ ▐█▐▌▐█▌ ",
|
||||
" ",
|
||||
]);
|
||||
expected.set_style(Rect::new(0, 0, 12, 8), Style::new().red());
|
||||
expected.set_style(Rect::new(0, 8, 20, 8), Style::new().green());
|
||||
expected.set_style(Rect::new(0, 16, 16, 8), Style::new().blue());
|
||||
assert_buffer_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_half_size_symbols() -> Result<()> {
|
||||
assert_eq!(get_symbol_half_size(0, 0, 0, 0), ' ');
|
||||
assert_eq!(get_symbol_half_size(1, 0, 0, 0), '▘');
|
||||
assert_eq!(get_symbol_half_size(0, 1, 0, 0), '▝');
|
||||
assert_eq!(get_symbol_half_size(1, 1, 0, 0), '▀');
|
||||
assert_eq!(get_symbol_half_size(0, 0, 1, 0), '▖');
|
||||
assert_eq!(get_symbol_half_size(1, 0, 1, 0), '▌');
|
||||
assert_eq!(get_symbol_half_size(0, 1, 1, 0), '▞');
|
||||
assert_eq!(get_symbol_half_size(1, 1, 1, 0), '▛');
|
||||
assert_eq!(get_symbol_half_size(0, 0, 0, 1), '▗');
|
||||
assert_eq!(get_symbol_half_size(1, 0, 0, 1), '▚');
|
||||
assert_eq!(get_symbol_half_size(0, 1, 0, 1), '▐');
|
||||
assert_eq!(get_symbol_half_size(1, 1, 0, 1), '▜');
|
||||
assert_eq!(get_symbol_half_size(0, 0, 1, 1), '▄');
|
||||
assert_eq!(get_symbol_half_size(1, 0, 1, 1), '▙');
|
||||
assert_eq!(get_symbol_half_size(0, 1, 1, 1), '▟');
|
||||
assert_eq!(get_symbol_half_size(1, 1, 1, 1), '█');
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_half_size_single_line() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.pixel_size(PixelSize::Quadrant)
|
||||
.lines(vec![Line::from("SingleLine")])
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 40, 4));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines(vec![
|
||||
"▟▀▙ ▀ ▝█ ▜▛ ▀ ",
|
||||
"▜▙ ▝█ █▀▙ ▟▀▟▘ █ ▟▀▙ ▐▌ ▝█ █▀▙ ▟▀▙ ",
|
||||
"▄▝█ █ █ █ ▜▄█ █ █▀▀ ▐▌▗▌ █ █ █ █▀▀ ",
|
||||
"▝▀▘ ▝▀▘ ▀ ▀ ▄▄▛ ▝▀▘ ▝▀▘ ▀▀▀▘▝▀▘ ▀ ▀ ▝▀▘ ",
|
||||
]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_half_size_truncated() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.pixel_size(PixelSize::Quadrant)
|
||||
.lines(vec![Line::from("Truncated")])
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 35, 3));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines(vec![
|
||||
"▛█▜ ▟ ▝█",
|
||||
" █ ▜▟▜▖█ █ █▀▙ ▟▀▙ ▝▀▙ ▝█▀ ▟▀▙ ▗▄█",
|
||||
" █ ▐▌▝▘█ █ █ █ █ ▄ ▟▀█ █▗ █▀▀ █ █",
|
||||
]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_half_size_multiple_lines() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.pixel_size(PixelSize::Quadrant)
|
||||
.lines(vec![Line::from("Multi"), Line::from("Lines")])
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 8));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines(vec![
|
||||
"█▖▟▌ ▝█ ▟ ▀ ",
|
||||
"███▌█ █ █ ▝█▀ ▝█ ",
|
||||
"█▝▐▌█ █ █ █▗ █ ",
|
||||
"▀ ▝▘▝▀▝▘▝▀▘ ▝▘ ▝▀▘ ",
|
||||
"▜▛ ▀ ",
|
||||
"▐▌ ▝█ █▀▙ ▟▀▙ ▟▀▀ ",
|
||||
"▐▌▗▌ █ █ █ █▀▀ ▝▀▙ ",
|
||||
"▀▀▀▘▝▀▘ ▀ ▀ ▝▀▘ ▀▀▘ ",
|
||||
]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_half_size_widget_style() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.pixel_size(PixelSize::Quadrant)
|
||||
.lines(vec![Line::from("Styled")])
|
||||
.style(Style::new().bold())
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 24, 4));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let mut expected = Buffer::with_lines(vec![
|
||||
"▟▀▙ ▟ ▝█ ▝█ ",
|
||||
"▜▙ ▝█▀ █ █ █ ▟▀▙ ▗▄█ ",
|
||||
"▄▝█ █▗ ▜▄█ █ █▀▀ █ █ ",
|
||||
"▝▀▘ ▝▘ ▄▄▛ ▝▀▘ ▝▀▘ ▝▀▝▘",
|
||||
]);
|
||||
expected.set_style(Rect::new(0, 0, 24, 4), Style::new().bold());
|
||||
assert_buffer_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_half_size_line_style() -> Result<()> {
|
||||
let big_text = BigTextBuilder::default()
|
||||
.pixel_size(PixelSize::Quadrant)
|
||||
.lines(vec![
|
||||
Line::from("Red".red()),
|
||||
Line::from("Green".green()),
|
||||
Line::from("Blue".blue()),
|
||||
])
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 12));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let mut expected = Buffer::with_lines(vec![
|
||||
"▜▛▜▖ ▝█ ",
|
||||
"▐▙▟▘▟▀▙ ▗▄█ ",
|
||||
"▐▌▜▖█▀▀ █ █ ",
|
||||
"▀▘▝▘▝▀▘ ▝▀▝▘ ",
|
||||
"▗▛▜▖ ",
|
||||
"█ ▜▟▜▖▟▀▙ ▟▀▙ █▀▙ ",
|
||||
"▜▖▜▌▐▌▝▘█▀▀ █▀▀ █ █ ",
|
||||
" ▀▀▘▀▀ ▝▀▘ ▝▀▘ ▀ ▀ ",
|
||||
"▜▛▜▖▝█ ",
|
||||
"▐▙▟▘ █ █ █ ▟▀▙ ",
|
||||
"▐▌▐▌ █ █ █ █▀▀ ",
|
||||
"▀▀▀ ▝▀▘ ▝▀▝▘▝▀▘ ",
|
||||
]);
|
||||
expected.set_style(Rect::new(0, 0, 12, 4), Style::new().red());
|
||||
expected.set_style(Rect::new(0, 4, 20, 4), Style::new().green());
|
||||
expected.set_style(Rect::new(0, 8, 16, 4), Style::new().blue());
|
||||
assert_buffer_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
use palette::{IntoColor, Okhsv, Srgb};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
use ratatui::prelude::*;
|
||||
|
||||
/// A widget that renders a color swatch of RGB colors.
|
||||
///
|
||||
@@ -9,13 +9,14 @@ use ratatui::{prelude::*, widgets::*};
|
||||
pub struct RgbSwatch;
|
||||
|
||||
impl Widget for RgbSwatch {
|
||||
#[allow(clippy::cast_precision_loss, clippy::similar_names)]
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
for (yi, y) in (area.top()..area.bottom()).enumerate() {
|
||||
let value = area.height as f32 - yi as f32;
|
||||
let value_fg = value / (area.height as f32);
|
||||
let value_bg = (value - 0.5) / (area.height as f32);
|
||||
let value = f32::from(area.height) - yi as f32;
|
||||
let value_fg = value / f32::from(area.height);
|
||||
let value_bg = (value - 0.5) / f32::from(area.height);
|
||||
for (xi, x) in (area.left()..area.right()).enumerate() {
|
||||
let hue = xi as f32 * 360.0 / area.width as f32;
|
||||
let hue = xi as f32 * 360.0 / f32::from(area.width);
|
||||
let fg = color_from_oklab(hue, Okhsv::max_saturation(), value_fg);
|
||||
let bg = color_from_oklab(hue, Okhsv::max_saturation(), value_bg);
|
||||
buf.get_mut(x, y).set_char('▀').set_fg(fg).set_bg(bg);
|
||||
@@ -24,7 +25,7 @@ impl Widget for RgbSwatch {
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a hue and value into an RGB color via the OkLab color space.
|
||||
/// Convert a hue and value into an RGB color via the Oklab color space.
|
||||
///
|
||||
/// See <https://bottosson.github.io/posts/oklab/> for more details.
|
||||
pub fn color_from_oklab(hue: f32, saturation: f32, value: f32) -> Color {
|
||||
|
||||
140
examples/demo2/destroy.rs
Normal file
140
examples/demo2/destroy.rs
Normal file
@@ -0,0 +1,140 @@
|
||||
use rand::Rng;
|
||||
use rand_chacha::rand_core::SeedableRng;
|
||||
use ratatui::{buffer::Cell, layout::Flex, prelude::*};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::big_text::{BigTextBuilder, PixelSize};
|
||||
|
||||
/// delay the start of the animation so it doesn't start immediately
|
||||
const DELAY: usize = 240;
|
||||
/// higher means more pixels per frame are modified in the animation
|
||||
const DRIP_SPEED: usize = 50;
|
||||
/// delay the start of the text animation so it doesn't start immediately after the initial delay
|
||||
const TEXT_DELAY: usize = 240;
|
||||
|
||||
/// Destroy mode activated by pressing `d`
|
||||
pub fn destroy(frame: &mut Frame<'_>) {
|
||||
let frame_count = frame.count().saturating_sub(DELAY);
|
||||
if frame_count == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let area = frame.size();
|
||||
let buf = frame.buffer_mut();
|
||||
|
||||
drip(frame_count, area, buf);
|
||||
text(frame_count, area, buf);
|
||||
}
|
||||
|
||||
/// Move a bunch of random pixels down one row.
|
||||
///
|
||||
/// Each pick some random pixels and move them each down one row. This is a very inefficient way to
|
||||
/// do this, but it works well enough for this demo.
|
||||
#[allow(
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::cast_precision_loss,
|
||||
clippy::cast_sign_loss
|
||||
)]
|
||||
fn drip(frame_count: usize, area: Rect, buf: &mut Buffer) {
|
||||
// a seeded rng as we have to move the same random pixels each frame
|
||||
let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(10);
|
||||
let ramp_frames = 450;
|
||||
let fractional_speed = frame_count as f64 / f64::from(ramp_frames);
|
||||
let variable_speed = DRIP_SPEED as f64 * fractional_speed * fractional_speed * fractional_speed;
|
||||
let pixel_count = (frame_count as f64 * variable_speed).floor() as usize;
|
||||
for _ in 0..pixel_count {
|
||||
let src_x = rng.gen_range(0..area.width);
|
||||
let src_y = rng.gen_range(1..area.height - 2);
|
||||
let src = buf.get_mut(src_x, src_y).clone();
|
||||
// 1% of the time, move a blank or pixel (10:1) to the top line of the screen
|
||||
if rng.gen_ratio(1, 100) {
|
||||
let dest_x = rng
|
||||
.gen_range(src_x.saturating_sub(5)..src_x.saturating_add(5))
|
||||
.clamp(area.left(), area.right() - 1);
|
||||
let dest_y = area.top() + 1;
|
||||
|
||||
let dest = buf.get_mut(dest_x, dest_y);
|
||||
// copy the cell to the new location about 1/10 of the time blank out the cell the rest
|
||||
// of the time. This has the effect of gradually removing the pixels from the screen.
|
||||
if rng.gen_ratio(1, 10) {
|
||||
*dest = src;
|
||||
} else {
|
||||
*dest = Cell::default();
|
||||
}
|
||||
} else {
|
||||
// move the pixel down one row
|
||||
let dest_x = src_x;
|
||||
let dest_y = src_y.saturating_add(1).min(area.bottom() - 2);
|
||||
// copy the cell to the new location
|
||||
let dest = buf.get_mut(dest_x, dest_y);
|
||||
*dest = src;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// draw some text fading in and out from black to red and back
|
||||
#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
|
||||
fn text(frame_count: usize, area: Rect, buf: &mut Buffer) {
|
||||
let sub_frame = frame_count.saturating_sub(TEXT_DELAY);
|
||||
if sub_frame == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let line = "RATATUI";
|
||||
let big_text = BigTextBuilder::default()
|
||||
.lines([line.into()])
|
||||
.pixel_size(PixelSize::Full)
|
||||
.style(Style::new().fg(Color::Rgb(255, 0, 0)))
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
// the font size is 8x8 for each character and we have 1 line
|
||||
let area = centered_rect(area, line.width() as u16 * 8, 8);
|
||||
|
||||
let mask_buf = &mut Buffer::empty(area);
|
||||
big_text.render(area, mask_buf);
|
||||
|
||||
let percentage = (sub_frame as f64 / 480.0).clamp(0.0, 1.0);
|
||||
|
||||
for row in area.rows() {
|
||||
for col in row.columns() {
|
||||
let cell = buf.get_mut(col.x, col.y);
|
||||
let mask_cell = mask_buf.get(col.x, col.y);
|
||||
cell.set_symbol(mask_cell.symbol());
|
||||
|
||||
// blend the mask cell color with the cell color
|
||||
let cell_color = cell.style().bg.unwrap_or(Color::Rgb(0, 0, 0));
|
||||
let mask_color = mask_cell.style().fg.unwrap_or(Color::Rgb(255, 0, 0));
|
||||
|
||||
let color = blend(mask_color, cell_color, percentage);
|
||||
cell.set_style(Style::new().fg(color));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn blend(mask_color: Color, cell_color: Color, percentage: f64) -> Color {
|
||||
let Color::Rgb(mask_red, mask_green, mask_blue) = mask_color else {
|
||||
return mask_color;
|
||||
};
|
||||
let Color::Rgb(cell_red, cell_green, cell_blue) = cell_color else {
|
||||
return mask_color;
|
||||
};
|
||||
|
||||
let remain = 1.0 - percentage;
|
||||
|
||||
let red = f64::from(mask_red).mul_add(percentage, f64::from(cell_red) * remain);
|
||||
let green = f64::from(mask_green).mul_add(percentage, f64::from(cell_green) * remain);
|
||||
let blue = f64::from(mask_blue).mul_add(percentage, f64::from(cell_blue) * remain);
|
||||
|
||||
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
|
||||
Color::Rgb(red as u8, green as u8, blue as u8)
|
||||
}
|
||||
|
||||
/// a centered rect of the given size
|
||||
fn centered_rect(area: Rect, width: u16, height: u16) -> Rect {
|
||||
let horizontal = Layout::horizontal([width]).flex(Flex::Center);
|
||||
let vertical = Layout::vertical([height]).flex(Flex::Center);
|
||||
let [area] = vertical.areas(area);
|
||||
let [area] = horizontal.areas(area);
|
||||
area
|
||||
}
|
||||
18
examples/demo2/errors.rs
Normal file
18
examples/demo2/errors.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
use color_eyre::{config::HookBuilder, Result};
|
||||
|
||||
use crate::term;
|
||||
|
||||
pub fn init_hooks() -> Result<()> {
|
||||
let (panic, error) = HookBuilder::default().into_hooks();
|
||||
let panic = panic.into_panic_hook();
|
||||
let error = error.into_eyre_hook();
|
||||
color_eyre::eyre::set_hook(Box::new(move |e| {
|
||||
let _ = term::restore();
|
||||
error(e)
|
||||
}))?;
|
||||
std::panic::set_hook(Box::new(move |info| {
|
||||
let _ = term::restore();
|
||||
panic(info);
|
||||
}));
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,17 +1,45 @@
|
||||
use anyhow::Result;
|
||||
pub use app::*;
|
||||
pub use colors::*;
|
||||
pub use root::*;
|
||||
pub use term::*;
|
||||
pub use theme::*;
|
||||
//! # [Ratatui] Demo2 example
|
||||
//!
|
||||
//! The latest version of this example is available in the [examples] folder in the repository.
|
||||
//!
|
||||
//! Please note that the examples are designed to be run against the `main` branch of the Github
|
||||
//! repository. This means that you may not be able to compile with the latest release version on
|
||||
//! crates.io, or the one that you have installed locally.
|
||||
//!
|
||||
//! See the [examples readme] for more information on finding examples that match the version of the
|
||||
//! library you are using.
|
||||
//!
|
||||
//! [Ratatui]: https://github.com/ratatui-org/ratatui
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
#![allow(
|
||||
clippy::enum_glob_use,
|
||||
clippy::missing_errors_doc,
|
||||
clippy::module_name_repetitions,
|
||||
clippy::must_use_candidate,
|
||||
clippy::wildcard_imports
|
||||
)]
|
||||
|
||||
mod app;
|
||||
mod big_text;
|
||||
mod colors;
|
||||
mod root;
|
||||
mod destroy;
|
||||
mod errors;
|
||||
mod tabs;
|
||||
mod term;
|
||||
mod theme;
|
||||
|
||||
pub use app::*;
|
||||
use color_eyre::Result;
|
||||
pub use colors::*;
|
||||
pub use term::*;
|
||||
pub use theme::*;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
App::run()
|
||||
errors::init_hooks()?;
|
||||
let terminal = &mut term::init()?;
|
||||
App::new().run(terminal)?;
|
||||
term::restore()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use itertools::Itertools;
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
use crate::{tabs::*, AppContext, THEME};
|
||||
|
||||
pub struct Root<'a> {
|
||||
context: &'a AppContext,
|
||||
}
|
||||
|
||||
impl<'a> Root<'a> {
|
||||
pub fn new(context: &'a AppContext) -> Self {
|
||||
Root { context }
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Root<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
Block::new().style(THEME.root).render(area, buf);
|
||||
let area = layout(area, Direction::Vertical, vec![1, 0, 1]);
|
||||
self.render_title_bar(area[0], buf);
|
||||
self.render_selected_tab(area[1], buf);
|
||||
self.render_bottom_bar(area[2], buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl Root<'_> {
|
||||
fn render_title_bar(&self, area: Rect, buf: &mut Buffer) {
|
||||
let area = layout(area, Direction::Horizontal, vec![0, 45]);
|
||||
|
||||
Paragraph::new(Span::styled("Ratatui", THEME.app_title)).render(area[0], buf);
|
||||
let titles = vec!["", " Recipe ", " Email ", " Traceroute ", " Weather "];
|
||||
Tabs::new(titles)
|
||||
.style(THEME.tabs)
|
||||
.highlight_style(THEME.tabs_selected)
|
||||
.select(self.context.tab_index)
|
||||
.divider("")
|
||||
.render(area[1], buf);
|
||||
}
|
||||
|
||||
fn render_selected_tab(&self, area: Rect, buf: &mut Buffer) {
|
||||
let row_index = self.context.row_index;
|
||||
match self.context.tab_index {
|
||||
0 => AboutTab::new(row_index).render(area, buf),
|
||||
1 => RecipeTab::new(row_index).render(area, buf),
|
||||
2 => EmailTab::new(row_index).render(area, buf),
|
||||
3 => TracerouteTab::new(row_index).render(area, buf),
|
||||
4 => WeatherTab::new(row_index).render(area, buf),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
}
|
||||
|
||||
fn render_bottom_bar(&self, area: Rect, buf: &mut Buffer) {
|
||||
let keys = [
|
||||
("Q/Esc", "Quit"),
|
||||
("Tab", "Next Tab"),
|
||||
("↑/k", "Up"),
|
||||
("↓/j", "Down"),
|
||||
];
|
||||
let spans = keys
|
||||
.iter()
|
||||
.flat_map(|(key, desc)| {
|
||||
let key = Span::styled(format!(" {} ", key), THEME.key_binding.key);
|
||||
let desc = Span::styled(format!(" {} ", desc), THEME.key_binding.description);
|
||||
[key, desc]
|
||||
})
|
||||
.collect_vec();
|
||||
Paragraph::new(Line::from(spans))
|
||||
.alignment(Alignment::Center)
|
||||
.fg(Color::Indexed(236))
|
||||
.bg(Color::Indexed(232))
|
||||
.render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
/// simple helper method to split an area into multiple sub-areas
|
||||
pub fn layout(area: Rect, direction: Direction, heights: Vec<u16>) -> Rc<[Rect]> {
|
||||
let constraints = heights
|
||||
.iter()
|
||||
.map(|&h| {
|
||||
if h > 0 {
|
||||
Constraint::Length(h)
|
||||
} else {
|
||||
Constraint::Min(0)
|
||||
}
|
||||
})
|
||||
.collect_vec();
|
||||
Layout::default()
|
||||
.direction(direction)
|
||||
.constraints(constraints)
|
||||
.split(area)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
use itertools::Itertools;
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
use crate::{layout, RgbSwatch, THEME};
|
||||
use crate::{RgbSwatch, THEME};
|
||||
|
||||
const RATATUI_LOGO: [&str; 32] = [
|
||||
" ███ ",
|
||||
@@ -38,22 +38,28 @@ const RATATUI_LOGO: [&str; 32] = [
|
||||
" █xxxxxxxxxxxxxxxxxxxxx█ █ ",
|
||||
];
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
pub struct AboutTab {
|
||||
selected_row: usize,
|
||||
row_index: usize,
|
||||
}
|
||||
|
||||
impl AboutTab {
|
||||
pub fn new(selected_row: usize) -> Self {
|
||||
Self { selected_row }
|
||||
pub fn prev_row(&mut self) {
|
||||
self.row_index = self.row_index.saturating_sub(1);
|
||||
}
|
||||
|
||||
pub fn next_row(&mut self) {
|
||||
self.row_index = self.row_index.saturating_add(1);
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for AboutTab {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
RgbSwatch.render(area, buf);
|
||||
let area = layout(area, Direction::Horizontal, vec![34, 0]);
|
||||
render_crate_description(area[1], buf);
|
||||
render_logo(self.selected_row, area[0], buf);
|
||||
let horizontal = Layout::horizontal([Constraint::Length(34), Constraint::Min(0)]);
|
||||
let [description, logo] = horizontal.areas(area);
|
||||
render_crate_description(description, buf);
|
||||
render_logo(self.row_index, logo, buf);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,10 +97,11 @@ fn render_crate_description(area: Rect, buf: &mut Buffer) {
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
/// Use half block characters to render a logo based on the RATATUI_LOGO const.
|
||||
/// Use half block characters to render a logo based on the `RATATUI_LOGO` const.
|
||||
///
|
||||
/// The logo is rendered in three colors, one for the rat, one for the terminal, and one for the
|
||||
/// rat's eye. The eye color alternates between two colors based on the selected row.
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
pub fn render_logo(selected_row: usize, area: Rect, buf: &mut Buffer) {
|
||||
let eye_color = if selected_row % 2 == 0 {
|
||||
THEME.logo.rat_eye
|
||||
@@ -116,6 +123,7 @@ pub fn render_logo(selected_row: usize, area: Rect, buf: &mut Buffer) {
|
||||
('█', '█') => {
|
||||
cell.set_char('█');
|
||||
cell.fg = rat_color;
|
||||
cell.bg = rat_color;
|
||||
}
|
||||
('█', ' ') => {
|
||||
cell.set_char('▀');
|
||||
|
||||
@@ -2,7 +2,7 @@ use itertools::Itertools;
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{layout, RgbSwatch, THEME};
|
||||
use crate::{RgbSwatch, THEME};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Email {
|
||||
@@ -39,16 +39,20 @@ const EMAILS: &[Email] = &[
|
||||
},
|
||||
];
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
pub struct EmailTab {
|
||||
selected_index: usize,
|
||||
row_index: usize,
|
||||
}
|
||||
|
||||
impl EmailTab {
|
||||
pub fn new(selected_index: usize) -> Self {
|
||||
Self {
|
||||
selected_index: selected_index % EMAILS.len(),
|
||||
}
|
||||
/// Select the previous email (with wrap around).
|
||||
pub fn prev(&mut self) {
|
||||
self.row_index = self.row_index.saturating_add(EMAILS.len() - 1) % EMAILS.len();
|
||||
}
|
||||
|
||||
/// Select the next email (with wrap around).
|
||||
pub fn next(&mut self) {
|
||||
self.row_index = self.row_index.saturating_add(1) % EMAILS.len();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,20 +64,22 @@ impl Widget for EmailTab {
|
||||
horizontal: 2,
|
||||
});
|
||||
Clear.render(area, buf);
|
||||
let area = layout(area, Direction::Vertical, vec![5, 0]);
|
||||
render_inbox(self.selected_index, area[0], buf);
|
||||
render_email(self.selected_index, area[1], buf);
|
||||
let vertical = Layout::vertical([Constraint::Length(5), Constraint::Min(0)]);
|
||||
let [inbox, email] = vertical.areas(area);
|
||||
render_inbox(self.row_index, inbox, buf);
|
||||
render_email(self.row_index, email, buf);
|
||||
}
|
||||
}
|
||||
fn render_inbox(selected_index: usize, area: Rect, buf: &mut Buffer) {
|
||||
let area = layout(area, Direction::Vertical, vec![1, 0]);
|
||||
let vertical = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
|
||||
let [tabs, inbox] = vertical.areas(area);
|
||||
let theme = THEME.email;
|
||||
Tabs::new(vec![" Inbox ", " Sent ", " Drafts "])
|
||||
.style(theme.tabs)
|
||||
.highlight_style(theme.tabs_selected)
|
||||
.select(0)
|
||||
.divider("")
|
||||
.render(area[0], buf);
|
||||
.render(tabs, buf);
|
||||
|
||||
let highlight_symbol = ">>";
|
||||
let from_width = EMAILS
|
||||
@@ -94,7 +100,7 @@ fn render_inbox(selected_index: usize, area: Rect, buf: &mut Buffer) {
|
||||
.style(theme.inbox)
|
||||
.highlight_style(theme.selected_item)
|
||||
.highlight_symbol(highlight_symbol),
|
||||
area[1],
|
||||
inbox,
|
||||
buf,
|
||||
&mut state,
|
||||
);
|
||||
@@ -106,7 +112,7 @@ fn render_inbox(selected_index: usize, area: Rect, buf: &mut Buffer) {
|
||||
.end_symbol(None)
|
||||
.track_symbol(None)
|
||||
.thumb_symbol("▐")
|
||||
.render(area[1], buf, &mut scrollbar_state);
|
||||
.render(inbox, buf, &mut scrollbar_state);
|
||||
}
|
||||
|
||||
fn render_email(selected_index: usize, area: Rect, buf: &mut Buffer) {
|
||||
@@ -120,7 +126,8 @@ fn render_email(selected_index: usize, area: Rect, buf: &mut Buffer) {
|
||||
let inner = block.inner(area);
|
||||
block.render(area, buf);
|
||||
if let Some(email) = email {
|
||||
let area = layout(inner, Direction::Vertical, vec![3, 0]);
|
||||
let vertical = Layout::vertical([Constraint::Length(3), Constraint::Min(0)]);
|
||||
let [headers_area, body_area] = vertical.areas(inner);
|
||||
let headers = vec![
|
||||
Line::from(vec![
|
||||
"From: ".set_style(theme.header),
|
||||
@@ -134,9 +141,11 @@ fn render_email(selected_index: usize, area: Rect, buf: &mut Buffer) {
|
||||
];
|
||||
Paragraph::new(headers)
|
||||
.style(theme.body)
|
||||
.render(area[0], buf);
|
||||
.render(headers_area, buf);
|
||||
let body = email.body.lines().map(Line::from).collect_vec();
|
||||
Paragraph::new(body).style(theme.body).render(area[1], buf);
|
||||
Paragraph::new(body)
|
||||
.style(theme.body)
|
||||
.render(body_area, buf);
|
||||
} else {
|
||||
Paragraph::new("No email selected").render(inner, buf);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use itertools::Itertools;
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
use crate::{layout, RgbSwatch, THEME};
|
||||
use crate::{RgbSwatch, THEME};
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
struct Ingredient {
|
||||
@@ -10,6 +10,7 @@ struct Ingredient {
|
||||
}
|
||||
|
||||
impl Ingredient {
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
fn height(&self) -> u16 {
|
||||
self.name.lines().count() as u16
|
||||
}
|
||||
@@ -84,16 +85,20 @@ const INGREDIENTS: &[Ingredient] = &[
|
||||
},
|
||||
];
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct RecipeTab {
|
||||
selected_row: usize,
|
||||
row_index: usize,
|
||||
}
|
||||
|
||||
impl RecipeTab {
|
||||
pub fn new(selected_row: usize) -> Self {
|
||||
Self {
|
||||
selected_row: selected_row % INGREDIENTS.len(),
|
||||
}
|
||||
/// Select the previous item in the ingredients list (with wrap around)
|
||||
pub fn prev(&mut self) {
|
||||
self.row_index = self.row_index.saturating_add(INGREDIENTS.len() - 1) % INGREDIENTS.len();
|
||||
}
|
||||
|
||||
/// Select the next item in the ingredients list (with wrap around)
|
||||
pub fn next(&mut self) {
|
||||
self.row_index = self.row_index.saturating_add(1) % INGREDIENTS.len();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,16 +122,17 @@ impl Widget for RecipeTab {
|
||||
height: area.height - 3,
|
||||
..area
|
||||
};
|
||||
render_scrollbar(self.selected_row, scrollbar_area, buf);
|
||||
render_scrollbar(self.row_index, scrollbar_area, buf);
|
||||
|
||||
let area = area.inner(&Margin {
|
||||
horizontal: 2,
|
||||
vertical: 1,
|
||||
});
|
||||
let area = layout(area, Direction::Horizontal, vec![44, 0]);
|
||||
let [recipe, ingredients] =
|
||||
Layout::horizontal([Constraint::Length(44), Constraint::Min(0)]).areas(area);
|
||||
|
||||
render_recipe(area[0], buf);
|
||||
render_ingredients(self.selected_row, area[1], buf);
|
||||
render_recipe(recipe, buf);
|
||||
render_ingredients(self.row_index, ingredients, buf);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,7 +149,7 @@ fn render_recipe(area: Rect, buf: &mut Buffer) {
|
||||
|
||||
fn render_ingredients(selected_row: usize, area: Rect, buf: &mut Buffer) {
|
||||
let mut state = TableState::default().with_selected(Some(selected_row));
|
||||
let rows = INGREDIENTS.iter().map(|&i| i.into()).collect_vec();
|
||||
let rows = INGREDIENTS.iter().copied();
|
||||
let theme = THEME.recipe;
|
||||
StatefulWidget::render(
|
||||
Table::new(rows, [Constraint::Length(7), Constraint::Length(30)])
|
||||
@@ -166,5 +172,5 @@ fn render_scrollbar(position: usize, area: Rect, buf: &mut Buffer) {
|
||||
.end_symbol(None)
|
||||
.track_symbol(None)
|
||||
.thumb_symbol("▐")
|
||||
.render(area, buf, &mut state)
|
||||
.render(area, buf, &mut state);
|
||||
}
|
||||
|
||||
@@ -4,18 +4,22 @@ use ratatui::{
|
||||
widgets::{canvas::*, *},
|
||||
};
|
||||
|
||||
use crate::{layout, RgbSwatch, THEME};
|
||||
use crate::{RgbSwatch, THEME};
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
pub struct TracerouteTab {
|
||||
selected_row: usize,
|
||||
row_index: usize,
|
||||
}
|
||||
|
||||
impl TracerouteTab {
|
||||
pub fn new(selected_row: usize) -> Self {
|
||||
Self {
|
||||
selected_row: selected_row % HOPS.len(),
|
||||
}
|
||||
/// Select the previous row (with wrap around).
|
||||
pub fn prev_row(&mut self) {
|
||||
self.row_index = self.row_index.saturating_add(HOPS.len() - 1) % HOPS.len();
|
||||
}
|
||||
|
||||
/// Select the next row (with wrap around).
|
||||
pub fn next_row(&mut self) {
|
||||
self.row_index = self.row_index.saturating_add(1) % HOPS.len();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,14 +32,14 @@ impl Widget for TracerouteTab {
|
||||
});
|
||||
Clear.render(area, buf);
|
||||
Block::new().style(THEME.content).render(area, buf);
|
||||
let area = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)])
|
||||
.split(area);
|
||||
let left_area = layout(area[0], Direction::Vertical, vec![0, 3]);
|
||||
render_hops(self.selected_row, left_area[0], buf);
|
||||
render_ping(self.selected_row, left_area[1], buf);
|
||||
render_map(self.selected_row, area[1], buf);
|
||||
let horizontal = Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]);
|
||||
let vertical = Layout::vertical([Constraint::Min(0), Constraint::Length(3)]);
|
||||
let [left, map] = horizontal.areas(area);
|
||||
let [hops, pings] = vertical.areas(left);
|
||||
|
||||
render_hops(self.row_index, hops, buf);
|
||||
render_ping(self.row_index, pings, buf);
|
||||
render_map(self.row_index, map, buf);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,15 +6,22 @@ use ratatui::{
|
||||
};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
use crate::{color_from_oklab, layout, RgbSwatch, THEME};
|
||||
use crate::{color_from_oklab, RgbSwatch, THEME};
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
pub struct WeatherTab {
|
||||
pub selected_row: usize,
|
||||
pub download_progress: usize,
|
||||
}
|
||||
|
||||
impl WeatherTab {
|
||||
pub fn new(selected_row: usize) -> Self {
|
||||
Self { selected_row }
|
||||
/// Simulate a download indicator by decrementing the row index.
|
||||
pub fn prev(&mut self) {
|
||||
self.download_progress = self.download_progress.saturating_sub(1);
|
||||
}
|
||||
|
||||
/// Simulate a download indicator by incrementing the row index.
|
||||
pub fn next(&mut self) {
|
||||
self.download_progress = self.download_progress.saturating_add(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,14 +39,21 @@ impl Widget for WeatherTab {
|
||||
horizontal: 2,
|
||||
vertical: 1,
|
||||
});
|
||||
let area = layout(area, Direction::Vertical, vec![0, 1, 1]);
|
||||
render_gauges(self.selected_row, area[2], buf);
|
||||
let [main, _, gauges] = Layout::vertical([
|
||||
Constraint::Min(0),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
])
|
||||
.areas(area);
|
||||
let [calendar, charts] =
|
||||
Layout::horizontal([Constraint::Length(23), Constraint::Min(0)]).areas(main);
|
||||
let [simple, horizontal] =
|
||||
Layout::vertical([Constraint::Length(29), Constraint::Min(0)]).areas(charts);
|
||||
|
||||
let area = layout(area[0], Direction::Horizontal, vec![23, 0]);
|
||||
render_calendar(area[0], buf);
|
||||
let area = layout(area[1], Direction::Horizontal, vec![29, 0]);
|
||||
render_simple_barchart(area[0], buf);
|
||||
render_horizontal_barchart(area[1], buf);
|
||||
render_calendar(calendar, buf);
|
||||
render_simple_barchart(simple, buf);
|
||||
render_horizontal_barchart(horizontal, buf);
|
||||
render_gauge(self.download_progress, gauges, buf);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +84,7 @@ fn render_simple_barchart(area: Rect, buf: &mut Buffer) {
|
||||
// This doesn't actually render correctly as the text is too wide for the bar
|
||||
// See https://github.com/ratatui-org/ratatui/issues/513 for more info
|
||||
// (the demo GIFs hack around this by hacking the calculation in bars.rs)
|
||||
.text_value(format!("{}°", value))
|
||||
.text_value(format!("{value}°"))
|
||||
.style(if value > 70 {
|
||||
Style::new().fg(Color::Red)
|
||||
} else {
|
||||
@@ -114,12 +128,14 @@ fn render_horizontal_barchart(area: Rect, buf: &mut Buffer) {
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
pub fn render_gauges(progress: usize, area: Rect, buf: &mut Buffer) {
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
pub fn render_gauge(progress: usize, area: Rect, buf: &mut Buffer) {
|
||||
let percent = (progress * 3).min(100) as f64;
|
||||
|
||||
render_line_gauge(percent, area, buf);
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
fn render_line_gauge(percent: f64, area: Rect, buf: &mut Buffer) {
|
||||
// cycle color hue based on the percent for a neat effect yellow -> red
|
||||
let hue = 90.0 - (percent as f32 * 0.6);
|
||||
@@ -127,7 +143,7 @@ fn render_line_gauge(percent: f64, area: Rect, buf: &mut Buffer) {
|
||||
let fg = color_from_oklab(hue, Okhsv::max_saturation(), value);
|
||||
let bg = color_from_oklab(hue, Okhsv::max_saturation(), value * 0.5);
|
||||
let label = if percent < 100.0 {
|
||||
format!("Downloading: {}%", percent)
|
||||
format!("Downloading: {percent}%")
|
||||
} else {
|
||||
"Download Complete!".into()
|
||||
};
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use std::{
|
||||
io::{self, stdout, Stdout},
|
||||
ops::{Deref, DerefMut},
|
||||
io::{self, stdout},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use color_eyre::{eyre::WrapErr, Result};
|
||||
use crossterm::{
|
||||
event::{self, Event},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
@@ -12,60 +11,32 @@ use crossterm::{
|
||||
};
|
||||
use ratatui::prelude::*;
|
||||
|
||||
/// A wrapper around the terminal that handles setting up and tearing down the terminal
|
||||
/// and provides a helper method to read events from the terminal.
|
||||
#[derive(Debug)]
|
||||
pub struct Term {
|
||||
terminal: Terminal<CrosstermBackend<Stdout>>,
|
||||
pub fn init() -> Result<Terminal<impl Backend>> {
|
||||
// this size is to match the size of the terminal when running the demo
|
||||
// using vhs in a 1280x640 sized window (github social preview size)
|
||||
let options = TerminalOptions {
|
||||
viewport: Viewport::Fixed(Rect::new(0, 0, 81, 18)),
|
||||
};
|
||||
let terminal = Terminal::with_options(CrosstermBackend::new(io::stdout()), options)?;
|
||||
enable_raw_mode().context("enable raw mode")?;
|
||||
stdout()
|
||||
.execute(EnterAlternateScreen)
|
||||
.wrap_err("enter alternate screen")?;
|
||||
Ok(terminal)
|
||||
}
|
||||
|
||||
impl Term {
|
||||
pub fn start() -> Result<Self> {
|
||||
// this size is to match the size of the terminal when running the demo
|
||||
// using vhs in a 1280x640 sized window (github social preview size)
|
||||
let options = TerminalOptions {
|
||||
viewport: Viewport::Fixed(Rect::new(0, 0, 81, 18)),
|
||||
};
|
||||
let terminal = Terminal::with_options(CrosstermBackend::new(io::stdout()), options)?;
|
||||
enable_raw_mode().context("enable raw mode")?;
|
||||
stdout()
|
||||
.execute(EnterAlternateScreen)
|
||||
.context("enter alternate screen")?;
|
||||
Ok(Self { terminal })
|
||||
}
|
||||
|
||||
pub fn stop() -> Result<()> {
|
||||
disable_raw_mode().context("disable raw mode")?;
|
||||
stdout()
|
||||
.execute(LeaveAlternateScreen)
|
||||
.context("leave alternate screen")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn next_event(timeout: Duration) -> io::Result<Option<Event>> {
|
||||
if !event::poll(timeout)? {
|
||||
return Ok(None);
|
||||
}
|
||||
let event = event::read()?;
|
||||
Ok(Some(event))
|
||||
}
|
||||
pub fn restore() -> Result<()> {
|
||||
disable_raw_mode().context("disable raw mode")?;
|
||||
stdout()
|
||||
.execute(LeaveAlternateScreen)
|
||||
.wrap_err("leave alternate screen")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl Deref for Term {
|
||||
type Target = Terminal<CrosstermBackend<Stdout>>;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.terminal
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for Term {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.terminal
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Term {
|
||||
fn drop(&mut self) {
|
||||
let _ = Term::stop();
|
||||
pub fn next_event(timeout: Duration) -> Result<Option<Event>> {
|
||||
if !event::poll(timeout)? {
|
||||
return Ok(None);
|
||||
}
|
||||
let event = event::read()?;
|
||||
Ok(Some(event))
|
||||
}
|
||||
|
||||
@@ -128,9 +128,9 @@ const LIGHT_BLUE: Color = Color::Rgb(64, 96, 192);
|
||||
const LIGHT_YELLOW: Color = Color::Rgb(192, 192, 96);
|
||||
const LIGHT_GREEN: Color = Color::Rgb(64, 192, 96);
|
||||
const LIGHT_RED: Color = Color::Rgb(192, 96, 96);
|
||||
const RED: Color = Color::Indexed(160);
|
||||
const BLACK: Color = Color::Indexed(232); // not really black, often #080808
|
||||
const DARK_GRAY: Color = Color::Indexed(238);
|
||||
const MID_GRAY: Color = Color::Indexed(244);
|
||||
const LIGHT_GRAY: Color = Color::Indexed(250);
|
||||
const WHITE: Color = Color::Indexed(255); // not really white, often #eeeeee
|
||||
const RED: Color = Color::Rgb(215, 0, 0);
|
||||
const BLACK: Color = Color::Rgb(8, 8, 8); // not really black, often #080808
|
||||
const DARK_GRAY: Color = Color::Rgb(68, 68, 68);
|
||||
const MID_GRAY: Color = Color::Rgb(128, 128, 128);
|
||||
const LIGHT_GRAY: Color = Color::Rgb(188, 188, 188);
|
||||
const WHITE: Color = Color::Rgb(238, 238, 238); // not really white, often #eeeeee
|
||||
|
||||
@@ -1,12 +1,30 @@
|
||||
//! # [Ratatui] Docs.rs example
|
||||
//!
|
||||
//! The latest version of this example is available in the [examples] folder in the repository.
|
||||
//!
|
||||
//! Please note that the examples are designed to be run against the `main` branch of the Github
|
||||
//! repository. This means that you may not be able to compile with the latest release version on
|
||||
//! crates.io, or the one that you have installed locally.
|
||||
//!
|
||||
//! See the [examples readme] for more information on finding examples that match the version of the
|
||||
//! library you are using.
|
||||
//!
|
||||
//! [Ratatui]: https://github.com/ratatui-org/ratatui
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
use std::io::{self, stdout};
|
||||
|
||||
use crossterm::{
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{Block, Borders, Paragraph},
|
||||
};
|
||||
|
||||
/// Example code for libr.rs
|
||||
/// Example code for lib.rs
|
||||
///
|
||||
/// When cargo-rdme supports doc comments that import from code, this will be imported
|
||||
/// rather than copied to the lib.rs file.
|
||||
@@ -19,7 +37,6 @@ fn main() -> io::Result<()> {
|
||||
let mut should_quit = false;
|
||||
while !should_quit {
|
||||
terminal.draw(match arg.as_str() {
|
||||
"hello_world" => hello_world,
|
||||
"layout" => layout,
|
||||
"styling" => styling,
|
||||
_ => hello_world,
|
||||
@@ -53,48 +70,36 @@ fn handle_events() -> io::Result<bool> {
|
||||
}
|
||||
|
||||
fn layout(frame: &mut Frame) {
|
||||
let main_layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(1),
|
||||
Constraint::Min(0),
|
||||
Constraint::Length(1),
|
||||
])
|
||||
.split(frame.size());
|
||||
let vertical = Layout::vertical([
|
||||
Constraint::Length(1),
|
||||
Constraint::Min(0),
|
||||
Constraint::Length(1),
|
||||
]);
|
||||
let horizontal = Layout::horizontal([Constraint::Ratio(1, 2); 2]);
|
||||
let [title_bar, main_area, status_bar] = vertical.areas(frame.size());
|
||||
let [left, right] = horizontal.areas(main_area);
|
||||
|
||||
frame.render_widget(
|
||||
Block::new().borders(Borders::TOP).title("Title Bar"),
|
||||
main_layout[0],
|
||||
title_bar,
|
||||
);
|
||||
frame.render_widget(
|
||||
Block::new().borders(Borders::TOP).title("Status Bar"),
|
||||
main_layout[2],
|
||||
);
|
||||
|
||||
let inner_layout = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
.split(main_layout[1]);
|
||||
frame.render_widget(
|
||||
Block::default().borders(Borders::ALL).title("Left"),
|
||||
inner_layout[0],
|
||||
);
|
||||
frame.render_widget(
|
||||
Block::default().borders(Borders::ALL).title("Right"),
|
||||
inner_layout[1],
|
||||
status_bar,
|
||||
);
|
||||
frame.render_widget(Block::default().borders(Borders::ALL).title("Left"), left);
|
||||
frame.render_widget(Block::default().borders(Borders::ALL).title("Right"), right);
|
||||
}
|
||||
|
||||
fn styling(frame: &mut Frame) {
|
||||
let areas = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
Constraint::Min(0),
|
||||
])
|
||||
.split(frame.size());
|
||||
let areas = Layout::vertical([
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
Constraint::Min(0),
|
||||
])
|
||||
.split(frame.size());
|
||||
|
||||
let span1 = Span::raw("Hello ");
|
||||
let span2 = Span::styled(
|
||||
|
||||
559
examples/flex.rs
Normal file
559
examples/flex.rs
Normal file
@@ -0,0 +1,559 @@
|
||||
//! # [Ratatui] Flex example
|
||||
//!
|
||||
//! The latest version of this example is available in the [examples] folder in the repository.
|
||||
//!
|
||||
//! Please note that the examples are designed to be run against the `main` branch of the Github
|
||||
//! repository. This means that you may not be able to compile with the latest release version on
|
||||
//! crates.io, or the one that you have installed locally.
|
||||
//!
|
||||
//! See the [examples readme] for more information on finding examples that match the version of the
|
||||
//! library you are using.
|
||||
//!
|
||||
//! [Ratatui]: https://github.com/ratatui-org/ratatui
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
#![allow(clippy::enum_glob_use, clippy::wildcard_imports)]
|
||||
|
||||
use std::io::{self, stdout};
|
||||
|
||||
use color_eyre::{config::HookBuilder, Result};
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode, KeyEventKind},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
};
|
||||
use ratatui::{
|
||||
layout::{Constraint::*, Flex},
|
||||
prelude::*,
|
||||
style::palette::tailwind,
|
||||
symbols::line,
|
||||
widgets::{block::Title, *},
|
||||
};
|
||||
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
|
||||
|
||||
const EXAMPLE_DATA: &[(&str, &[Constraint])] = &[
|
||||
(
|
||||
"Min(u16) takes any excess space always",
|
||||
&[Length(10), Min(10), Max(10), Percentage(10), Ratio(1,10)],
|
||||
),
|
||||
(
|
||||
"Fill(u16) takes any excess space always",
|
||||
&[Length(20), Percentage(20), Ratio(1, 5), Fill(1)],
|
||||
),
|
||||
(
|
||||
"Here's all constraints in one line",
|
||||
&[Length(10), Min(10), Max(10), Percentage(10), Ratio(1,10), Fill(1)],
|
||||
),
|
||||
(
|
||||
"",
|
||||
&[Max(50), Min(50)],
|
||||
),
|
||||
(
|
||||
"",
|
||||
&[Max(20), Length(10)],
|
||||
),
|
||||
(
|
||||
"",
|
||||
&[Max(20), Length(10)],
|
||||
),
|
||||
(
|
||||
"Min grows always but also allows Fill to grow",
|
||||
&[Percentage(50), Fill(1), Fill(2), Min(50)],
|
||||
),
|
||||
(
|
||||
"In `Legacy`, the last constraint of lowest priority takes excess space",
|
||||
&[Length(20), Length(20), Percentage(20)],
|
||||
),
|
||||
("", &[Length(20), Percentage(20), Length(20)]),
|
||||
("A lowest priority constraint will be broken before a high priority constraint", &[Ratio(1,4), Percentage(20)]),
|
||||
("`Length` is higher priority than `Percentage`", &[Percentage(20), Length(10)]),
|
||||
("`Min/Max` is higher priority than `Length`", &[Length(10), Max(20)]),
|
||||
("", &[Length(100), Min(20)]),
|
||||
("`Length` is higher priority than `Min/Max`", &[Max(20), Length(10)]),
|
||||
("", &[Min(20), Length(90)]),
|
||||
("Fill is the lowest priority and will fill any excess space", &[Fill(1), Ratio(1, 4)]),
|
||||
("Fill can be used to scale proportionally with other Fill blocks", &[Fill(1), Percentage(20), Fill(2)]),
|
||||
("", &[Ratio(1, 3), Percentage(20), Ratio(2, 3)]),
|
||||
("Legacy will stretch the last lowest priority constraint\nStretch will only stretch equal weighted constraints", &[Length(20), Length(15)]),
|
||||
("", &[Percentage(20), Length(15)]),
|
||||
("`Fill(u16)` fills up excess space, but is lower priority to spacers.\ni.e. Fill will only have widths in Flex::Stretch and Flex::Legacy", &[Fill(1), Fill(1)]),
|
||||
("", &[Length(20), Length(20)]),
|
||||
(
|
||||
"When not using `Flex::Stretch` or `Flex::Legacy`,\n`Min(u16)` and `Max(u16)` collapse to their lowest values",
|
||||
&[Min(20), Max(20)],
|
||||
),
|
||||
(
|
||||
"",
|
||||
&[Max(20)],
|
||||
),
|
||||
("", &[Min(20), Max(20), Length(20), Length(20)]),
|
||||
("", &[Fill(0), Fill(0)]),
|
||||
(
|
||||
"`Fill(1)` can be to scale with respect to other `Fill(2)`",
|
||||
&[Fill(1), Fill(2)],
|
||||
),
|
||||
(
|
||||
"",
|
||||
&[Fill(1), Min(10), Max(10), Fill(2)],
|
||||
),
|
||||
(
|
||||
"`Fill(0)` collapses if there are other non-zero `Fill(_)`\nconstraints. e.g. `[Fill(0), Fill(0), Fill(1)]`:",
|
||||
&[
|
||||
Fill(0),
|
||||
Fill(0),
|
||||
Fill(1),
|
||||
],
|
||||
),
|
||||
];
|
||||
|
||||
#[derive(Default, Clone, Copy)]
|
||||
struct App {
|
||||
selected_tab: SelectedTab,
|
||||
scroll_offset: u16,
|
||||
spacing: u16,
|
||||
state: AppState,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
enum AppState {
|
||||
#[default]
|
||||
Running,
|
||||
Quit,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct Example {
|
||||
constraints: Vec<Constraint>,
|
||||
description: String,
|
||||
flex: Flex,
|
||||
spacing: u16,
|
||||
}
|
||||
|
||||
/// Tabs for the different layouts
|
||||
///
|
||||
/// Note: the order of the variants will determine the order of the tabs this uses several derive
|
||||
/// macros from the `strum` crate to make it easier to iterate over the variants.
|
||||
/// (`FromRepr`,`Display`,`EnumIter`).
|
||||
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, FromRepr, Display, EnumIter)]
|
||||
enum SelectedTab {
|
||||
#[default]
|
||||
Legacy,
|
||||
Start,
|
||||
Center,
|
||||
End,
|
||||
SpaceAround,
|
||||
SpaceBetween,
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
// assuming the user changes spacing about a 100 times or so
|
||||
Layout::init_cache(EXAMPLE_DATA.len() * SelectedTab::iter().len() * 100);
|
||||
init_error_hooks()?;
|
||||
let terminal = init_terminal()?;
|
||||
App::default().run(terminal)?;
|
||||
|
||||
restore_terminal()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn run(&mut self, mut terminal: Terminal<impl Backend>) -> Result<()> {
|
||||
self.draw(&mut terminal)?;
|
||||
while self.is_running() {
|
||||
self.handle_events()?;
|
||||
self.draw(&mut terminal)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_running(self) -> bool {
|
||||
self.state == AppState::Running
|
||||
}
|
||||
|
||||
fn draw(self, terminal: &mut Terminal<impl Backend>) -> io::Result<()> {
|
||||
terminal.draw(|frame| frame.render_widget(self, frame.size()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_events(&mut self) -> Result<()> {
|
||||
use KeyCode::*;
|
||||
match event::read()? {
|
||||
Event::Key(key) if key.kind == KeyEventKind::Press => match key.code {
|
||||
Char('q') | Esc => self.quit(),
|
||||
Char('l') | Right => self.next(),
|
||||
Char('h') | Left => self.previous(),
|
||||
Char('j') | Down => self.down(),
|
||||
Char('k') | Up => self.up(),
|
||||
Char('g') | Home => self.top(),
|
||||
Char('G') | End => self.bottom(),
|
||||
Char('+') => self.increment_spacing(),
|
||||
Char('-') => self.decrement_spacing(),
|
||||
_ => (),
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn next(&mut self) {
|
||||
self.selected_tab = self.selected_tab.next();
|
||||
}
|
||||
|
||||
fn previous(&mut self) {
|
||||
self.selected_tab = self.selected_tab.previous();
|
||||
}
|
||||
|
||||
fn up(&mut self) {
|
||||
self.scroll_offset = self.scroll_offset.saturating_sub(1);
|
||||
}
|
||||
|
||||
fn down(&mut self) {
|
||||
self.scroll_offset = self
|
||||
.scroll_offset
|
||||
.saturating_add(1)
|
||||
.min(max_scroll_offset());
|
||||
}
|
||||
|
||||
fn top(&mut self) {
|
||||
self.scroll_offset = 0;
|
||||
}
|
||||
|
||||
fn bottom(&mut self) {
|
||||
self.scroll_offset = max_scroll_offset();
|
||||
}
|
||||
|
||||
fn increment_spacing(&mut self) {
|
||||
self.spacing = self.spacing.saturating_add(1);
|
||||
}
|
||||
|
||||
fn decrement_spacing(&mut self) {
|
||||
self.spacing = self.spacing.saturating_sub(1);
|
||||
}
|
||||
|
||||
fn quit(&mut self) {
|
||||
self.state = AppState::Quit;
|
||||
}
|
||||
}
|
||||
|
||||
// when scrolling, make sure we don't scroll past the last example
|
||||
fn max_scroll_offset() -> u16 {
|
||||
example_height()
|
||||
- EXAMPLE_DATA
|
||||
.last()
|
||||
.map_or(0, |(desc, _)| get_description_height(desc) + 4)
|
||||
}
|
||||
|
||||
/// The height of all examples combined
|
||||
///
|
||||
/// Each may or may not have a title so we need to account for that.
|
||||
fn example_height() -> u16 {
|
||||
EXAMPLE_DATA
|
||||
.iter()
|
||||
.map(|(desc, _)| get_description_height(desc) + 4)
|
||||
.sum()
|
||||
}
|
||||
|
||||
impl Widget for App {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let layout = Layout::vertical([Length(3), Length(1), Fill(0)]);
|
||||
let [tabs, axis, demo] = layout.areas(area);
|
||||
self.tabs().render(tabs, buf);
|
||||
let scroll_needed = self.render_demo(demo, buf);
|
||||
let axis_width = if scroll_needed {
|
||||
axis.width.saturating_sub(1)
|
||||
} else {
|
||||
axis.width
|
||||
};
|
||||
Self::axis(axis_width, self.spacing).render(axis, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn tabs(self) -> impl Widget {
|
||||
let tab_titles = SelectedTab::iter().map(SelectedTab::to_tab_title);
|
||||
let block = Block::new()
|
||||
.title(Title::from("Flex Layouts ".bold()))
|
||||
.title(" Use ◄ ► to change tab, ▲ ▼ to scroll, - + to change spacing ");
|
||||
Tabs::new(tab_titles)
|
||||
.block(block)
|
||||
.highlight_style(Modifier::REVERSED)
|
||||
.select(self.selected_tab as usize)
|
||||
.divider(" ")
|
||||
.padding("", "")
|
||||
}
|
||||
|
||||
/// a bar like `<----- 80 px (gap: 2 px)? ----->`
|
||||
fn axis(width: u16, spacing: u16) -> impl Widget {
|
||||
let width = width as usize;
|
||||
// only show gap when spacing is not zero
|
||||
let label = if spacing != 0 {
|
||||
format!("{width} px (gap: {spacing} px)")
|
||||
} else {
|
||||
format!("{width} px")
|
||||
};
|
||||
let bar_width = width.saturating_sub(2); // we want to `<` and `>` at the ends
|
||||
let width_bar = format!("<{label:-^bar_width$}>");
|
||||
Paragraph::new(width_bar.dark_gray()).centered()
|
||||
}
|
||||
|
||||
/// Render the demo content
|
||||
///
|
||||
/// This function renders the demo content into a separate buffer and then splices the buffer
|
||||
/// into the main buffer. This is done to make it possible to handle scrolling easily.
|
||||
///
|
||||
/// Returns bool indicating whether scroll was needed
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
fn render_demo(self, area: Rect, buf: &mut Buffer) -> bool {
|
||||
// render demo content into a separate buffer so all examples fit we add an extra
|
||||
// area.height to make sure the last example is fully visible even when the scroll offset is
|
||||
// at the max
|
||||
let height = example_height();
|
||||
let demo_area = Rect::new(0, 0, area.width, height);
|
||||
let mut demo_buf = Buffer::empty(demo_area);
|
||||
|
||||
let scrollbar_needed = self.scroll_offset != 0 || height > area.height;
|
||||
let content_area = if scrollbar_needed {
|
||||
Rect {
|
||||
width: demo_area.width - 1,
|
||||
..demo_area
|
||||
}
|
||||
} else {
|
||||
demo_area
|
||||
};
|
||||
|
||||
let mut spacing = self.spacing;
|
||||
self.selected_tab
|
||||
.render(content_area, &mut demo_buf, &mut spacing);
|
||||
|
||||
let visible_content = demo_buf
|
||||
.content
|
||||
.into_iter()
|
||||
.skip((area.width * self.scroll_offset) as usize)
|
||||
.take(area.area() as usize);
|
||||
for (i, cell) in visible_content.enumerate() {
|
||||
let x = i as u16 % area.width;
|
||||
let y = i as u16 / area.width;
|
||||
*buf.get_mut(area.x + x, area.y + y) = cell;
|
||||
}
|
||||
|
||||
if scrollbar_needed {
|
||||
let area = area.intersection(buf.area);
|
||||
let mut state = ScrollbarState::new(max_scroll_offset() as usize)
|
||||
.position(self.scroll_offset as usize);
|
||||
Scrollbar::new(ScrollbarOrientation::VerticalRight).render(area, buf, &mut state);
|
||||
}
|
||||
scrollbar_needed
|
||||
}
|
||||
}
|
||||
|
||||
impl SelectedTab {
|
||||
/// Get the previous tab, if there is no previous tab return the current tab.
|
||||
fn previous(self) -> Self {
|
||||
let current_index: usize = self as usize;
|
||||
let previous_index = current_index.saturating_sub(1);
|
||||
Self::from_repr(previous_index).unwrap_or(self)
|
||||
}
|
||||
|
||||
/// Get the next tab, if there is no next tab return the current tab.
|
||||
fn next(self) -> Self {
|
||||
let current_index = self as usize;
|
||||
let next_index = current_index.saturating_add(1);
|
||||
Self::from_repr(next_index).unwrap_or(self)
|
||||
}
|
||||
|
||||
/// Convert a `SelectedTab` into a `Line` to display it by the `Tabs` widget.
|
||||
fn to_tab_title(value: Self) -> Line<'static> {
|
||||
use tailwind::*;
|
||||
let text = value.to_string();
|
||||
let color = match value {
|
||||
Self::Legacy => ORANGE.c400,
|
||||
Self::Start => SKY.c400,
|
||||
Self::Center => SKY.c300,
|
||||
Self::End => SKY.c200,
|
||||
Self::SpaceAround => INDIGO.c400,
|
||||
Self::SpaceBetween => INDIGO.c300,
|
||||
};
|
||||
format!(" {text} ").fg(color).bg(Color::Black).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl StatefulWidget for SelectedTab {
|
||||
type State = u16;
|
||||
fn render(self, area: Rect, buf: &mut Buffer, spacing: &mut Self::State) {
|
||||
let spacing = *spacing;
|
||||
match self {
|
||||
Self::Legacy => Self::render_examples(area, buf, Flex::Legacy, spacing),
|
||||
Self::Start => Self::render_examples(area, buf, Flex::Start, spacing),
|
||||
Self::Center => Self::render_examples(area, buf, Flex::Center, spacing),
|
||||
Self::End => Self::render_examples(area, buf, Flex::End, spacing),
|
||||
Self::SpaceAround => Self::render_examples(area, buf, Flex::SpaceAround, spacing),
|
||||
Self::SpaceBetween => Self::render_examples(area, buf, Flex::SpaceBetween, spacing),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SelectedTab {
|
||||
fn render_examples(area: Rect, buf: &mut Buffer, flex: Flex, spacing: u16) {
|
||||
let heights = EXAMPLE_DATA
|
||||
.iter()
|
||||
.map(|(desc, _)| get_description_height(desc) + 4);
|
||||
let areas = Layout::vertical(heights).flex(Flex::Start).split(area);
|
||||
for (area, (description, constraints)) in areas.iter().zip(EXAMPLE_DATA.iter()) {
|
||||
Example::new(constraints, description, flex, spacing).render(*area, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Example {
|
||||
fn new(constraints: &[Constraint], description: &str, flex: Flex, spacing: u16) -> Self {
|
||||
Self {
|
||||
constraints: constraints.into(),
|
||||
description: description.into(),
|
||||
flex,
|
||||
spacing,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Example {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let title_height = get_description_height(&self.description);
|
||||
let layout = Layout::vertical([Length(title_height), Fill(0)]);
|
||||
let [title, illustrations] = layout.areas(area);
|
||||
|
||||
let (blocks, spacers) = Layout::horizontal(&self.constraints)
|
||||
.flex(self.flex)
|
||||
.spacing(self.spacing)
|
||||
.split_with_spacers(illustrations);
|
||||
|
||||
if !self.description.is_empty() {
|
||||
Paragraph::new(
|
||||
self.description
|
||||
.split('\n')
|
||||
.map(|s| format!("// {s}").italic().fg(tailwind::SLATE.c400))
|
||||
.map(Line::from)
|
||||
.collect::<Vec<Line>>(),
|
||||
)
|
||||
.render(title, buf);
|
||||
}
|
||||
|
||||
for (block, constraint) in blocks.iter().zip(&self.constraints) {
|
||||
Self::illustration(*constraint, block.width).render(*block, buf);
|
||||
}
|
||||
|
||||
for spacer in spacers.iter() {
|
||||
Self::render_spacer(*spacer, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Example {
|
||||
fn render_spacer(spacer: Rect, buf: &mut Buffer) {
|
||||
if spacer.width > 1 {
|
||||
let corners_only = symbols::border::Set {
|
||||
top_left: line::NORMAL.top_left,
|
||||
top_right: line::NORMAL.top_right,
|
||||
bottom_left: line::NORMAL.bottom_left,
|
||||
bottom_right: line::NORMAL.bottom_right,
|
||||
vertical_left: " ",
|
||||
vertical_right: " ",
|
||||
horizontal_top: " ",
|
||||
horizontal_bottom: " ",
|
||||
};
|
||||
Block::bordered()
|
||||
.border_set(corners_only)
|
||||
.border_style(Style::reset().dark_gray())
|
||||
.render(spacer, buf);
|
||||
} else {
|
||||
Paragraph::new(Text::from(vec![
|
||||
Line::from(""),
|
||||
Line::from("│"),
|
||||
Line::from("│"),
|
||||
Line::from(""),
|
||||
]))
|
||||
.style(Style::reset().dark_gray())
|
||||
.render(spacer, buf);
|
||||
}
|
||||
let width = spacer.width;
|
||||
let label = if width > 4 {
|
||||
format!("{width} px")
|
||||
} else if width > 2 {
|
||||
format!("{width}")
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let text = Text::from(vec![
|
||||
Line::raw(""),
|
||||
Line::raw(""),
|
||||
Line::styled(label, Style::reset().dark_gray()),
|
||||
]);
|
||||
Paragraph::new(text)
|
||||
.style(Style::reset().dark_gray())
|
||||
.alignment(Alignment::Center)
|
||||
.render(spacer, buf);
|
||||
}
|
||||
|
||||
fn illustration(constraint: Constraint, width: u16) -> impl Widget {
|
||||
let main_color = color_for_constraint(constraint);
|
||||
let fg_color = Color::White;
|
||||
let title = format!("{constraint}");
|
||||
let content = format!("{width} px");
|
||||
let text = format!("{title}\n{content}");
|
||||
let block = Block::bordered()
|
||||
.border_set(symbols::border::QUADRANT_OUTSIDE)
|
||||
.border_style(Style::reset().fg(main_color).reversed())
|
||||
.style(Style::default().fg(fg_color).bg(main_color));
|
||||
Paragraph::new(text).centered().block(block)
|
||||
}
|
||||
}
|
||||
|
||||
const fn color_for_constraint(constraint: Constraint) -> Color {
|
||||
use tailwind::*;
|
||||
match constraint {
|
||||
Constraint::Min(_) => BLUE.c900,
|
||||
Constraint::Max(_) => BLUE.c800,
|
||||
Constraint::Length(_) => SLATE.c700,
|
||||
Constraint::Percentage(_) => SLATE.c800,
|
||||
Constraint::Ratio(_, _) => SLATE.c900,
|
||||
Constraint::Fill(_) => SLATE.c950,
|
||||
}
|
||||
}
|
||||
|
||||
fn init_error_hooks() -> Result<()> {
|
||||
let (panic, error) = HookBuilder::default().into_hooks();
|
||||
let panic = panic.into_panic_hook();
|
||||
let error = error.into_eyre_hook();
|
||||
color_eyre::eyre::set_hook(Box::new(move |e| {
|
||||
let _ = restore_terminal();
|
||||
error(e)
|
||||
}))?;
|
||||
std::panic::set_hook(Box::new(move |info| {
|
||||
let _ = restore_terminal();
|
||||
panic(info);
|
||||
}));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn init_terminal() -> Result<Terminal<impl Backend>> {
|
||||
enable_raw_mode()?;
|
||||
stdout().execute(EnterAlternateScreen)?;
|
||||
let backend = CrosstermBackend::new(stdout());
|
||||
let terminal = Terminal::new(backend)?;
|
||||
Ok(terminal)
|
||||
}
|
||||
|
||||
fn restore_terminal() -> Result<()> {
|
||||
disable_raw_mode()?;
|
||||
stdout().execute(LeaveAlternateScreen)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
fn get_description_height(s: &str) -> u16 {
|
||||
if s.is_empty() {
|
||||
0
|
||||
} else {
|
||||
s.split('\n').count() as u16
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,23 @@
|
||||
use std::{
|
||||
io::{self, stdout, Stdout},
|
||||
rc::Rc,
|
||||
time::Duration,
|
||||
};
|
||||
//! # [Ratatui] Gauge example
|
||||
//!
|
||||
//! The latest version of this example is available in the [examples] folder in the repository.
|
||||
//!
|
||||
//! Please note that the examples are designed to be run against the `main` branch of the Github
|
||||
//! repository. This means that you may not be able to compile with the latest release version on
|
||||
//! crates.io, or the one that you have installed locally.
|
||||
//!
|
||||
//! See the [examples readme] for more information on finding examples that match the version of the
|
||||
//! library you are using.
|
||||
//!
|
||||
//! [Ratatui]: https://github.com/ratatui-org/ratatui
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
use anyhow::Result;
|
||||
#![allow(clippy::enum_glob_use)]
|
||||
|
||||
use std::{io::stdout, time::Duration};
|
||||
|
||||
use color_eyre::{config::HookBuilder, Result};
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode, KeyEventKind},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
@@ -12,84 +25,85 @@ use crossterm::{
|
||||
};
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{block::Title, *},
|
||||
style::palette::tailwind,
|
||||
widgets::{block::Title, Block, Borders, Gauge, Padding, Paragraph},
|
||||
};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
App::run()
|
||||
}
|
||||
|
||||
struct App {
|
||||
term: Term,
|
||||
should_quit: bool,
|
||||
state: AppState,
|
||||
}
|
||||
const GAUGE1_COLOR: Color = tailwind::RED.c800;
|
||||
const GAUGE2_COLOR: Color = tailwind::GREEN.c800;
|
||||
const GAUGE3_COLOR: Color = tailwind::BLUE.c800;
|
||||
const GAUGE4_COLOR: Color = tailwind::ORANGE.c800;
|
||||
const CUSTOM_LABEL_COLOR: Color = tailwind::SLATE.c200;
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
struct AppState {
|
||||
struct App {
|
||||
state: AppState,
|
||||
progress_columns: u16,
|
||||
progress1: u16,
|
||||
progress2: u16,
|
||||
progress2: f64,
|
||||
progress3: f64,
|
||||
progress4: u16,
|
||||
progress4: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
enum AppState {
|
||||
#[default]
|
||||
Running,
|
||||
Started,
|
||||
Quitting,
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
init_error_hooks()?;
|
||||
let terminal = init_terminal()?;
|
||||
App::default().run(terminal)?;
|
||||
restore_terminal()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn run() -> Result<()> {
|
||||
// run at ~10 fps minus the time it takes to draw
|
||||
let timeout = Duration::from_secs_f32(1.0 / 10.0);
|
||||
let mut app = Self::start()?;
|
||||
while !app.should_quit {
|
||||
app.update();
|
||||
app.draw()?;
|
||||
app.handle_events(timeout)?;
|
||||
fn run(&mut self, mut terminal: Terminal<impl Backend>) -> Result<()> {
|
||||
while self.state != AppState::Quitting {
|
||||
self.draw(&mut terminal)?;
|
||||
self.handle_events()?;
|
||||
self.update(terminal.size()?.width);
|
||||
}
|
||||
app.stop()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn start() -> Result<Self> {
|
||||
Ok(App {
|
||||
term: Term::start()?,
|
||||
should_quit: false,
|
||||
state: AppState {
|
||||
progress1: 0,
|
||||
progress2: 0,
|
||||
progress3: 0.0,
|
||||
progress4: 0,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn stop(&mut self) -> Result<()> {
|
||||
Term::stop()?;
|
||||
fn draw(&self, terminal: &mut Terminal<impl Backend>) -> Result<()> {
|
||||
terminal.draw(|f| f.render_widget(self, f.size()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update(&mut self) {
|
||||
self.state.progress1 = (self.state.progress1 + 4).min(100);
|
||||
self.state.progress2 = (self.state.progress2 + 3).min(100);
|
||||
self.state.progress3 = (self.state.progress3 + 0.02).min(1.0);
|
||||
self.state.progress4 = (self.state.progress4 + 1).min(100);
|
||||
fn update(&mut self, terminal_width: u16) {
|
||||
if self.state != AppState::Started {
|
||||
return;
|
||||
}
|
||||
|
||||
// progress1 and progress2 help show the difference between ratio and percentage measuring
|
||||
// the same thing, but converting to either a u16 or f64. Effectively, we're showing the
|
||||
// difference between how a continuous gauge acts for floor and rounded values.
|
||||
self.progress_columns = (self.progress_columns + 1).clamp(0, terminal_width);
|
||||
self.progress1 = self.progress_columns * 100 / terminal_width;
|
||||
self.progress2 = f64::from(self.progress_columns) * 100.0 / f64::from(terminal_width);
|
||||
|
||||
// progress3 and progress4 similarly show the difference between unicode and non-unicode
|
||||
// gauges measuring the same thing.
|
||||
self.progress3 = (self.progress3 + 0.1).clamp(40.0, 100.0);
|
||||
self.progress4 = (self.progress4 + 0.1).clamp(40.0, 100.0);
|
||||
}
|
||||
|
||||
fn draw(&mut self) -> Result<()> {
|
||||
self.term.draw(|frame| {
|
||||
let state = self.state;
|
||||
let layout = Self::equal_layout(frame);
|
||||
Self::render_gauge1(state.progress1, frame, layout[0]);
|
||||
Self::render_gauge2(state.progress2, frame, layout[1]);
|
||||
Self::render_gauge3(state.progress3, frame, layout[2]);
|
||||
Self::render_gauge4(state.progress4, frame, layout[3]);
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_events(&mut self, timeout: Duration) -> io::Result<()> {
|
||||
fn handle_events(&mut self) -> Result<()> {
|
||||
let timeout = Duration::from_secs_f32(1.0 / 20.0);
|
||||
if event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
self.should_quit = true;
|
||||
use KeyCode::*;
|
||||
match key.code {
|
||||
Char(' ') | Enter => self.start(),
|
||||
Char('q') | Esc => self.quit(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,91 +111,133 @@ impl App {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn equal_layout(frame: &Frame) -> Rc<[Rect]> {
|
||||
Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
])
|
||||
.split(frame.size())
|
||||
fn start(&mut self) {
|
||||
self.state = AppState::Started;
|
||||
}
|
||||
|
||||
fn render_gauge1(progress: u16, frame: &mut Frame, area: Rect) {
|
||||
let title = Self::title_block("Gauge with percentage progress");
|
||||
let gauge = Gauge::default()
|
||||
fn quit(&mut self) {
|
||||
self.state = AppState::Quitting;
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for &App {
|
||||
#[allow(clippy::similar_names)]
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
use Constraint::*;
|
||||
let layout = Layout::vertical([Length(2), Min(0), Length(1)]);
|
||||
let [header_area, gauge_area, footer_area] = layout.areas(area);
|
||||
|
||||
let layout = Layout::vertical([Ratio(1, 4); 4]);
|
||||
let [gauge1_area, gauge2_area, gauge3_area, gauge4_area] = layout.areas(gauge_area);
|
||||
|
||||
render_header(header_area, buf);
|
||||
render_footer(footer_area, buf);
|
||||
|
||||
self.render_gauge1(gauge1_area, buf);
|
||||
self.render_gauge2(gauge2_area, buf);
|
||||
self.render_gauge3(gauge3_area, buf);
|
||||
self.render_gauge4(gauge4_area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_header(area: Rect, buf: &mut Buffer) {
|
||||
Paragraph::new("Ratatui Gauge Example")
|
||||
.bold()
|
||||
.alignment(Alignment::Center)
|
||||
.fg(CUSTOM_LABEL_COLOR)
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
fn render_footer(area: Rect, buf: &mut Buffer) {
|
||||
Paragraph::new("Press ENTER to start")
|
||||
.alignment(Alignment::Center)
|
||||
.fg(CUSTOM_LABEL_COLOR)
|
||||
.bold()
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn render_gauge1(&self, area: Rect, buf: &mut Buffer) {
|
||||
let title = title_block("Gauge with percentage");
|
||||
Gauge::default()
|
||||
.block(title)
|
||||
.gauge_style(Style::new().light_red())
|
||||
.percent(progress);
|
||||
frame.render_widget(gauge, area);
|
||||
.gauge_style(GAUGE1_COLOR)
|
||||
.percent(self.progress1)
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
fn render_gauge2(progress: u16, frame: &mut Frame, area: Rect) {
|
||||
let title = Self::title_block("Gauge with percentage progress and custom label");
|
||||
let label = format!("{}/100", progress);
|
||||
let gauge = Gauge::default()
|
||||
.block(title)
|
||||
.gauge_style(Style::new().blue().on_light_blue())
|
||||
.percent(progress)
|
||||
.label(label);
|
||||
frame.render_widget(gauge, area);
|
||||
}
|
||||
|
||||
fn render_gauge3(progress: f64, frame: &mut Frame, area: Rect) {
|
||||
let title =
|
||||
Self::title_block("Gauge with ratio progress, custom label with style, and unicode");
|
||||
fn render_gauge2(&self, area: Rect, buf: &mut Buffer) {
|
||||
let title = title_block("Gauge with ratio and custom label");
|
||||
let label = Span::styled(
|
||||
format!("{:.2}%", progress * 100.0),
|
||||
Style::new().red().italic().bold(),
|
||||
format!("{:.1}/100", self.progress2),
|
||||
Style::new().italic().bold().fg(CUSTOM_LABEL_COLOR),
|
||||
);
|
||||
let gauge = Gauge::default()
|
||||
Gauge::default()
|
||||
.block(title)
|
||||
.gauge_style(Style::default().fg(Color::Yellow))
|
||||
.ratio(progress)
|
||||
.gauge_style(GAUGE2_COLOR)
|
||||
.ratio(self.progress2 / 100.0)
|
||||
.label(label)
|
||||
.use_unicode(true);
|
||||
frame.render_widget(gauge, area);
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
fn render_gauge4(progress: u16, frame: &mut Frame, area: Rect) {
|
||||
let title = Self::title_block("Gauge with percentage progress and label");
|
||||
let label = format!("{}/100", progress);
|
||||
let gauge = Gauge::default()
|
||||
fn render_gauge3(&self, area: Rect, buf: &mut Buffer) {
|
||||
let title = title_block("Gauge with ratio (no unicode)");
|
||||
let label = format!("{:.1}%", self.progress3);
|
||||
Gauge::default()
|
||||
.block(title)
|
||||
.gauge_style(Style::new().green().italic())
|
||||
.percent(progress)
|
||||
.label(label);
|
||||
frame.render_widget(gauge, area);
|
||||
.gauge_style(GAUGE3_COLOR)
|
||||
.ratio(self.progress3 / 100.0)
|
||||
.label(label)
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
fn title_block(title: &str) -> Block {
|
||||
let title = Title::from(title).alignment(Alignment::Center);
|
||||
Block::default().title(title).borders(Borders::TOP)
|
||||
fn render_gauge4(&self, area: Rect, buf: &mut Buffer) {
|
||||
let title = title_block("Gauge with ratio (unicode)");
|
||||
let label = format!("{:.1}%", self.progress3);
|
||||
Gauge::default()
|
||||
.block(title)
|
||||
.gauge_style(GAUGE4_COLOR)
|
||||
.ratio(self.progress4 / 100.0)
|
||||
.label(label)
|
||||
.use_unicode(true)
|
||||
.render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
struct Term {
|
||||
terminal: Terminal<CrosstermBackend<Stdout>>,
|
||||
fn title_block(title: &str) -> Block {
|
||||
let title = Title::from(title).alignment(Alignment::Center);
|
||||
Block::default()
|
||||
.title(title)
|
||||
.borders(Borders::NONE)
|
||||
.fg(CUSTOM_LABEL_COLOR)
|
||||
.padding(Padding::vertical(1))
|
||||
}
|
||||
|
||||
impl Term {
|
||||
pub fn start() -> io::Result<Term> {
|
||||
stdout().execute(EnterAlternateScreen)?;
|
||||
enable_raw_mode()?;
|
||||
let terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
|
||||
Ok(Self { terminal })
|
||||
}
|
||||
|
||||
pub fn stop() -> io::Result<()> {
|
||||
disable_raw_mode()?;
|
||||
stdout().execute(LeaveAlternateScreen)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: impl FnOnce(&mut Frame)) -> Result<()> {
|
||||
self.terminal.draw(frame)?;
|
||||
Ok(())
|
||||
}
|
||||
fn init_error_hooks() -> color_eyre::Result<()> {
|
||||
let (panic, error) = HookBuilder::default().into_hooks();
|
||||
let panic = panic.into_panic_hook();
|
||||
let error = error.into_eyre_hook();
|
||||
color_eyre::eyre::set_hook(Box::new(move |e| {
|
||||
let _ = restore_terminal();
|
||||
error(e)
|
||||
}))?;
|
||||
std::panic::set_hook(Box::new(move |info| {
|
||||
let _ = restore_terminal();
|
||||
panic(info);
|
||||
}));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn init_terminal() -> color_eyre::Result<Terminal<impl Backend>> {
|
||||
enable_raw_mode()?;
|
||||
stdout().execute(EnterAlternateScreen)?;
|
||||
let backend = CrosstermBackend::new(stdout());
|
||||
let terminal = Terminal::new(backend)?;
|
||||
Ok(terminal)
|
||||
}
|
||||
|
||||
fn restore_terminal() -> color_eyre::Result<()> {
|
||||
disable_raw_mode()?;
|
||||
stdout().execute(LeaveAlternateScreen)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
//! # [Ratatui] Hello World example
|
||||
//!
|
||||
//! The latest version of this example is available in the [examples] folder in the repository.
|
||||
//!
|
||||
//! Please note that the examples are designed to be run against the `main` branch of the Github
|
||||
//! repository. This means that you may not be able to compile with the latest release version on
|
||||
//! crates.io, or the one that you have installed locally.
|
||||
//!
|
||||
//! See the [examples readme] for more information on finding examples that match the version of the
|
||||
//! library you are using.
|
||||
//!
|
||||
//! [Ratatui]: https://github.com/ratatui-org/ratatui
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
use std::{
|
||||
io::{self, Stdout},
|
||||
time::Duration,
|
||||
@@ -9,7 +24,7 @@ use crossterm::{
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
use ratatui::{prelude::*, widgets::Paragraph};
|
||||
|
||||
/// This is a bare minimum example. There are many approaches to running an application loop, so
|
||||
/// this is not meant to be prescriptive. It is only meant to demonstrate the basic setup and
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
//! # [Ratatui] Inline example
|
||||
//!
|
||||
//! The latest version of this example is available in the [examples] folder in the repository.
|
||||
//!
|
||||
//! Please note that the examples are designed to be run against the `main` branch of the Github
|
||||
//! repository. This means that you may not be able to compile with the latest release version on
|
||||
//! crates.io, or the one that you have installed locally.
|
||||
//!
|
||||
//! See the [examples readme] for more information on finding examples that match the version of the
|
||||
//! library you are using.
|
||||
//!
|
||||
//! [Ratatui]: https://github.com/ratatui-org/ratatui
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
#![allow(clippy::wildcard_imports)]
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, VecDeque},
|
||||
error::Error,
|
||||
@@ -114,6 +131,7 @@ fn input_handling(tx: mpsc::Sender<Event>) {
|
||||
});
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_precision_loss, clippy::needless_pass_by_value)]
|
||||
fn workers(tx: mpsc::Sender<Event>) -> Vec<Worker> {
|
||||
(0..4)
|
||||
.map(|id| {
|
||||
@@ -153,6 +171,7 @@ fn downloads() -> Downloads {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
workers: Vec<Worker>,
|
||||
@@ -179,7 +198,7 @@ fn run_app<B: Backend>(
|
||||
Event::DownloadUpdate(worker_id, _download_id, progress) => {
|
||||
let download = downloads.in_progress.get_mut(&worker_id).unwrap();
|
||||
download.progress = progress;
|
||||
redraw = false
|
||||
redraw = false;
|
||||
}
|
||||
Event::DownloadDone(worker_id, download_id) => {
|
||||
let download = downloads.in_progress.remove(&worker_id).unwrap();
|
||||
@@ -215,28 +234,24 @@ fn run_app<B: Backend>(
|
||||
}
|
||||
|
||||
fn ui(f: &mut Frame, downloads: &Downloads) {
|
||||
let size = f.size();
|
||||
let area = f.size();
|
||||
|
||||
let block = Block::default().title(block::Title::from("Progress").alignment(Alignment::Center));
|
||||
f.render_widget(block, size);
|
||||
f.render_widget(block, area);
|
||||
|
||||
let chunks = Layout::default()
|
||||
.constraints([Constraint::Length(2), Constraint::Length(4)])
|
||||
.margin(1)
|
||||
.split(size);
|
||||
let vertical = Layout::vertical([Constraint::Length(2), Constraint::Length(4)]).margin(1);
|
||||
let horizontal = Layout::horizontal([Constraint::Percentage(20), Constraint::Percentage(80)]);
|
||||
let [progress_area, main] = vertical.areas(area);
|
||||
let [list_area, gauge_area] = horizontal.areas(main);
|
||||
|
||||
// total progress
|
||||
let done = NUM_DOWNLOADS - downloads.pending.len() - downloads.in_progress.len();
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
let progress = LineGauge::default()
|
||||
.gauge_style(Style::default().fg(Color::Blue))
|
||||
.label(format!("{done}/{NUM_DOWNLOADS}"))
|
||||
.ratio(done as f64 / NUM_DOWNLOADS as f64);
|
||||
f.render_widget(progress, chunks[0]);
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(20), Constraint::Percentage(80)])
|
||||
.split(chunks[1]);
|
||||
f.render_widget(progress, progress_area);
|
||||
|
||||
// in progress downloads
|
||||
let items: Vec<ListItem> = downloads
|
||||
@@ -259,21 +274,22 @@ fn ui(f: &mut Frame, downloads: &Downloads) {
|
||||
})
|
||||
.collect();
|
||||
let list = List::new(items);
|
||||
f.render_widget(list, chunks[0]);
|
||||
f.render_widget(list, list_area);
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
for (i, (_, download)) in downloads.in_progress.iter().enumerate() {
|
||||
let gauge = Gauge::default()
|
||||
.gauge_style(Style::default().fg(Color::Yellow))
|
||||
.ratio(download.progress / 100.0);
|
||||
if chunks[1].top().saturating_add(i as u16) > size.bottom() {
|
||||
if gauge_area.top().saturating_add(i as u16) > area.bottom() {
|
||||
continue;
|
||||
}
|
||||
f.render_widget(
|
||||
gauge,
|
||||
Rect {
|
||||
x: chunks[1].left(),
|
||||
y: chunks[1].top().saturating_add(i as u16),
|
||||
width: chunks[1].width,
|
||||
x: gauge_area.left(),
|
||||
y: gauge_area.top().saturating_add(i as u16),
|
||||
width: gauge_area.width,
|
||||
height: 1,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
//! # [Ratatui] Layout example
|
||||
//!
|
||||
//! The latest version of this example is available in the [examples] folder in the repository.
|
||||
//!
|
||||
//! Please note that the examples are designed to be run against the `main` branch of the Github
|
||||
//! repository. This means that you may not be able to compile with the latest release version on
|
||||
//! crates.io, or the one that you have installed locally.
|
||||
//!
|
||||
//! See the [examples readme] for more information on finding examples that match the version of the
|
||||
//! library you are using.
|
||||
//!
|
||||
//! [Ratatui]: https://github.com/ratatui-org/ratatui
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
#![allow(clippy::enum_glob_use)]
|
||||
|
||||
use std::{error::Error, io};
|
||||
|
||||
use crossterm::{
|
||||
@@ -6,7 +23,11 @@ use crossterm::{
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use ratatui::{layout::Constraint::*, prelude::*, widgets::*};
|
||||
use ratatui::{
|
||||
layout::Constraint::*,
|
||||
prelude::*,
|
||||
widgets::{Block, Borders, Paragraph},
|
||||
};
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
@@ -40,64 +61,58 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
|
||||
terminal.draw(ui)?;
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
if key.code == KeyCode::Char('q') {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn ui(frame: &mut Frame) {
|
||||
let main_layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Length(4), // text
|
||||
Length(50), // examples
|
||||
Min(0), // fills remaining space
|
||||
])
|
||||
.split(frame.size());
|
||||
let vertical = Layout::vertical([
|
||||
Length(4), // text
|
||||
Length(50), // examples
|
||||
Min(0), // fills remaining space
|
||||
]);
|
||||
let [text_area, examples_area, _] = vertical.areas(frame.size());
|
||||
|
||||
// title
|
||||
frame.render_widget(
|
||||
Paragraph::new(vec![
|
||||
Line::from("Horizontal Layout Example. Press q to quit".dark_gray())
|
||||
.alignment(Alignment::Center),
|
||||
Line::from("Horizontal Layout Example. Press q to quit".dark_gray()).centered(),
|
||||
Line::from("Each line has 2 constraints, plus Min(0) to fill the remaining space."),
|
||||
Line::from("E.g. the second line of the Len/Min box is [Length(2), Min(2), Min(0)]"),
|
||||
Line::from("Note: constraint labels that don't fit are truncated"),
|
||||
]),
|
||||
main_layout[0],
|
||||
text_area,
|
||||
);
|
||||
|
||||
let example_rows = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Length(9),
|
||||
Length(9),
|
||||
Length(9),
|
||||
Length(9),
|
||||
Length(9),
|
||||
Min(0), // fills remaining space
|
||||
])
|
||||
.split(main_layout[1]);
|
||||
let example_rows = Layout::vertical([
|
||||
Length(9),
|
||||
Length(9),
|
||||
Length(9),
|
||||
Length(9),
|
||||
Length(9),
|
||||
Min(0), // fills remaining space
|
||||
])
|
||||
.split(examples_area);
|
||||
let example_areas = example_rows
|
||||
.iter()
|
||||
.flat_map(|area| {
|
||||
Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([
|
||||
Length(14),
|
||||
Length(14),
|
||||
Length(14),
|
||||
Length(14),
|
||||
Length(14),
|
||||
Min(0), // fills remaining space
|
||||
])
|
||||
.split(*area)
|
||||
.iter()
|
||||
.copied()
|
||||
.take(5) // ignore Min(0)
|
||||
.collect_vec()
|
||||
Layout::horizontal([
|
||||
Length(14),
|
||||
Length(14),
|
||||
Length(14),
|
||||
Length(14),
|
||||
Length(14),
|
||||
Min(0), // fills remaining space
|
||||
])
|
||||
.split(*area)
|
||||
.iter()
|
||||
.copied()
|
||||
.take(5) // ignore Min(0)
|
||||
.collect_vec()
|
||||
})
|
||||
.collect_vec();
|
||||
|
||||
@@ -182,12 +197,9 @@ fn render_example_combination(
|
||||
.border_style(Style::default().fg(Color::DarkGray));
|
||||
let inner = block.inner(area);
|
||||
frame.render_widget(block, area);
|
||||
let layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(vec![Length(1); constraints.len() + 1])
|
||||
.split(inner);
|
||||
for (i, (a, b)) in constraints.iter().enumerate() {
|
||||
render_single_example(frame, layout[i], vec![*a, *b, Min(0)]);
|
||||
let layout = Layout::vertical(vec![Length(1); constraints.len() + 1]).split(inner);
|
||||
for (i, (a, b)) in constraints.into_iter().enumerate() {
|
||||
render_single_example(frame, layout[i], vec![a, b, Min(0)]);
|
||||
}
|
||||
// This is to make it easy to visually see the alignment of the examples
|
||||
// with the constraints.
|
||||
@@ -199,21 +211,20 @@ fn render_single_example(frame: &mut Frame, area: Rect, constraints: Vec<Constra
|
||||
let red = Paragraph::new(constraint_label(constraints[0])).on_red();
|
||||
let blue = Paragraph::new(constraint_label(constraints[1])).on_blue();
|
||||
let green = Paragraph::new("·".repeat(12)).on_green();
|
||||
let layout = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(constraints)
|
||||
.split(area);
|
||||
frame.render_widget(red, layout[0]);
|
||||
frame.render_widget(blue, layout[1]);
|
||||
frame.render_widget(green, layout[2]);
|
||||
let horizontal = Layout::horizontal(constraints);
|
||||
let [r, b, g] = horizontal.areas(area);
|
||||
frame.render_widget(red, r);
|
||||
frame.render_widget(blue, b);
|
||||
frame.render_widget(green, g);
|
||||
}
|
||||
|
||||
fn constraint_label(constraint: Constraint) -> String {
|
||||
match constraint {
|
||||
Length(n) => format!("{n}"),
|
||||
Min(n) => format!("{n}"),
|
||||
Max(n) => format!("{n}"),
|
||||
Percentage(n) => format!("{n}"),
|
||||
Ratio(a, b) => format!("{a}:{b}"),
|
||||
Constraint::Ratio(a, b) => format!("{a}:{b}"),
|
||||
Constraint::Length(n)
|
||||
| Constraint::Min(n)
|
||||
| Constraint::Max(n)
|
||||
| Constraint::Percentage(n)
|
||||
| Constraint::Fill(n) => format!("{n}"),
|
||||
}
|
||||
}
|
||||
|
||||
540
examples/list.rs
540
examples/list.rs
@@ -1,26 +1,299 @@
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
//! # [Ratatui] List example
|
||||
//!
|
||||
//! The latest version of this example is available in the [examples] folder in the repository.
|
||||
//!
|
||||
//! Please note that the examples are designed to be run against the `main` branch of the Github
|
||||
//! repository. This means that you may not be able to compile with the latest release version on
|
||||
//! crates.io, or the one that you have installed locally.
|
||||
//!
|
||||
//! See the [examples readme] for more information on finding examples that match the version of the
|
||||
//! library you are using.
|
||||
//!
|
||||
//! [Ratatui]: https://github.com/ratatui-org/ratatui
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
#![allow(clippy::enum_glob_use, clippy::wildcard_imports)]
|
||||
|
||||
use std::{error::Error, io, io::stdout};
|
||||
|
||||
use color_eyre::config::HookBuilder;
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
event::{self, Event, KeyCode, KeyEventKind},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
use ratatui::{prelude::*, style::palette::tailwind, widgets::*};
|
||||
|
||||
struct StatefulList<T> {
|
||||
state: ListState,
|
||||
items: Vec<T>,
|
||||
const TODO_HEADER_BG: Color = tailwind::BLUE.c950;
|
||||
const NORMAL_ROW_COLOR: Color = tailwind::SLATE.c950;
|
||||
const ALT_ROW_COLOR: Color = tailwind::SLATE.c900;
|
||||
const SELECTED_STYLE_FG: Color = tailwind::BLUE.c300;
|
||||
const TEXT_COLOR: Color = tailwind::SLATE.c200;
|
||||
const COMPLETED_TEXT_COLOR: Color = tailwind::GREEN.c500;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
enum Status {
|
||||
Todo,
|
||||
Completed,
|
||||
}
|
||||
|
||||
impl<T> StatefulList<T> {
|
||||
fn with_items(items: Vec<T>) -> StatefulList<T> {
|
||||
struct TodoItem<'a> {
|
||||
todo: &'a str,
|
||||
info: &'a str,
|
||||
status: Status,
|
||||
}
|
||||
|
||||
struct StatefulList<'a> {
|
||||
state: ListState,
|
||||
items: Vec<TodoItem<'a>>,
|
||||
last_selected: Option<usize>,
|
||||
}
|
||||
|
||||
/// This struct holds the current state of the app. In particular, it has the `items` field which is
|
||||
/// a wrapper around `ListState`. Keeping track of the items state let us render the associated
|
||||
/// widget with its state and have access to features such as natural scrolling.
|
||||
///
|
||||
/// Check the event handling at the bottom to see how to change the state on incoming events.
|
||||
/// Check the drawing logic for items on how to specify the highlighting style for selected items.
|
||||
struct App<'a> {
|
||||
items: StatefulList<'a>,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
init_error_hooks()?;
|
||||
let terminal = init_terminal()?;
|
||||
|
||||
// create app and run it
|
||||
App::new().run(terminal)?;
|
||||
|
||||
restore_terminal()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn init_error_hooks() -> color_eyre::Result<()> {
|
||||
let (panic, error) = HookBuilder::default().into_hooks();
|
||||
let panic = panic.into_panic_hook();
|
||||
let error = error.into_eyre_hook();
|
||||
color_eyre::eyre::set_hook(Box::new(move |e| {
|
||||
let _ = restore_terminal();
|
||||
error(e)
|
||||
}))?;
|
||||
std::panic::set_hook(Box::new(move |info| {
|
||||
let _ = restore_terminal();
|
||||
panic(info);
|
||||
}));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn init_terminal() -> color_eyre::Result<Terminal<impl Backend>> {
|
||||
enable_raw_mode()?;
|
||||
stdout().execute(EnterAlternateScreen)?;
|
||||
let backend = CrosstermBackend::new(stdout());
|
||||
let terminal = Terminal::new(backend)?;
|
||||
Ok(terminal)
|
||||
}
|
||||
|
||||
fn restore_terminal() -> color_eyre::Result<()> {
|
||||
disable_raw_mode()?;
|
||||
stdout().execute(LeaveAlternateScreen)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl<'a> App<'a> {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
items: StatefulList::with_items([
|
||||
("Rewrite everything with Rust!", "I can't hold my inner voice. He tells me to rewrite the complete universe with Rust", Status::Todo),
|
||||
("Rewrite all of your tui apps with Ratatui", "Yes, you heard that right. Go and replace your tui with Ratatui.", Status::Completed),
|
||||
("Pet your cat", "Minnak loves to be pet by you! Don't forget to pet and give some treats!", Status::Todo),
|
||||
("Walk with your dog", "Max is bored, go walk with him!", Status::Todo),
|
||||
("Pay the bills", "Pay the train subscription!!!", Status::Completed),
|
||||
("Refactor list example", "If you see this info that means I completed this task!", Status::Completed),
|
||||
]),
|
||||
}
|
||||
}
|
||||
|
||||
/// Changes the status of the selected list item
|
||||
fn change_status(&mut self) {
|
||||
if let Some(i) = self.items.state.selected() {
|
||||
self.items.items[i].status = match self.items.items[i].status {
|
||||
Status::Completed => Status::Todo,
|
||||
Status::Todo => Status::Completed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn go_top(&mut self) {
|
||||
self.items.state.select(Some(0));
|
||||
}
|
||||
|
||||
fn go_bottom(&mut self) {
|
||||
self.items.state.select(Some(self.items.items.len() - 1));
|
||||
}
|
||||
}
|
||||
|
||||
impl App<'_> {
|
||||
fn run(&mut self, mut terminal: Terminal<impl Backend>) -> io::Result<()> {
|
||||
loop {
|
||||
self.draw(&mut terminal)?;
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
use KeyCode::*;
|
||||
match key.code {
|
||||
Char('q') | Esc => return Ok(()),
|
||||
Char('h') | Left => self.items.unselect(),
|
||||
Char('j') | Down => self.items.next(),
|
||||
Char('k') | Up => self.items.previous(),
|
||||
Char('l') | Right | Enter => self.change_status(),
|
||||
Char('g') => self.go_top(),
|
||||
Char('G') => self.go_bottom(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(&mut self, terminal: &mut Terminal<impl Backend>) -> io::Result<()> {
|
||||
terminal.draw(|f| f.render_widget(self, f.size()))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for &mut App<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
// Create a space for header, todo list and the footer.
|
||||
let vertical = Layout::vertical([
|
||||
Constraint::Length(2),
|
||||
Constraint::Min(0),
|
||||
Constraint::Length(2),
|
||||
]);
|
||||
let [header_area, rest_area, footer_area] = vertical.areas(area);
|
||||
|
||||
// Create two chunks with equal vertical screen space. One for the list and the other for
|
||||
// the info block.
|
||||
let vertical = Layout::vertical([Constraint::Percentage(50), Constraint::Percentage(50)]);
|
||||
let [upper_item_list_area, lower_item_list_area] = vertical.areas(rest_area);
|
||||
|
||||
render_title(header_area, buf);
|
||||
self.render_todo(upper_item_list_area, buf);
|
||||
self.render_info(lower_item_list_area, buf);
|
||||
render_footer(footer_area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl App<'_> {
|
||||
fn render_todo(&mut self, area: Rect, buf: &mut Buffer) {
|
||||
// We create two blocks, one is for the header (outer) and the other is for list (inner).
|
||||
let outer_block = Block::default()
|
||||
.borders(Borders::NONE)
|
||||
.fg(TEXT_COLOR)
|
||||
.bg(TODO_HEADER_BG)
|
||||
.title("TODO List")
|
||||
.title_alignment(Alignment::Center);
|
||||
let inner_block = Block::default()
|
||||
.borders(Borders::NONE)
|
||||
.fg(TEXT_COLOR)
|
||||
.bg(NORMAL_ROW_COLOR);
|
||||
|
||||
// We get the inner area from outer_block. We'll use this area later to render the table.
|
||||
let outer_area = area;
|
||||
let inner_area = outer_block.inner(outer_area);
|
||||
|
||||
// We can render the header in outer_area.
|
||||
outer_block.render(outer_area, buf);
|
||||
|
||||
// Iterate through all elements in the `items` and stylize them.
|
||||
let items: Vec<ListItem> = self
|
||||
.items
|
||||
.items
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, todo_item)| todo_item.to_list_item(i))
|
||||
.collect();
|
||||
|
||||
// Create a List from all list items and highlight the currently selected one
|
||||
let items = List::new(items)
|
||||
.block(inner_block)
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.add_modifier(Modifier::REVERSED)
|
||||
.fg(SELECTED_STYLE_FG),
|
||||
)
|
||||
.highlight_symbol(">")
|
||||
.highlight_spacing(HighlightSpacing::Always);
|
||||
|
||||
// We can now render the item list
|
||||
// (look careful we are using StatefulWidget's render.)
|
||||
// ratatui::widgets::StatefulWidget::render as stateful_render
|
||||
StatefulWidget::render(items, inner_area, buf, &mut self.items.state);
|
||||
}
|
||||
|
||||
fn render_info(&self, area: Rect, buf: &mut Buffer) {
|
||||
// We get the info depending on the item's state.
|
||||
let info = if let Some(i) = self.items.state.selected() {
|
||||
match self.items.items[i].status {
|
||||
Status::Completed => "✓ DONE: ".to_string() + self.items.items[i].info,
|
||||
Status::Todo => "TODO: ".to_string() + self.items.items[i].info,
|
||||
}
|
||||
} else {
|
||||
"Nothing to see here...".to_string()
|
||||
};
|
||||
|
||||
// We show the list item's info under the list in this paragraph
|
||||
let outer_info_block = Block::default()
|
||||
.borders(Borders::NONE)
|
||||
.fg(TEXT_COLOR)
|
||||
.bg(TODO_HEADER_BG)
|
||||
.title("TODO Info")
|
||||
.title_alignment(Alignment::Center);
|
||||
let inner_info_block = Block::default()
|
||||
.borders(Borders::NONE)
|
||||
.bg(NORMAL_ROW_COLOR)
|
||||
.padding(Padding::horizontal(1));
|
||||
|
||||
// This is a similar process to what we did for list. outer_info_area will be used for
|
||||
// header inner_info_area will be used for the list info.
|
||||
let outer_info_area = area;
|
||||
let inner_info_area = outer_info_block.inner(outer_info_area);
|
||||
|
||||
// We can render the header. Inner info will be rendered later
|
||||
outer_info_block.render(outer_info_area, buf);
|
||||
|
||||
let info_paragraph = Paragraph::new(info)
|
||||
.block(inner_info_block)
|
||||
.fg(TEXT_COLOR)
|
||||
.wrap(Wrap { trim: false });
|
||||
|
||||
// We can now render the item info
|
||||
info_paragraph.render(inner_info_area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_title(area: Rect, buf: &mut Buffer) {
|
||||
Paragraph::new("Ratatui List Example")
|
||||
.bold()
|
||||
.centered()
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
fn render_footer(area: Rect, buf: &mut Buffer) {
|
||||
Paragraph::new("\nUse ↓↑ to move, ← to unselect, → to change status, g/G to go top/bottom.")
|
||||
.centered()
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
impl StatefulList<'_> {
|
||||
fn with_items<'a>(items: [(&'a str, &'a str, Status); 6]) -> StatefulList<'a> {
|
||||
StatefulList {
|
||||
state: ListState::default(),
|
||||
items,
|
||||
items: items.iter().map(TodoItem::from).collect(),
|
||||
last_selected: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +306,7 @@ impl<T> StatefulList<T> {
|
||||
i + 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
None => self.last_selected.unwrap_or(0),
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
@@ -47,232 +320,43 @@ impl<T> StatefulList<T> {
|
||||
i - 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
None => self.last_selected.unwrap_or(0),
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
|
||||
fn unselect(&mut self) {
|
||||
let offset = self.state.offset();
|
||||
self.last_selected = self.state.selected();
|
||||
self.state.select(None);
|
||||
*self.state.offset_mut() = offset;
|
||||
}
|
||||
}
|
||||
|
||||
/// This struct holds the current state of the app. In particular, it has the `items` field which is
|
||||
/// a wrapper around `ListState`. Keeping track of the items state let us render the associated
|
||||
/// widget with its state and have access to features such as natural scrolling.
|
||||
///
|
||||
/// Check the event handling at the bottom to see how to change the state on incoming events.
|
||||
/// Check the drawing logic for items on how to specify the highlighting style for selected items.
|
||||
struct App<'a> {
|
||||
items: StatefulList<(&'a str, usize)>,
|
||||
events: Vec<(&'a str, &'a str)>,
|
||||
}
|
||||
impl TodoItem<'_> {
|
||||
fn to_list_item(&self, index: usize) -> ListItem {
|
||||
let bg_color = match index % 2 {
|
||||
0 => NORMAL_ROW_COLOR,
|
||||
_ => ALT_ROW_COLOR,
|
||||
};
|
||||
let line = match self.status {
|
||||
Status::Todo => Line::styled(format!(" ☐ {}", self.todo), TEXT_COLOR),
|
||||
Status::Completed => Line::styled(
|
||||
format!(" ✓ {}", self.todo),
|
||||
(COMPLETED_TEXT_COLOR, bg_color),
|
||||
),
|
||||
};
|
||||
|
||||
impl<'a> App<'a> {
|
||||
fn new() -> App<'a> {
|
||||
App {
|
||||
items: StatefulList::with_items(vec![
|
||||
("Item0", 1),
|
||||
("Item1", 2),
|
||||
("Item2", 1),
|
||||
("Item3", 3),
|
||||
("Item4", 1),
|
||||
("Item5", 4),
|
||||
("Item6", 1),
|
||||
("Item7", 3),
|
||||
("Item8", 1),
|
||||
("Item9", 6),
|
||||
("Item10", 1),
|
||||
("Item11", 3),
|
||||
("Item12", 1),
|
||||
("Item13", 2),
|
||||
("Item14", 1),
|
||||
("Item15", 1),
|
||||
("Item16", 4),
|
||||
("Item17", 1),
|
||||
("Item18", 5),
|
||||
("Item19", 4),
|
||||
("Item20", 1),
|
||||
("Item21", 2),
|
||||
("Item22", 1),
|
||||
("Item23", 3),
|
||||
("Item24", 1),
|
||||
]),
|
||||
events: vec![
|
||||
("Event1", "INFO"),
|
||||
("Event2", "INFO"),
|
||||
("Event3", "CRITICAL"),
|
||||
("Event4", "ERROR"),
|
||||
("Event5", "INFO"),
|
||||
("Event6", "INFO"),
|
||||
("Event7", "WARNING"),
|
||||
("Event8", "INFO"),
|
||||
("Event9", "INFO"),
|
||||
("Event10", "INFO"),
|
||||
("Event11", "CRITICAL"),
|
||||
("Event12", "INFO"),
|
||||
("Event13", "INFO"),
|
||||
("Event14", "INFO"),
|
||||
("Event15", "INFO"),
|
||||
("Event16", "INFO"),
|
||||
("Event17", "ERROR"),
|
||||
("Event18", "ERROR"),
|
||||
("Event19", "INFO"),
|
||||
("Event20", "INFO"),
|
||||
("Event21", "WARNING"),
|
||||
("Event22", "INFO"),
|
||||
("Event23", "INFO"),
|
||||
("Event24", "WARNING"),
|
||||
("Event25", "INFO"),
|
||||
("Event26", "INFO"),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
/// Rotate through the event list.
|
||||
/// This only exists to simulate some kind of "progress"
|
||||
fn on_tick(&mut self) {
|
||||
let event = self.events.remove(0);
|
||||
self.events.push(event);
|
||||
ListItem::new(line).bg(bg_color)
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// create app and run it
|
||||
let tick_rate = Duration::from_millis(250);
|
||||
let app = App::new();
|
||||
let res = run_app(&mut terminal, app, tick_rate);
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> io::Result<()> {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &mut app))?;
|
||||
|
||||
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Left | KeyCode::Char('h') => app.items.unselect(),
|
||||
KeyCode::Down | KeyCode::Char('j') => app.items.next(),
|
||||
KeyCode::Up | KeyCode::Char('k') => app.items.previous(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
app.on_tick();
|
||||
last_tick = Instant::now();
|
||||
impl<'a> From<&(&'a str, &'a str, Status)> for TodoItem<'a> {
|
||||
fn from((todo, info, status): &(&'a str, &'a str, Status)) -> Self {
|
||||
Self {
|
||||
todo,
|
||||
info,
|
||||
status: *status,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui(f: &mut Frame, app: &mut App) {
|
||||
// Create two chunks with equal horizontal screen space
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
.split(f.size());
|
||||
|
||||
// Iterate through all elements in the `items` app and append some debug text to it.
|
||||
let items: Vec<ListItem> = app
|
||||
.items
|
||||
.items
|
||||
.iter()
|
||||
.map(|i| {
|
||||
let mut lines = vec![Line::from(i.0)];
|
||||
for _ in 0..i.1 {
|
||||
lines.push(
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit."
|
||||
.italic()
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
ListItem::new(lines).style(Style::default().fg(Color::Black).bg(Color::White))
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Create a List from all list items and highlight the currently selected one
|
||||
let items = List::new(items)
|
||||
.block(Block::default().borders(Borders::ALL).title("List"))
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.bg(Color::LightGreen)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)
|
||||
.highlight_symbol(">> ");
|
||||
|
||||
// We can now render the item list
|
||||
f.render_stateful_widget(items, chunks[0], &mut app.items.state);
|
||||
|
||||
// Let's do the same for the events.
|
||||
// The event list doesn't have any state and only displays the current state of the list.
|
||||
let events: Vec<ListItem> = app
|
||||
.events
|
||||
.iter()
|
||||
.rev()
|
||||
.map(|&(event, level)| {
|
||||
// Colorcode the level depending on its type
|
||||
let s = match level {
|
||||
"CRITICAL" => Style::default().fg(Color::Red),
|
||||
"ERROR" => Style::default().fg(Color::Magenta),
|
||||
"WARNING" => Style::default().fg(Color::Yellow),
|
||||
"INFO" => Style::default().fg(Color::Blue),
|
||||
_ => Style::default(),
|
||||
};
|
||||
// Add a example datetime and apply proper spacing between them
|
||||
let header = Line::from(vec![
|
||||
Span::styled(format!("{level:<9}"), s),
|
||||
" ".into(),
|
||||
"2020-01-01 10:00:00".italic(),
|
||||
]);
|
||||
// The event gets its own line
|
||||
let log = Line::from(vec![event.into()]);
|
||||
|
||||
// Here several things happen:
|
||||
// 1. Add a `---` spacing line above the final list entry
|
||||
// 2. Add the Level + datetime
|
||||
// 3. Add a spacer line
|
||||
// 4. Add the actual event
|
||||
ListItem::new(vec![
|
||||
Line::from("-".repeat(chunks[1].width as usize)),
|
||||
header,
|
||||
Line::from(""),
|
||||
log,
|
||||
])
|
||||
})
|
||||
.collect();
|
||||
let events_list = List::new(events)
|
||||
.block(Block::default().borders(Borders::ALL).title("List"))
|
||||
.direction(ListDirection::BottomToTop);
|
||||
f.render_widget(events_list, chunks[1]);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,22 @@
|
||||
/// This example is useful for testing how your terminal emulator handles different modifiers.
|
||||
/// It will render a grid of combinations of foreground and background colors with all
|
||||
/// modifiers applied to them.
|
||||
//! # [Ratatui] Modifiers example
|
||||
//!
|
||||
//! The latest version of this example is available in the [examples] folder in the repository.
|
||||
//!
|
||||
//! Please note that the examples are designed to be run against the `main` branch of the Github
|
||||
//! repository. This means that you may not be able to compile with the latest release version on
|
||||
//! crates.io, or the one that you have installed locally.
|
||||
//!
|
||||
//! See the [examples readme] for more information on finding examples that match the version of the
|
||||
//! library you are using.
|
||||
//!
|
||||
//! [Ratatui]: https://github.com/ratatui-org/ratatui
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
// This example is useful for testing how your terminal emulator handles different modifiers.
|
||||
// It will render a grid of combinations of foreground and background colors with all
|
||||
// modifiers applied to them.
|
||||
|
||||
use std::{
|
||||
error::Error,
|
||||
io::{self, Stdout},
|
||||
@@ -15,7 +31,7 @@ use crossterm::{
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
use ratatui::{prelude::*, widgets::Paragraph};
|
||||
|
||||
type Result<T> = result::Result<T, Box<dyn Error>>;
|
||||
|
||||
@@ -35,7 +51,7 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
|
||||
|
||||
if event::poll(Duration::from_millis(250))? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
if key.code == KeyCode::Char('q') {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
@@ -44,24 +60,18 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
|
||||
}
|
||||
|
||||
fn ui(frame: &mut Frame) {
|
||||
let layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Length(1), Constraint::Min(0)])
|
||||
.split(frame.size());
|
||||
let vertical = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
|
||||
let [text_area, main_area] = vertical.areas(frame.size());
|
||||
frame.render_widget(
|
||||
Paragraph::new("Note: not all terminals support all modifiers")
|
||||
.style(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
|
||||
layout[0],
|
||||
text_area,
|
||||
);
|
||||
let layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Length(1); 50])
|
||||
.split(layout[1])
|
||||
let layout = Layout::vertical([Constraint::Length(1); 50])
|
||||
.split(main_area)
|
||||
.iter()
|
||||
.flat_map(|area| {
|
||||
Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(20); 5])
|
||||
Layout::horizontal([Constraint::Percentage(20); 5])
|
||||
.split(*area)
|
||||
.to_vec()
|
||||
})
|
||||
@@ -78,8 +88,8 @@ fn ui(frame: &mut Frame) {
|
||||
.chain(Modifier::all().iter())
|
||||
.collect_vec();
|
||||
let mut index = 0;
|
||||
for bg in colors.iter() {
|
||||
for fg in colors.iter() {
|
||||
for bg in &colors {
|
||||
for fg in &colors {
|
||||
for modifier in &all_modifiers {
|
||||
let modifier_name = format!("{modifier:11?}");
|
||||
let padding = (" ").repeat(12 - modifier_name.len());
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
//! # [Ratatui] Panic Hook example
|
||||
//!
|
||||
//! The latest version of this example is available in the [examples] folder in the repository.
|
||||
//!
|
||||
//! Please note that the examples are designed to be run against the `main` branch of the Github
|
||||
//! repository. This means that you may not be able to compile with the latest release version on
|
||||
//! crates.io, or the one that you have installed locally.
|
||||
//!
|
||||
//! See the [examples readme] for more information on finding examples that match the version of the
|
||||
//! library you are using.
|
||||
//!
|
||||
//! [Ratatui]: https://github.com/ratatui-org/ratatui
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
//! How to use a panic hook to reset the terminal before printing the panic to
|
||||
//! the terminal.
|
||||
//!
|
||||
@@ -20,7 +35,10 @@ use crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{Block, Borders, Paragraph},
|
||||
};
|
||||
|
||||
type Result<T> = std::result::Result<T, Box<dyn Error>>;
|
||||
|
||||
@@ -128,7 +146,7 @@ fn ui(f: &mut Frame, app: &App) {
|
||||
.title("Panic Handler Demo")
|
||||
.borders(Borders::ALL);
|
||||
|
||||
let p = Paragraph::new(text).block(b).alignment(Alignment::Center);
|
||||
let p = Paragraph::new(text).block(b).centered();
|
||||
|
||||
f.render_widget(p, f.size());
|
||||
}
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
//! # [Ratatui] Paragraph example
|
||||
//!
|
||||
//! The latest version of this example is available in the [examples] folder in the repository.
|
||||
//!
|
||||
//! Please note that the examples are designed to be run against the `main` branch of the Github
|
||||
//! repository. This means that you may not be able to compile with the latest release version on
|
||||
//! crates.io, or the one that you have installed locally.
|
||||
//!
|
||||
//! See the [examples readme] for more information on finding examples that match the version of the
|
||||
//! library you are using.
|
||||
//!
|
||||
//! [Ratatui]: https://github.com/ratatui-org/ratatui
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
@@ -9,15 +24,18 @@ use crossterm::{
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{Block, Borders, Paragraph, Wrap},
|
||||
};
|
||||
|
||||
struct App {
|
||||
scroll: u16,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn new() -> App {
|
||||
App { scroll: 0 }
|
||||
const fn new() -> Self {
|
||||
Self { scroll: 0 }
|
||||
}
|
||||
|
||||
fn on_tick(&mut self) {
|
||||
@@ -67,7 +85,7 @@ fn run_app<B: Backend>(
|
||||
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
if key.code == KeyCode::Char('q') {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
@@ -90,15 +108,7 @@ fn ui(f: &mut Frame, app: &App) {
|
||||
let block = Block::default().black();
|
||||
f.render_widget(block, size);
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
])
|
||||
.split(size);
|
||||
let layout = Layout::vertical([Constraint::Ratio(1, 4); 4]).split(size);
|
||||
|
||||
let text = vec![
|
||||
Line::from("This is a line "),
|
||||
@@ -129,26 +139,26 @@ fn ui(f: &mut Frame, app: &App) {
|
||||
let paragraph = Paragraph::new(text.clone())
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.block(create_block("Default alignment (Left), no wrap"));
|
||||
f.render_widget(paragraph, chunks[0]);
|
||||
f.render_widget(paragraph, layout[0]);
|
||||
|
||||
let paragraph = Paragraph::new(text.clone())
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.block(create_block("Default alignment (Left), with wrap"))
|
||||
.wrap(Wrap { trim: true });
|
||||
f.render_widget(paragraph, chunks[1]);
|
||||
f.render_widget(paragraph, layout[1]);
|
||||
|
||||
let paragraph = Paragraph::new(text.clone())
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.block(create_block("Right alignment, with wrap"))
|
||||
.alignment(Alignment::Right)
|
||||
.right_aligned()
|
||||
.wrap(Wrap { trim: true });
|
||||
f.render_widget(paragraph, chunks[2]);
|
||||
f.render_widget(paragraph, layout[2]);
|
||||
|
||||
let paragraph = Paragraph::new(text)
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.block(create_block("Center alignment, with wrap, with scroll"))
|
||||
.alignment(Alignment::Center)
|
||||
.centered()
|
||||
.wrap(Wrap { trim: true })
|
||||
.scroll((app.scroll, 0));
|
||||
f.render_widget(paragraph, chunks[3]);
|
||||
f.render_widget(paragraph, layout[3]);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
//! # [Ratatui] Popup example
|
||||
//!
|
||||
//! The latest version of this example is available in the [examples] folder in the repository.
|
||||
//!
|
||||
//! Please note that the examples are designed to be run against the `main` branch of the Github
|
||||
//! repository. This means that you may not be able to compile with the latest release version on
|
||||
//! crates.io, or the one that you have installed locally.
|
||||
//!
|
||||
//! See the [examples readme] for more information on finding examples that match the version of the
|
||||
//! library you are using.
|
||||
//!
|
||||
//! [Ratatui]: https://github.com/ratatui-org/ratatui
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
// See also https://github.com/joshka/tui-popup and
|
||||
// https://github.com/sephiroth74/tui-confirm-dialog
|
||||
|
||||
use std::{error::Error, io};
|
||||
|
||||
use crossterm::{
|
||||
@@ -5,15 +23,18 @@ use crossterm::{
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{Block, Borders, Clear, Paragraph, Wrap},
|
||||
};
|
||||
|
||||
struct App {
|
||||
show_popup: bool,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn new() -> App {
|
||||
App { show_popup: false }
|
||||
const fn new() -> Self {
|
||||
Self { show_popup: false }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,11 +83,10 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
|
||||
}
|
||||
|
||||
fn ui(f: &mut Frame, app: &App) {
|
||||
let size = f.size();
|
||||
let area = f.size();
|
||||
|
||||
let chunks = Layout::default()
|
||||
.constraints([Constraint::Percentage(20), Constraint::Percentage(80)])
|
||||
.split(size);
|
||||
let vertical = Layout::vertical([Constraint::Percentage(20), Constraint::Percentage(80)]);
|
||||
let [instructions, content] = vertical.areas(area);
|
||||
|
||||
let text = if app.show_popup {
|
||||
"Press p to close the popup"
|
||||
@@ -74,19 +94,19 @@ fn ui(f: &mut Frame, app: &App) {
|
||||
"Press p to show the popup"
|
||||
};
|
||||
let paragraph = Paragraph::new(text.slow_blink())
|
||||
.alignment(Alignment::Center)
|
||||
.centered()
|
||||
.wrap(Wrap { trim: true });
|
||||
f.render_widget(paragraph, chunks[0]);
|
||||
f.render_widget(paragraph, instructions);
|
||||
|
||||
let block = Block::default()
|
||||
.title("Content")
|
||||
.borders(Borders::ALL)
|
||||
.on_blue();
|
||||
f.render_widget(block, chunks[1]);
|
||||
f.render_widget(block, content);
|
||||
|
||||
if app.show_popup {
|
||||
let block = Block::default().title("Popup").borders(Borders::ALL);
|
||||
let area = centered_rect(60, 20, size);
|
||||
let area = centered_rect(60, 20, area);
|
||||
f.render_widget(Clear, area); //this clears out the background
|
||||
f.render_widget(block, area);
|
||||
}
|
||||
@@ -94,21 +114,17 @@ fn ui(f: &mut Frame, app: &App) {
|
||||
|
||||
/// helper function to create a centered rect using up certain percentage of the available rect `r`
|
||||
fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
|
||||
let popup_layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Percentage((100 - percent_y) / 2),
|
||||
Constraint::Percentage(percent_y),
|
||||
Constraint::Percentage((100 - percent_y) / 2),
|
||||
])
|
||||
.split(r);
|
||||
let popup_layout = Layout::vertical([
|
||||
Constraint::Percentage((100 - percent_y) / 2),
|
||||
Constraint::Percentage(percent_y),
|
||||
Constraint::Percentage((100 - percent_y) / 2),
|
||||
])
|
||||
.split(r);
|
||||
|
||||
Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([
|
||||
Constraint::Percentage((100 - percent_x) / 2),
|
||||
Constraint::Percentage(percent_x),
|
||||
Constraint::Percentage((100 - percent_x) / 2),
|
||||
])
|
||||
.split(popup_layout[1])[1]
|
||||
Layout::horizontal([
|
||||
Constraint::Percentage((100 - percent_x) / 2),
|
||||
Constraint::Percentage(percent_x),
|
||||
Constraint::Percentage((100 - percent_x) / 2),
|
||||
])
|
||||
.split(popup_layout[1])[1]
|
||||
}
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
//! # [Ratatui] Logo example
|
||||
//!
|
||||
//! The latest version of this example is available in the [examples] folder in the repository.
|
||||
//!
|
||||
//! Please note that the examples are designed to be run against the `main` branch of the Github
|
||||
//! repository. This means that you may not be able to compile with the latest release version on
|
||||
//! crates.io, or the one that you have installed locally.
|
||||
//!
|
||||
//! See the [examples readme] for more information on finding examples that match the version of the
|
||||
//! library you are using.
|
||||
//!
|
||||
//! [Ratatui]: https://github.com/ratatui-org/ratatui
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
use std::{
|
||||
io::{self, stdout},
|
||||
thread::sleep,
|
||||
@@ -7,49 +22,46 @@ use std::{
|
||||
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
||||
use indoc::indoc;
|
||||
use itertools::izip;
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
use ratatui::{prelude::*, widgets::Paragraph};
|
||||
|
||||
/// A fun example of using half block characters to draw a logo
|
||||
fn main() -> io::Result<()> {
|
||||
#[allow(clippy::many_single_char_names)]
|
||||
fn logo() -> String {
|
||||
let r = indoc! {"
|
||||
▄▄▄
|
||||
█▄▄▀
|
||||
█ █
|
||||
"}
|
||||
.lines();
|
||||
"};
|
||||
let a = indoc! {"
|
||||
▄▄
|
||||
█▄▄█
|
||||
█ █
|
||||
"}
|
||||
.lines();
|
||||
"};
|
||||
let t = indoc! {"
|
||||
▄▄▄
|
||||
█
|
||||
█
|
||||
"}
|
||||
.lines();
|
||||
"};
|
||||
let u = indoc! {"
|
||||
▄ ▄
|
||||
█ █
|
||||
▀▄▄▀
|
||||
"}
|
||||
.lines();
|
||||
"};
|
||||
let i = indoc! {"
|
||||
▄
|
||||
█
|
||||
█
|
||||
"}
|
||||
.lines();
|
||||
"};
|
||||
izip!(r.lines(), a.lines(), t.lines(), u.lines(), i.lines())
|
||||
.map(|(r, a, t, u, i)| format!("{r:5}{a:5}{t:4}{a:5}{t:4}{u:5}{i:5}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
fn main() -> io::Result<()> {
|
||||
let mut terminal = init()?;
|
||||
terminal.draw(|frame| {
|
||||
let logo = izip!(r, a.clone(), t.clone(), a, t, u, i)
|
||||
.map(|(r, a, t, a2, t2, u, i)| {
|
||||
format!("{:5}{:5}{:4}{:5}{:4}{:5}{:5}", r, a, t, a2, t2, u, i)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
frame.render_widget(Paragraph::new(logo), frame.size());
|
||||
frame.render_widget(Paragraph::new(logo()), frame.size());
|
||||
})?;
|
||||
sleep(Duration::from_secs(5));
|
||||
restore()?;
|
||||
@@ -57,7 +69,7 @@ fn main() -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn init() -> io::Result<Terminal<impl Backend>> {
|
||||
fn init() -> io::Result<Terminal<impl Backend>> {
|
||||
enable_raw_mode()?;
|
||||
let options = TerminalOptions {
|
||||
viewport: Viewport::Inline(3),
|
||||
@@ -65,7 +77,7 @@ pub fn init() -> io::Result<Terminal<impl Backend>> {
|
||||
Terminal::with_options(CrosstermBackend::new(stdout()), options)
|
||||
}
|
||||
|
||||
pub fn restore() -> io::Result<()> {
|
||||
fn restore() -> io::Result<()> {
|
||||
disable_raw_mode()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
//! # [Ratatui] Scrollbar example
|
||||
//!
|
||||
//! The latest version of this example is available in the [examples] folder in the repository.
|
||||
//!
|
||||
//! Please note that the examples are designed to be run against the `main` branch of the Github
|
||||
//! repository. This means that you may not be able to compile with the latest release version on
|
||||
//! crates.io, or the one that you have installed locally.
|
||||
//!
|
||||
//! See the [examples readme] for more information on finding examples that match the version of the
|
||||
//! library you are using.
|
||||
//!
|
||||
//! [Ratatui]: https://github.com/ratatui-org/ratatui
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
#![warn(clippy::pedantic)]
|
||||
#![allow(clippy::wildcard_imports)]
|
||||
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
@@ -62,22 +80,22 @@ fn run_app<B: Backend>(
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Char('j') => {
|
||||
KeyCode::Char('j') | KeyCode::Down => {
|
||||
app.vertical_scroll = app.vertical_scroll.saturating_add(1);
|
||||
app.vertical_scroll_state =
|
||||
app.vertical_scroll_state.position(app.vertical_scroll);
|
||||
}
|
||||
KeyCode::Char('k') => {
|
||||
KeyCode::Char('k') | KeyCode::Up => {
|
||||
app.vertical_scroll = app.vertical_scroll.saturating_sub(1);
|
||||
app.vertical_scroll_state =
|
||||
app.vertical_scroll_state.position(app.vertical_scroll);
|
||||
}
|
||||
KeyCode::Char('h') => {
|
||||
KeyCode::Char('h') | KeyCode::Left => {
|
||||
app.horizontal_scroll = app.horizontal_scroll.saturating_sub(1);
|
||||
app.horizontal_scroll_state =
|
||||
app.horizontal_scroll_state.position(app.horizontal_scroll);
|
||||
}
|
||||
KeyCode::Char('l') => {
|
||||
KeyCode::Char('l') | KeyCode::Right => {
|
||||
app.horizontal_scroll = app.horizontal_scroll.saturating_add(1);
|
||||
app.horizontal_scroll_state =
|
||||
app.horizontal_scroll_state.position(app.horizontal_scroll);
|
||||
@@ -92,6 +110,7 @@ fn run_app<B: Backend>(
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines, clippy::cast_possible_truncation)]
|
||||
fn ui(f: &mut Frame, app: &mut App) {
|
||||
let size = f.size();
|
||||
|
||||
@@ -100,19 +119,14 @@ fn ui(f: &mut Frame, app: &mut App) {
|
||||
let mut long_line = s.repeat(usize::from(size.width) / s.len() + 4);
|
||||
long_line.push('\n');
|
||||
|
||||
let block = Block::default().black();
|
||||
f.render_widget(block, size);
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Min(1),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
])
|
||||
.split(size);
|
||||
let chunks = Layout::vertical([
|
||||
Constraint::Min(1),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
])
|
||||
.split(size);
|
||||
|
||||
let text = vec![
|
||||
Line::from("This is a line "),
|
||||
@@ -123,10 +137,7 @@ fn ui(f: &mut Frame, app: &mut App) {
|
||||
Line::from("This is a line".reset()),
|
||||
Line::from(vec![
|
||||
Span::raw("Masked text: "),
|
||||
Span::styled(
|
||||
Masked::new("password", '*'),
|
||||
Style::default().fg(Color::Red),
|
||||
),
|
||||
Span::styled(Masked::new("password", '*'), Style::new().fg(Color::Red)),
|
||||
]),
|
||||
Line::from("This is a line "),
|
||||
Line::from("This is a line ".red()),
|
||||
@@ -136,28 +147,17 @@ fn ui(f: &mut Frame, app: &mut App) {
|
||||
Line::from("This is a line".reset()),
|
||||
Line::from(vec![
|
||||
Span::raw("Masked text: "),
|
||||
Span::styled(
|
||||
Masked::new("password", '*'),
|
||||
Style::default().fg(Color::Red),
|
||||
),
|
||||
Span::styled(Masked::new("password", '*'), Style::new().fg(Color::Red)),
|
||||
]),
|
||||
];
|
||||
app.vertical_scroll_state = app.vertical_scroll_state.content_length(text.len());
|
||||
app.horizontal_scroll_state = app.horizontal_scroll_state.content_length(long_line.len());
|
||||
|
||||
let create_block = |title| {
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.gray()
|
||||
.title(Span::styled(
|
||||
title,
|
||||
Style::default().add_modifier(Modifier::BOLD),
|
||||
))
|
||||
};
|
||||
let create_block = |title: &'static str| Block::bordered().gray().title(title.bold());
|
||||
|
||||
let title = Block::default()
|
||||
.title("Use h j k l to scroll ◄ ▲ ▼ ►")
|
||||
.title_alignment(Alignment::Center);
|
||||
let title = Block::new()
|
||||
.title_alignment(Alignment::Center)
|
||||
.title("Use h j k l or ◄ ▲ ▼ ► to scroll ".bold());
|
||||
f.render_widget(title, chunks[0]);
|
||||
|
||||
let paragraph = Paragraph::new(text.clone())
|
||||
@@ -166,8 +166,7 @@ fn ui(f: &mut Frame, app: &mut App) {
|
||||
.scroll((app.vertical_scroll as u16, 0));
|
||||
f.render_widget(paragraph, chunks[1]);
|
||||
f.render_stateful_widget(
|
||||
Scrollbar::default()
|
||||
.orientation(ScrollbarOrientation::VerticalRight)
|
||||
Scrollbar::new(ScrollbarOrientation::VerticalRight)
|
||||
.begin_symbol(Some("↑"))
|
||||
.end_symbol(Some("↓")),
|
||||
chunks[1],
|
||||
@@ -182,8 +181,7 @@ fn ui(f: &mut Frame, app: &mut App) {
|
||||
.scroll((app.vertical_scroll as u16, 0));
|
||||
f.render_widget(paragraph, chunks[2]);
|
||||
f.render_stateful_widget(
|
||||
Scrollbar::default()
|
||||
.orientation(ScrollbarOrientation::VerticalLeft)
|
||||
Scrollbar::new(ScrollbarOrientation::VerticalLeft)
|
||||
.symbols(scrollbar::VERTICAL)
|
||||
.begin_symbol(None)
|
||||
.track_symbol(None)
|
||||
@@ -203,8 +201,7 @@ fn ui(f: &mut Frame, app: &mut App) {
|
||||
.scroll((0, app.horizontal_scroll as u16));
|
||||
f.render_widget(paragraph, chunks[3]);
|
||||
f.render_stateful_widget(
|
||||
Scrollbar::default()
|
||||
.orientation(ScrollbarOrientation::HorizontalBottom)
|
||||
Scrollbar::new(ScrollbarOrientation::HorizontalBottom)
|
||||
.thumb_symbol("🬋")
|
||||
.end_symbol(None),
|
||||
chunks[3].inner(&Margin {
|
||||
@@ -222,8 +219,7 @@ fn ui(f: &mut Frame, app: &mut App) {
|
||||
.scroll((0, app.horizontal_scroll as u16));
|
||||
f.render_widget(paragraph, chunks[4]);
|
||||
f.render_stateful_widget(
|
||||
Scrollbar::default()
|
||||
.orientation(ScrollbarOrientation::HorizontalBottom)
|
||||
Scrollbar::new(ScrollbarOrientation::HorizontalBottom)
|
||||
.thumb_symbol("░")
|
||||
.track_symbol(Some("─")),
|
||||
chunks[4].inner(&Margin {
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
//! # [Ratatui] Sparkline example
|
||||
//!
|
||||
//! The latest version of this example is available in the [examples] folder in the repository.
|
||||
//!
|
||||
//! Please note that the examples are designed to be run against the `main` branch of the Github
|
||||
//! repository. This means that you may not be able to compile with the latest release version on
|
||||
//! crates.io, or the one that you have installed locally.
|
||||
//!
|
||||
//! See the [examples readme] for more information on finding examples that match the version of the
|
||||
//! library you are using.
|
||||
//!
|
||||
//! [Ratatui]: https://github.com/ratatui-org/ratatui
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
@@ -13,17 +28,20 @@ use rand::{
|
||||
distributions::{Distribution, Uniform},
|
||||
rngs::ThreadRng,
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{Block, Borders, Sparkline},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RandomSignal {
|
||||
struct RandomSignal {
|
||||
distribution: Uniform<u64>,
|
||||
rng: ThreadRng,
|
||||
}
|
||||
|
||||
impl RandomSignal {
|
||||
pub fn new(lower: u64, upper: u64) -> RandomSignal {
|
||||
RandomSignal {
|
||||
fn new(lower: u64, upper: u64) -> Self {
|
||||
Self {
|
||||
distribution: Uniform::new(lower, upper),
|
||||
rng: rand::thread_rng(),
|
||||
}
|
||||
@@ -45,12 +63,12 @@ struct App {
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn new() -> App {
|
||||
fn new() -> Self {
|
||||
let mut signal = RandomSignal::new(0, 100);
|
||||
let data1 = signal.by_ref().take(200).collect::<Vec<u64>>();
|
||||
let data2 = signal.by_ref().take(200).collect::<Vec<u64>>();
|
||||
let data3 = signal.by_ref().take(200).collect::<Vec<u64>>();
|
||||
App {
|
||||
Self {
|
||||
signal,
|
||||
data1,
|
||||
data2,
|
||||
@@ -112,7 +130,7 @@ fn run_app<B: Backend>(
|
||||
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
if key.code == KeyCode::Char('q') {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
@@ -125,14 +143,12 @@ fn run_app<B: Backend>(
|
||||
}
|
||||
|
||||
fn ui(f: &mut Frame, app: &App) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Min(0),
|
||||
])
|
||||
.split(f.size());
|
||||
let chunks = Layout::vertical([
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Min(0),
|
||||
])
|
||||
.split(f.size());
|
||||
let sparkline = Sparkline::default()
|
||||
.block(
|
||||
Block::default()
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
//! # [Ratatui] Table example
|
||||
//!
|
||||
//! The latest version of this example is available in the [examples] folder in the repository.
|
||||
//!
|
||||
//! Please note that the examples are designed to be run against the `main` branch of the Github
|
||||
//! repository. This means that you may not be able to compile with the latest release version on
|
||||
//! crates.io, or the one that you have installed locally.
|
||||
//!
|
||||
//! See the [examples readme] for more information on finding examples that match the version of the
|
||||
//! library you are using.
|
||||
//!
|
||||
//! [Ratatui]: https://github.com/ratatui-org/ratatui
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
#![allow(clippy::enum_glob_use, clippy::wildcard_imports)]
|
||||
|
||||
use std::{error::Error, io};
|
||||
|
||||
use crossterm::{
|
||||
@@ -5,38 +22,91 @@ use crossterm::{
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
use style::palette::tailwind;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
struct App<'a> {
|
||||
state: TableState,
|
||||
items: Vec<Vec<&'a str>>,
|
||||
const PALETTES: [tailwind::Palette; 4] = [
|
||||
tailwind::BLUE,
|
||||
tailwind::EMERALD,
|
||||
tailwind::INDIGO,
|
||||
tailwind::RED,
|
||||
];
|
||||
const INFO_TEXT: &str =
|
||||
"(Esc) quit | (↑) move up | (↓) move down | (→) next color | (←) previous color";
|
||||
|
||||
const ITEM_HEIGHT: usize = 4;
|
||||
|
||||
struct TableColors {
|
||||
buffer_bg: Color,
|
||||
header_bg: Color,
|
||||
header_fg: Color,
|
||||
row_fg: Color,
|
||||
selected_style_fg: Color,
|
||||
normal_row_color: Color,
|
||||
alt_row_color: Color,
|
||||
footer_border_color: Color,
|
||||
}
|
||||
|
||||
impl<'a> App<'a> {
|
||||
fn new() -> App<'a> {
|
||||
App {
|
||||
state: TableState::default(),
|
||||
items: vec![
|
||||
vec!["Row11", "Row12", "Row13"],
|
||||
vec!["Row21", "Row22", "Row23"],
|
||||
vec!["Row31", "Row32", "Row33"],
|
||||
vec!["Row41", "Row42", "Row43"],
|
||||
vec!["Row51", "Row52", "Row53"],
|
||||
vec!["Row61", "Row62\nTest", "Row63"],
|
||||
vec!["Row71", "Row72", "Row73"],
|
||||
vec!["Row81", "Row82", "Row83"],
|
||||
vec!["Row91", "Row92", "Row93"],
|
||||
vec!["Row101", "Row102", "Row103"],
|
||||
vec!["Row111", "Row112", "Row113"],
|
||||
vec!["Row121", "Row122", "Row123"],
|
||||
vec!["Row131", "Row132", "Row133"],
|
||||
vec!["Row141", "Row142", "Row143"],
|
||||
vec!["Row151", "Row152", "Row153"],
|
||||
vec!["Row161", "Row162", "Row163"],
|
||||
vec!["Row171", "Row172", "Row173"],
|
||||
vec!["Row181", "Row182", "Row183"],
|
||||
vec!["Row191", "Row192", "Row193"],
|
||||
],
|
||||
impl TableColors {
|
||||
const fn new(color: &tailwind::Palette) -> Self {
|
||||
Self {
|
||||
buffer_bg: tailwind::SLATE.c950,
|
||||
header_bg: color.c900,
|
||||
header_fg: tailwind::SLATE.c200,
|
||||
row_fg: tailwind::SLATE.c200,
|
||||
selected_style_fg: color.c400,
|
||||
normal_row_color: tailwind::SLATE.c950,
|
||||
alt_row_color: tailwind::SLATE.c900,
|
||||
footer_border_color: color.c400,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Data {
|
||||
name: String,
|
||||
address: String,
|
||||
email: String,
|
||||
}
|
||||
|
||||
impl Data {
|
||||
const fn ref_array(&self) -> [&String; 3] {
|
||||
[&self.name, &self.address, &self.email]
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn address(&self) -> &str {
|
||||
&self.address
|
||||
}
|
||||
|
||||
fn email(&self) -> &str {
|
||||
&self.email
|
||||
}
|
||||
}
|
||||
|
||||
struct App {
|
||||
state: TableState,
|
||||
items: Vec<Data>,
|
||||
longest_item_lens: (u16, u16, u16), // order is (name, address, email)
|
||||
scroll_state: ScrollbarState,
|
||||
colors: TableColors,
|
||||
color_index: usize,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn new() -> Self {
|
||||
let data_vec = generate_fake_names();
|
||||
Self {
|
||||
state: TableState::default().with_selected(0),
|
||||
longest_item_lens: constraint_len_calculator(&data_vec),
|
||||
scroll_state: ScrollbarState::new((data_vec.len() - 1) * ITEM_HEIGHT),
|
||||
colors: TableColors::new(&PALETTES[0]),
|
||||
color_index: 0,
|
||||
items: data_vec,
|
||||
}
|
||||
}
|
||||
pub fn next(&mut self) {
|
||||
@@ -51,6 +121,7 @@ impl<'a> App<'a> {
|
||||
None => 0,
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
self.scroll_state = self.scroll_state.position(i * ITEM_HEIGHT);
|
||||
}
|
||||
|
||||
pub fn previous(&mut self) {
|
||||
@@ -65,7 +136,46 @@ impl<'a> App<'a> {
|
||||
None => 0,
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
self.scroll_state = self.scroll_state.position(i * ITEM_HEIGHT);
|
||||
}
|
||||
|
||||
pub fn next_color(&mut self) {
|
||||
self.color_index = (self.color_index + 1) % PALETTES.len();
|
||||
}
|
||||
|
||||
pub fn previous_color(&mut self) {
|
||||
let count = PALETTES.len();
|
||||
self.color_index = (self.color_index + count - 1) % count;
|
||||
}
|
||||
|
||||
pub fn set_colors(&mut self) {
|
||||
self.colors = TableColors::new(&PALETTES[self.color_index]);
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_fake_names() -> Vec<Data> {
|
||||
use fakeit::{address, contact, name};
|
||||
|
||||
(0..20)
|
||||
.map(|_| {
|
||||
let name = name::full();
|
||||
let address = format!(
|
||||
"{}\n{}, {} {}",
|
||||
address::street(),
|
||||
address::city(),
|
||||
address::state(),
|
||||
address::zip()
|
||||
);
|
||||
let email = contact::email();
|
||||
|
||||
Data {
|
||||
name,
|
||||
address,
|
||||
email,
|
||||
}
|
||||
})
|
||||
.sorted_by(|a, b| a.name.cmp(&b.name))
|
||||
.collect_vec()
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
@@ -102,10 +212,13 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
use KeyCode::*;
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Down | KeyCode::Char('j') => app.next(),
|
||||
KeyCode::Up | KeyCode::Char('k') => app.previous(),
|
||||
Char('q') | Esc => return Ok(()),
|
||||
Char('j') | Down => app.next(),
|
||||
Char('k') | Up => app.previous(),
|
||||
Char('l') | Right => app.next_color(),
|
||||
Char('h') | Left => app.previous_color(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -114,40 +227,142 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
|
||||
}
|
||||
|
||||
fn ui(f: &mut Frame, app: &mut App) {
|
||||
let rects = Layout::default()
|
||||
.constraints([Constraint::Percentage(100)])
|
||||
.split(f.size());
|
||||
let rects = Layout::vertical([Constraint::Min(5), Constraint::Length(3)]).split(f.size());
|
||||
|
||||
let selected_style = Style::default().add_modifier(Modifier::REVERSED);
|
||||
let normal_style = Style::default().bg(Color::Blue);
|
||||
let header_cells = ["Header1", "Header2", "Header3"]
|
||||
.iter()
|
||||
.map(|h| Cell::from(*h).style(Style::default().fg(Color::Red)));
|
||||
let header = Row::new(header_cells)
|
||||
.style(normal_style)
|
||||
.height(1)
|
||||
.bottom_margin(1);
|
||||
let rows = app.items.iter().map(|item| {
|
||||
let height = item
|
||||
.iter()
|
||||
.map(|content| content.chars().filter(|c| *c == '\n').count())
|
||||
.max()
|
||||
.unwrap_or(0)
|
||||
+ 1;
|
||||
let cells = item.iter().map(|c| Cell::from(*c));
|
||||
Row::new(cells).height(height as u16).bottom_margin(1)
|
||||
app.set_colors();
|
||||
|
||||
render_table(f, app, rects[0]);
|
||||
|
||||
render_scrollbar(f, app, rects[0]);
|
||||
|
||||
render_footer(f, app, rects[1]);
|
||||
}
|
||||
|
||||
fn render_table(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
let header_style = Style::default()
|
||||
.fg(app.colors.header_fg)
|
||||
.bg(app.colors.header_bg);
|
||||
let selected_style = Style::default()
|
||||
.add_modifier(Modifier::REVERSED)
|
||||
.fg(app.colors.selected_style_fg);
|
||||
|
||||
let header = ["Name", "Address", "Email"]
|
||||
.into_iter()
|
||||
.map(Cell::from)
|
||||
.collect::<Row>()
|
||||
.style(header_style)
|
||||
.height(1);
|
||||
let rows = app.items.iter().enumerate().map(|(i, data)| {
|
||||
let color = match i % 2 {
|
||||
0 => app.colors.normal_row_color,
|
||||
_ => app.colors.alt_row_color,
|
||||
};
|
||||
let item = data.ref_array();
|
||||
item.into_iter()
|
||||
.map(|content| Cell::from(Text::from(format!("\n{content}\n"))))
|
||||
.collect::<Row>()
|
||||
.style(Style::new().fg(app.colors.row_fg).bg(color))
|
||||
.height(4)
|
||||
});
|
||||
let bar = " █ ";
|
||||
let t = Table::new(
|
||||
rows,
|
||||
[
|
||||
Constraint::Percentage(50),
|
||||
Constraint::Max(30),
|
||||
Constraint::Min(10),
|
||||
// + 1 is for padding.
|
||||
Constraint::Length(app.longest_item_lens.0 + 1),
|
||||
Constraint::Min(app.longest_item_lens.1 + 1),
|
||||
Constraint::Min(app.longest_item_lens.2),
|
||||
],
|
||||
)
|
||||
.header(header)
|
||||
.block(Block::default().borders(Borders::ALL).title("Table"))
|
||||
.highlight_style(selected_style)
|
||||
.highlight_symbol(">> ");
|
||||
f.render_stateful_widget(t, rects[0], &mut app.state);
|
||||
.highlight_symbol(Text::from(vec![
|
||||
"".into(),
|
||||
bar.into(),
|
||||
bar.into(),
|
||||
"".into(),
|
||||
]))
|
||||
.bg(app.colors.buffer_bg)
|
||||
.highlight_spacing(HighlightSpacing::Always);
|
||||
f.render_stateful_widget(t, area, &mut app.state);
|
||||
}
|
||||
|
||||
fn constraint_len_calculator(items: &[Data]) -> (u16, u16, u16) {
|
||||
let name_len = items
|
||||
.iter()
|
||||
.map(Data::name)
|
||||
.map(UnicodeWidthStr::width)
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
let address_len = items
|
||||
.iter()
|
||||
.map(Data::address)
|
||||
.flat_map(str::lines)
|
||||
.map(UnicodeWidthStr::width)
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
let email_len = items
|
||||
.iter()
|
||||
.map(Data::email)
|
||||
.map(UnicodeWidthStr::width)
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
(name_len as u16, address_len as u16, email_len as u16)
|
||||
}
|
||||
|
||||
fn render_scrollbar(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
f.render_stateful_widget(
|
||||
Scrollbar::default()
|
||||
.orientation(ScrollbarOrientation::VerticalRight)
|
||||
.begin_symbol(None)
|
||||
.end_symbol(None),
|
||||
area.inner(&Margin {
|
||||
vertical: 1,
|
||||
horizontal: 1,
|
||||
}),
|
||||
&mut app.scroll_state,
|
||||
);
|
||||
}
|
||||
|
||||
fn render_footer(f: &mut Frame, app: &App, area: Rect) {
|
||||
let info_footer = Paragraph::new(Line::from(INFO_TEXT))
|
||||
.style(Style::new().fg(app.colors.row_fg).bg(app.colors.buffer_bg))
|
||||
.centered()
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::new().fg(app.colors.footer_border_color))
|
||||
.border_type(BorderType::Double),
|
||||
);
|
||||
f.render_widget(info_footer, area);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::Data;
|
||||
|
||||
#[test]
|
||||
fn constraint_len_calculator() {
|
||||
let test_data = vec![
|
||||
Data {
|
||||
name: "Emirhan Tala".to_string(),
|
||||
address: "Cambridgelaan 6XX\n3584 XX Utrecht".to_string(),
|
||||
email: "tala.emirhan@gmail.com".to_string(),
|
||||
},
|
||||
Data {
|
||||
name: "thistextis26characterslong".to_string(),
|
||||
address: "this line is 31 characters long\nbottom line is 33 characters long"
|
||||
.to_string(),
|
||||
email: "thisemailis40caharacterslong@ratatui.com".to_string(),
|
||||
},
|
||||
];
|
||||
let (longest_name_len, longest_address_len, longest_email_len) =
|
||||
crate::constraint_len_calculator(&test_data);
|
||||
|
||||
assert_eq!(26, longest_name_len);
|
||||
assert_eq!(33, longest_address_len);
|
||||
assert_eq!(40, longest_email_len);
|
||||
}
|
||||
}
|
||||
|
||||
312
examples/tabs.rs
312
examples/tabs.rs
@@ -1,112 +1,252 @@
|
||||
use std::{error::Error, io};
|
||||
//! # [Ratatui] Tabs example
|
||||
//!
|
||||
//! The latest version of this example is available in the [examples] folder in the repository.
|
||||
//!
|
||||
//! Please note that the examples are designed to be run against the `main` branch of the Github
|
||||
//! repository. This means that you may not be able to compile with the latest release version on
|
||||
//! crates.io, or the one that you have installed locally.
|
||||
//!
|
||||
//! See the [examples readme] for more information on finding examples that match the version of the
|
||||
//! library you are using.
|
||||
//!
|
||||
//! [Ratatui]: https://github.com/ratatui-org/ratatui
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
#![allow(clippy::wildcard_imports, clippy::enum_glob_use)]
|
||||
|
||||
use std::io::stdout;
|
||||
|
||||
use color_eyre::{config::HookBuilder, Result};
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
event::{self, Event, KeyCode, KeyEventKind},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
use ratatui::{prelude::*, style::palette::tailwind, widgets::*};
|
||||
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
|
||||
|
||||
struct App<'a> {
|
||||
pub titles: Vec<&'a str>,
|
||||
pub index: usize,
|
||||
#[derive(Default)]
|
||||
struct App {
|
||||
state: AppState,
|
||||
selected_tab: SelectedTab,
|
||||
}
|
||||
|
||||
impl<'a> App<'a> {
|
||||
fn new() -> App<'a> {
|
||||
App {
|
||||
titles: vec!["Tab0", "Tab1", "Tab2", "Tab3"],
|
||||
index: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next(&mut self) {
|
||||
self.index = (self.index + 1) % self.titles.len();
|
||||
}
|
||||
|
||||
pub fn previous(&mut self) {
|
||||
if self.index > 0 {
|
||||
self.index -= 1;
|
||||
} else {
|
||||
self.index = self.titles.len() - 1;
|
||||
}
|
||||
}
|
||||
#[derive(Default, Clone, Copy, PartialEq, Eq)]
|
||||
enum AppState {
|
||||
#[default]
|
||||
Running,
|
||||
Quitting,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// create app and run it
|
||||
let app = App::new();
|
||||
let res = run_app(&mut terminal, app);
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{err:?}");
|
||||
}
|
||||
#[derive(Default, Clone, Copy, Display, FromRepr, EnumIter)]
|
||||
enum SelectedTab {
|
||||
#[default]
|
||||
#[strum(to_string = "Tab 1")]
|
||||
Tab1,
|
||||
#[strum(to_string = "Tab 2")]
|
||||
Tab2,
|
||||
#[strum(to_string = "Tab 3")]
|
||||
Tab3,
|
||||
#[strum(to_string = "Tab 4")]
|
||||
Tab4,
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
init_error_hooks()?;
|
||||
let mut terminal = init_terminal()?;
|
||||
App::default().run(&mut terminal)?;
|
||||
restore_terminal()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<()> {
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &app))?;
|
||||
impl App {
|
||||
fn run(&mut self, terminal: &mut Terminal<impl Backend>) -> Result<()> {
|
||||
while self.state == AppState::Running {
|
||||
self.draw(terminal)?;
|
||||
self.handle_events()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw(&self, terminal: &mut Terminal<impl Backend>) -> Result<()> {
|
||||
terminal.draw(|frame| frame.render_widget(self, frame.size()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_events(&mut self) -> std::io::Result<()> {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
use KeyCode::*;
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Right | KeyCode::Char('l') => app.next(),
|
||||
KeyCode::Left | KeyCode::Char('h') => app.previous(),
|
||||
Char('l') | Right => self.next_tab(),
|
||||
Char('h') | Left => self.previous_tab(),
|
||||
Char('q') | Esc => self.quit(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn next_tab(&mut self) {
|
||||
self.selected_tab = self.selected_tab.next();
|
||||
}
|
||||
|
||||
pub fn previous_tab(&mut self) {
|
||||
self.selected_tab = self.selected_tab.previous();
|
||||
}
|
||||
|
||||
pub fn quit(&mut self) {
|
||||
self.state = AppState::Quitting;
|
||||
}
|
||||
}
|
||||
|
||||
fn ui(f: &mut Frame, app: &App) {
|
||||
let size = f.size();
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Length(3), Constraint::Min(0)])
|
||||
.split(size);
|
||||
impl SelectedTab {
|
||||
/// Get the previous tab, if there is no previous tab return the current tab.
|
||||
fn previous(self) -> Self {
|
||||
let current_index: usize = self as usize;
|
||||
let previous_index = current_index.saturating_sub(1);
|
||||
Self::from_repr(previous_index).unwrap_or(self)
|
||||
}
|
||||
|
||||
let block = Block::default().on_white().black();
|
||||
f.render_widget(block, size);
|
||||
let titles = app
|
||||
.titles
|
||||
.iter()
|
||||
.map(|t| {
|
||||
let (first, rest) = t.split_at(1);
|
||||
Line::from(vec![first.yellow(), rest.green()])
|
||||
})
|
||||
.collect();
|
||||
let tabs = Tabs::new(titles)
|
||||
.block(Block::default().borders(Borders::ALL).title("Tabs"))
|
||||
.select(app.index)
|
||||
.style(Style::default().cyan().on_gray())
|
||||
.highlight_style(Style::default().bold().on_black());
|
||||
f.render_widget(tabs, chunks[0]);
|
||||
let inner = match app.index {
|
||||
0 => Block::default().title("Inner 0").borders(Borders::ALL),
|
||||
1 => Block::default().title("Inner 1").borders(Borders::ALL),
|
||||
2 => Block::default().title("Inner 2").borders(Borders::ALL),
|
||||
3 => Block::default().title("Inner 3").borders(Borders::ALL),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
f.render_widget(inner, chunks[1]);
|
||||
/// Get the next tab, if there is no next tab return the current tab.
|
||||
fn next(self) -> Self {
|
||||
let current_index = self as usize;
|
||||
let next_index = current_index.saturating_add(1);
|
||||
Self::from_repr(next_index).unwrap_or(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for &App {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
use Constraint::*;
|
||||
let vertical = Layout::vertical([Length(1), Min(0), Length(1)]);
|
||||
let [header_area, inner_area, footer_area] = vertical.areas(area);
|
||||
|
||||
let horizontal = Layout::horizontal([Min(0), Length(20)]);
|
||||
let [tabs_area, title_area] = horizontal.areas(header_area);
|
||||
|
||||
render_title(title_area, buf);
|
||||
self.render_tabs(tabs_area, buf);
|
||||
self.selected_tab.render(inner_area, buf);
|
||||
render_footer(footer_area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn render_tabs(&self, area: Rect, buf: &mut Buffer) {
|
||||
let titles = SelectedTab::iter().map(SelectedTab::title);
|
||||
let highlight_style = (Color::default(), self.selected_tab.palette().c700);
|
||||
let selected_tab_index = self.selected_tab as usize;
|
||||
Tabs::new(titles)
|
||||
.highlight_style(highlight_style)
|
||||
.select(selected_tab_index)
|
||||
.padding("", "")
|
||||
.divider(" ")
|
||||
.render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_title(area: Rect, buf: &mut Buffer) {
|
||||
"Ratatui Tabs Example".bold().render(area, buf);
|
||||
}
|
||||
|
||||
fn render_footer(area: Rect, buf: &mut Buffer) {
|
||||
Line::raw("◄ ► to change tab | Press q to quit")
|
||||
.centered()
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
impl Widget for SelectedTab {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
// in a real app these might be separate widgets
|
||||
match self {
|
||||
Self::Tab1 => self.render_tab0(area, buf),
|
||||
Self::Tab2 => self.render_tab1(area, buf),
|
||||
Self::Tab3 => self.render_tab2(area, buf),
|
||||
Self::Tab4 => self.render_tab3(area, buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SelectedTab {
|
||||
/// Return tab's name as a styled `Line`
|
||||
fn title(self) -> Line<'static> {
|
||||
format!(" {self} ")
|
||||
.fg(tailwind::SLATE.c200)
|
||||
.bg(self.palette().c900)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn render_tab0(self, area: Rect, buf: &mut Buffer) {
|
||||
Paragraph::new("Hello, World!")
|
||||
.block(self.block())
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
fn render_tab1(self, area: Rect, buf: &mut Buffer) {
|
||||
Paragraph::new("Welcome to the Ratatui tabs example!")
|
||||
.block(self.block())
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
fn render_tab2(self, area: Rect, buf: &mut Buffer) {
|
||||
Paragraph::new("Look! I'm different than others!")
|
||||
.block(self.block())
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
fn render_tab3(self, area: Rect, buf: &mut Buffer) {
|
||||
Paragraph::new("I know, these are some basic changes. But I think you got the main idea.")
|
||||
.block(self.block())
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
/// A block surrounding the tab's content
|
||||
fn block(self) -> Block<'static> {
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_set(symbols::border::PROPORTIONAL_TALL)
|
||||
.padding(Padding::horizontal(1))
|
||||
.border_style(self.palette().c700)
|
||||
}
|
||||
|
||||
const fn palette(self) -> tailwind::Palette {
|
||||
match self {
|
||||
Self::Tab1 => tailwind::BLUE,
|
||||
Self::Tab2 => tailwind::EMERALD,
|
||||
Self::Tab3 => tailwind::INDIGO,
|
||||
Self::Tab4 => tailwind::RED,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn init_error_hooks() -> color_eyre::Result<()> {
|
||||
let (panic, error) = HookBuilder::default().into_hooks();
|
||||
let panic = panic.into_panic_hook();
|
||||
let error = error.into_eyre_hook();
|
||||
color_eyre::eyre::set_hook(Box::new(move |e| {
|
||||
let _ = restore_terminal();
|
||||
error(e)
|
||||
}))?;
|
||||
std::panic::set_hook(Box::new(move |info| {
|
||||
let _ = restore_terminal();
|
||||
panic(info);
|
||||
}));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn init_terminal() -> color_eyre::Result<Terminal<impl Backend>> {
|
||||
enable_raw_mode()?;
|
||||
stdout().execute(EnterAlternateScreen)?;
|
||||
let backend = CrosstermBackend::new(stdout());
|
||||
let terminal = Terminal::new(backend)?;
|
||||
Ok(terminal)
|
||||
}
|
||||
|
||||
fn restore_terminal() -> color_eyre::Result<()> {
|
||||
disable_raw_mode()?;
|
||||
stdout().execute(LeaveAlternateScreen)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,25 +1,43 @@
|
||||
//! # [Ratatui] User Input example
|
||||
//!
|
||||
//! The latest version of this example is available in the [examples] folder in the repository.
|
||||
//!
|
||||
//! Please note that the examples are designed to be run against the `main` branch of the Github
|
||||
//! repository. This means that you may not be able to compile with the latest release version on
|
||||
//! crates.io, or the one that you have installed locally.
|
||||
//!
|
||||
//! See the [examples readme] for more information on finding examples that match the version of the
|
||||
//! library you are using.
|
||||
//!
|
||||
//! [Ratatui]: https://github.com/ratatui-org/ratatui
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
// A simple example demonstrating how to handle user input. This is a bit out of the scope of
|
||||
// the library as it does not provide any input handling out of the box. However, it may helps
|
||||
// some to get started.
|
||||
//
|
||||
// This is a very simple example:
|
||||
// * An input box always focused. Every character you type is registered here.
|
||||
// * An entered character is inserted at the cursor position.
|
||||
// * Pressing Backspace erases the left character before the cursor position
|
||||
// * Pressing Enter pushes the current input in the history of previous messages. **Note: ** as
|
||||
// this is a relatively simple example unicode characters are unsupported and their use will
|
||||
// result in undefined behaviour.
|
||||
//
|
||||
// See also https://github.com/rhysd/tui-textarea and https://github.com/sayanarijit/tui-input/
|
||||
|
||||
use std::{error::Error, io};
|
||||
|
||||
/// A simple example demonstrating how to handle user input. This is
|
||||
/// a bit out of the scope of the library as it does not provide any
|
||||
/// input handling out of the box. However, it may helps some to get
|
||||
/// started.
|
||||
///
|
||||
/// This is a very simple example:
|
||||
/// * An input box always focused. Every character you type is registered
|
||||
/// here.
|
||||
/// * An entered character is inserted at the cursor position.
|
||||
/// * Pressing Backspace erases the left character before the cursor position
|
||||
/// * Pressing Enter pushes the current input in the history of previous
|
||||
/// messages.
|
||||
/// **Note: ** as this is a relatively simple example unicode characters are unsupported and
|
||||
/// their use will result in undefined behaviour.
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{Block, Borders, List, ListItem, Paragraph},
|
||||
};
|
||||
|
||||
enum InputMode {
|
||||
Normal,
|
||||
@@ -38,18 +56,16 @@ struct App {
|
||||
messages: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for App {
|
||||
fn default() -> App {
|
||||
App {
|
||||
impl App {
|
||||
const fn new() -> Self {
|
||||
Self {
|
||||
input: String::new(),
|
||||
input_mode: InputMode::Normal,
|
||||
messages: Vec::new(),
|
||||
cursor_position: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn move_cursor_left(&mut self) {
|
||||
let cursor_moved_left = self.cursor_position.saturating_sub(1);
|
||||
self.cursor_position = self.clamp_cursor(cursor_moved_left);
|
||||
@@ -112,7 +128,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// create app and run it
|
||||
let app = App::default();
|
||||
let app = App::new();
|
||||
let res = run_app(&mut terminal, app);
|
||||
|
||||
// restore terminal
|
||||
@@ -165,21 +181,19 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
_ => {}
|
||||
InputMode::Editing => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui(f: &mut Frame, app: &App) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(3),
|
||||
Constraint::Min(1),
|
||||
])
|
||||
.split(f.size());
|
||||
let vertical = Layout::vertical([
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(3),
|
||||
Constraint::Min(1),
|
||||
]);
|
||||
let [help_area, input_area, messages_area] = vertical.areas(f.size());
|
||||
|
||||
let (msg, style) = match app.input_mode {
|
||||
InputMode::Normal => (
|
||||
@@ -203,10 +217,9 @@ fn ui(f: &mut Frame, app: &App) {
|
||||
Style::default(),
|
||||
),
|
||||
};
|
||||
let mut text = Text::from(Line::from(msg));
|
||||
text.patch_style(style);
|
||||
let text = Text::from(Line::from(msg)).patch_style(style);
|
||||
let help_message = Paragraph::new(text);
|
||||
f.render_widget(help_message, chunks[0]);
|
||||
f.render_widget(help_message, help_area);
|
||||
|
||||
let input = Paragraph::new(app.input.as_str())
|
||||
.style(match app.input_mode {
|
||||
@@ -214,7 +227,7 @@ fn ui(f: &mut Frame, app: &App) {
|
||||
InputMode::Editing => Style::default().fg(Color::Yellow),
|
||||
})
|
||||
.block(Block::default().borders(Borders::ALL).title("Input"));
|
||||
f.render_widget(input, chunks[1]);
|
||||
f.render_widget(input, input_area);
|
||||
match app.input_mode {
|
||||
InputMode::Normal =>
|
||||
// Hide the cursor. `Frame` does this by default, so we don't need to do anything here
|
||||
@@ -223,13 +236,14 @@ fn ui(f: &mut Frame, app: &App) {
|
||||
InputMode::Editing => {
|
||||
// Make the cursor visible and ask ratatui to put it at the specified coordinates after
|
||||
// rendering
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
f.set_cursor(
|
||||
// Draw the cursor at the current position in the input field.
|
||||
// This position is can be controlled via the left and right arrow key
|
||||
chunks[1].x + app.cursor_position as u16 + 1,
|
||||
input_area.x + app.cursor_position as u16 + 1,
|
||||
// Move one line down, from the border to the input line
|
||||
chunks[1].y + 1,
|
||||
)
|
||||
input_area.y + 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,5 +258,5 @@ fn ui(f: &mut Frame, app: &App) {
|
||||
.collect();
|
||||
let messages =
|
||||
List::new(messages).block(Block::default().borders(Borders::ALL).title("Messages"));
|
||||
f.render_widget(messages, chunks[2]);
|
||||
f.render_widget(messages, messages_area);
|
||||
}
|
||||
|
||||
14
examples/vhs/constraints.tape
Normal file
14
examples/vhs/constraints.tape
Normal file
@@ -0,0 +1,14 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/constraints.tape`
|
||||
Output "target/constraints.gif"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set FontSize 18
|
||||
Set Width 1200
|
||||
Set Height 700
|
||||
Hide
|
||||
Type "cargo run --example=constraints --features=crossterm"
|
||||
Enter
|
||||
Sleep 2s
|
||||
Show
|
||||
Sleep 5s
|
||||
Right @5s 7
|
||||
@@ -18,4 +18,4 @@ Space
|
||||
Left
|
||||
Space
|
||||
Left
|
||||
Space
|
||||
Space
|
||||
18
examples/vhs/demo2-destroy.tape
Normal file
18
examples/vhs/demo2-destroy.tape
Normal file
@@ -0,0 +1,18 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/demo.tape`
|
||||
# NOTE: Requires VHS 0.6.1 or later for Screenshot support
|
||||
Output "target/demo2-destroy.gif"
|
||||
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
|
||||
Set Height 480
|
||||
Set Padding 0
|
||||
Hide
|
||||
Type "cargo run --example demo2 --features crossterm,widget-calendar"
|
||||
Enter
|
||||
Sleep 2s
|
||||
Show
|
||||
Type "d"
|
||||
Sleep 30s
|
||||
@@ -40,4 +40,4 @@ Tab
|
||||
# Weather
|
||||
Set TypingSpeed 100ms
|
||||
Down 40
|
||||
Sleep 2s
|
||||
Sleep 2s
|
||||
@@ -46,4 +46,4 @@ Tab
|
||||
Screenshot "target/demo2-weather.png"
|
||||
Set TypingSpeed 100ms
|
||||
Down 40
|
||||
Sleep 2s
|
||||
Sleep 2s
|
||||
17
examples/vhs/flex.tape
Normal file
17
examples/vhs/flex.tape
Normal file
@@ -0,0 +1,17 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/layout.tape`
|
||||
Output "target/flex.gif"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 1200
|
||||
Set Height 1410
|
||||
Hide
|
||||
Type "cargo run --example=flex --features=crossterm"
|
||||
Enter
|
||||
Sleep 2s
|
||||
Show
|
||||
Sleep 2s
|
||||
Right @5s 7
|
||||
Sleep 2s
|
||||
Left 7
|
||||
Sleep 2s
|
||||
Down @200ms 50
|
||||
@@ -3,10 +3,12 @@
|
||||
Output "target/gauge.gif"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 1200
|
||||
Set Height 550
|
||||
Set Height 850
|
||||
Hide
|
||||
Type "cargo run --example=gauge --features=crossterm"
|
||||
Enter
|
||||
Sleep 1s
|
||||
Sleep 2s
|
||||
Show
|
||||
Sleep 20s
|
||||
Sleep 2s
|
||||
Enter 1
|
||||
Sleep 15s
|
||||
@@ -25,12 +25,12 @@ set -o pipefail
|
||||
# ensure that running each example doesn't have to wait for the build
|
||||
cargo build --examples --features=crossterm,all-widgets
|
||||
|
||||
for tape in examples/*.tape; do
|
||||
gif=${tape/examples\//}
|
||||
gif=${gif/.tape/.gif}
|
||||
~/go/bin/vhs $tape --quiet
|
||||
for tape_path in examples/vhs/*.tape; do
|
||||
tape_file=${tape_path/examples\/vhs\//} # strip the examples/vhs/ prefix
|
||||
gif_file=${tape_file/.tape/.gif} # replace the .tape suffix with .gif
|
||||
~/go/bin/vhs $tape_path --quiet
|
||||
# this can be pasted into the examples README.md
|
||||
echo "[${gif}]: https://github.com/ratatui-org/ratatui/blob/images/examples/${gif}?raw=true"
|
||||
echo "[${gif_file}]: https://github.com/ratatui-org/ratatui/blob/images/examples/${gif_file}?raw=true"
|
||||
done
|
||||
git switch images
|
||||
git pull --rebase upstream images
|
||||
@@ -3,13 +3,22 @@
|
||||
Output "target/list.gif"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 1200
|
||||
Set Height 600
|
||||
Set Height 612
|
||||
Hide
|
||||
Type "cargo run --example=list --features=crossterm"
|
||||
Enter
|
||||
Sleep 1s
|
||||
Sleep 10s
|
||||
Show
|
||||
Down@1s 4
|
||||
Up@1s 2
|
||||
Sleep 2s
|
||||
Down@1.5s 3
|
||||
Right@1.5s 1
|
||||
Sleep 1.5s
|
||||
Down@1.5s 3
|
||||
Sleep 1.5s
|
||||
Up@1s 1
|
||||
Sleep 1s
|
||||
Right@1.5s 1
|
||||
Sleep 1.5s
|
||||
Up@1s 4
|
||||
Left@1s 1
|
||||
Sleep 5s
|
||||
Sleep 2s
|
||||
@@ -2,15 +2,25 @@
|
||||
# To run this script, install vhs and run `vhs ./examples/table.tape`
|
||||
Output "target/table.gif"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 1200
|
||||
Set Height 600
|
||||
Set Width 1400
|
||||
Set Height 768
|
||||
Hide
|
||||
Type "cargo run --example=table --features=crossterm"
|
||||
Enter
|
||||
Sleep 1s
|
||||
Show
|
||||
Down@1s 4
|
||||
Up@1s 2
|
||||
Down@1s 8
|
||||
Up@1s 12
|
||||
Sleep 5s
|
||||
Sleep 2s
|
||||
Set TypingSpeed 1s
|
||||
Down 3
|
||||
Up 6
|
||||
Sleep 1s
|
||||
Down 3
|
||||
Sleep 1s
|
||||
Right 1
|
||||
Sleep 1s
|
||||
Right 1
|
||||
Sleep 1s
|
||||
Right 1
|
||||
Sleep 1s
|
||||
Right 1
|
||||
Sleep 2s
|
||||
@@ -3,12 +3,13 @@
|
||||
Output "target/tabs.gif"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 1200
|
||||
Set Height 300
|
||||
Set Height 368
|
||||
Hide
|
||||
Type "cargo run --example=tabs --features=crossterm"
|
||||
Enter
|
||||
Sleep 1s
|
||||
Sleep 2s
|
||||
Show
|
||||
Right@1s 4
|
||||
Left@1s 2
|
||||
Sleep 5s
|
||||
Sleep 1s
|
||||
Right@2.5s 3
|
||||
Left@2.5s 3
|
||||
Sleep 2s
|
||||
@@ -38,7 +38,7 @@
|
||||
//! # std::io::Result::Ok(())
|
||||
//! ```
|
||||
//!
|
||||
//! See the the [examples] directory for more examples.
|
||||
//! See the the [Examples] directory for more examples.
|
||||
//!
|
||||
//! # Raw Mode
|
||||
//!
|
||||
@@ -96,9 +96,9 @@
|
||||
//! [Crossterm]: https://crates.io/crates/crossterm
|
||||
//! [Termion]: https://crates.io/crates/termion
|
||||
//! [Termwiz]: https://crates.io/crates/termwiz
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/tree/main/examples#readme
|
||||
//! [Examples]: https://github.com/ratatui-org/ratatui/tree/main/examples/README.md
|
||||
//! [Backend Comparison]:
|
||||
//! https://ratatui-org.github.io/ratatui-book/concepts/backends/comparison.html
|
||||
//! https://ratatui.rs/concepts/backends/comparison/
|
||||
//! [Ratatui Website]: https://ratatui-org.github.io/ratatui-book
|
||||
use std::io;
|
||||
|
||||
|
||||
@@ -70,14 +70,14 @@ use crate::{
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
///
|
||||
/// See the the [examples] directory for more examples. See the [`backend`] module documentation
|
||||
/// See the the [Examples] directory for more examples. See the [`backend`] module documentation
|
||||
/// for more details on raw mode and alternate screen.
|
||||
///
|
||||
/// [`Write`]: std::io::Write
|
||||
/// [`Terminal`]: crate::terminal::Terminal
|
||||
/// [`backend`]: crate::backend
|
||||
/// [Crossterm]: https://crates.io/crates/crossterm
|
||||
/// [examples]: https://github.com/ratatui-org/ratatui/tree/main/examples#examples
|
||||
/// [Examples]: https://github.com/ratatui-org/ratatui/tree/main/examples/README.md
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct CrosstermBackend<W: Write> {
|
||||
/// The writer used to send commands to the terminal.
|
||||
@@ -97,8 +97,8 @@ where
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let backend = CrosstermBackend::new(stdout());
|
||||
/// ```
|
||||
pub fn new(writer: W) -> CrosstermBackend<W> {
|
||||
CrosstermBackend { writer }
|
||||
pub const fn new(writer: W) -> Self {
|
||||
Self { writer }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,25 +252,25 @@ where
|
||||
impl From<Color> for CColor {
|
||||
fn from(color: Color) -> Self {
|
||||
match color {
|
||||
Color::Reset => CColor::Reset,
|
||||
Color::Black => CColor::Black,
|
||||
Color::Red => CColor::DarkRed,
|
||||
Color::Green => CColor::DarkGreen,
|
||||
Color::Yellow => CColor::DarkYellow,
|
||||
Color::Blue => CColor::DarkBlue,
|
||||
Color::Magenta => CColor::DarkMagenta,
|
||||
Color::Cyan => CColor::DarkCyan,
|
||||
Color::Gray => CColor::Grey,
|
||||
Color::DarkGray => CColor::DarkGrey,
|
||||
Color::LightRed => CColor::Red,
|
||||
Color::LightGreen => CColor::Green,
|
||||
Color::LightBlue => CColor::Blue,
|
||||
Color::LightYellow => CColor::Yellow,
|
||||
Color::LightMagenta => CColor::Magenta,
|
||||
Color::LightCyan => CColor::Cyan,
|
||||
Color::White => CColor::White,
|
||||
Color::Indexed(i) => CColor::AnsiValue(i),
|
||||
Color::Rgb(r, g, b) => CColor::Rgb { r, g, b },
|
||||
Color::Reset => Self::Reset,
|
||||
Color::Black => Self::Black,
|
||||
Color::Red => Self::DarkRed,
|
||||
Color::Green => Self::DarkGreen,
|
||||
Color::Yellow => Self::DarkYellow,
|
||||
Color::Blue => Self::DarkBlue,
|
||||
Color::Magenta => Self::DarkMagenta,
|
||||
Color::Cyan => Self::DarkCyan,
|
||||
Color::Gray => Self::Grey,
|
||||
Color::DarkGray => Self::DarkGrey,
|
||||
Color::LightRed => Self::Red,
|
||||
Color::LightGreen => Self::Green,
|
||||
Color::LightBlue => Self::Blue,
|
||||
Color::LightYellow => Self::Yellow,
|
||||
Color::LightMagenta => Self::Magenta,
|
||||
Color::LightCyan => Self::Cyan,
|
||||
Color::White => Self::White,
|
||||
Color::Indexed(i) => Self::AnsiValue(i),
|
||||
Color::Rgb(r, g, b) => Self::Rgb { r, g, b },
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -304,14 +304,13 @@ impl From<CColor> for Color {
|
||||
/// The `ModifierDiff` struct is used to calculate the difference between two `Modifier`
|
||||
/// values. This is useful when updating the terminal display, as it allows for more
|
||||
/// efficient updates by only sending the necessary changes.
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
struct ModifierDiff {
|
||||
pub from: Modifier,
|
||||
pub to: Modifier,
|
||||
}
|
||||
|
||||
impl ModifierDiff {
|
||||
fn queue<W>(&self, mut w: W) -> io::Result<()>
|
||||
fn queue<W>(self, mut w: W) -> io::Result<()>
|
||||
where
|
||||
W: io::Write,
|
||||
{
|
||||
@@ -377,22 +376,22 @@ impl From<CAttribute> for Modifier {
|
||||
// `Attribute*s*` (note the *s*) contains multiple `Attribute`
|
||||
// We convert `Attribute` to `Attribute*s*` (containing only 1 value) to avoid implementing
|
||||
// the conversion again
|
||||
Modifier::from(CAttributes::from(value))
|
||||
Self::from(CAttributes::from(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CAttributes> for Modifier {
|
||||
fn from(value: CAttributes) -> Self {
|
||||
let mut res = Modifier::empty();
|
||||
let mut res = Self::empty();
|
||||
|
||||
if value.has(CAttribute::Bold) {
|
||||
res |= Modifier::BOLD;
|
||||
res |= Self::BOLD;
|
||||
}
|
||||
if value.has(CAttribute::Dim) {
|
||||
res |= Modifier::DIM;
|
||||
res |= Self::DIM;
|
||||
}
|
||||
if value.has(CAttribute::Italic) {
|
||||
res |= Modifier::ITALIC;
|
||||
res |= Self::ITALIC;
|
||||
}
|
||||
if value.has(CAttribute::Underlined)
|
||||
|| value.has(CAttribute::DoubleUnderlined)
|
||||
@@ -400,22 +399,22 @@ impl From<CAttributes> for Modifier {
|
||||
|| value.has(CAttribute::Underdotted)
|
||||
|| value.has(CAttribute::Underdashed)
|
||||
{
|
||||
res |= Modifier::UNDERLINED;
|
||||
res |= Self::UNDERLINED;
|
||||
}
|
||||
if value.has(CAttribute::SlowBlink) {
|
||||
res |= Modifier::SLOW_BLINK;
|
||||
res |= Self::SLOW_BLINK;
|
||||
}
|
||||
if value.has(CAttribute::RapidBlink) {
|
||||
res |= Modifier::RAPID_BLINK;
|
||||
res |= Self::RAPID_BLINK;
|
||||
}
|
||||
if value.has(CAttribute::Reverse) {
|
||||
res |= Modifier::REVERSED;
|
||||
res |= Self::REVERSED;
|
||||
}
|
||||
if value.has(CAttribute::Hidden) {
|
||||
res |= Modifier::HIDDEN;
|
||||
res |= Self::HIDDEN;
|
||||
}
|
||||
if value.has(CAttribute::CrossedOut) {
|
||||
res |= Modifier::CROSSED_OUT;
|
||||
res |= Self::CROSSED_OUT;
|
||||
}
|
||||
|
||||
res
|
||||
@@ -449,10 +448,10 @@ impl From<ContentStyle> for Style {
|
||||
}
|
||||
|
||||
Self {
|
||||
fg: value.foreground_color.map(|c| c.into()),
|
||||
bg: value.background_color.map(|c| c.into()),
|
||||
fg: value.foreground_color.map(Into::into),
|
||||
bg: value.background_color.map(Into::into),
|
||||
#[cfg(feature = "underline-color")]
|
||||
underline_color: value.underline_color.map(|c| c.into()),
|
||||
underline_color: value.underline_color.map(Into::into),
|
||||
add_modifier: value.attributes.into(),
|
||||
sub_modifier,
|
||||
}
|
||||
@@ -668,6 +667,6 @@ mod tests {
|
||||
..Default::default()
|
||||
}),
|
||||
Style::default().underline_color(Color::Red)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,8 +82,8 @@ where
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let backend = TermionBackend::new(stdout());
|
||||
/// ```
|
||||
pub fn new(writer: W) -> TermionBackend<W> {
|
||||
TermionBackend { writer }
|
||||
pub const fn new(writer: W) -> Self {
|
||||
Self { writer }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,16 +209,13 @@ where
|
||||
self.writer.flush()
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
struct Fg(Color);
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
struct Bg(Color);
|
||||
|
||||
/// The `ModifierDiff` struct is used to calculate the difference between two `Modifier`
|
||||
/// values. This is useful when updating the terminal display, as it allows for more
|
||||
/// efficient updates by only sending the necessary changes.
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
struct ModifierDiff {
|
||||
from: Modifier,
|
||||
to: Modifier,
|
||||
@@ -319,37 +316,37 @@ from_termion_for_color!(LightWhite, White);
|
||||
|
||||
impl From<tcolor::AnsiValue> for Color {
|
||||
fn from(value: tcolor::AnsiValue) -> Self {
|
||||
Color::Indexed(value.0)
|
||||
Self::Indexed(value.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tcolor::Bg<tcolor::AnsiValue>> for Style {
|
||||
fn from(value: tcolor::Bg<tcolor::AnsiValue>) -> Self {
|
||||
Style::default().bg(Color::Indexed(value.0 .0))
|
||||
Self::default().bg(Color::Indexed(value.0 .0))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tcolor::Fg<tcolor::AnsiValue>> for Style {
|
||||
fn from(value: tcolor::Fg<tcolor::AnsiValue>) -> Self {
|
||||
Style::default().fg(Color::Indexed(value.0 .0))
|
||||
Self::default().fg(Color::Indexed(value.0 .0))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tcolor::Rgb> for Color {
|
||||
fn from(value: tcolor::Rgb) -> Self {
|
||||
Color::Rgb(value.0, value.1, value.2)
|
||||
Self::Rgb(value.0, value.1, value.2)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tcolor::Bg<tcolor::Rgb>> for Style {
|
||||
fn from(value: tcolor::Bg<tcolor::Rgb>) -> Self {
|
||||
Style::default().bg(Color::Rgb(value.0 .0, value.0 .1, value.0 .2))
|
||||
Self::default().bg(Color::Rgb(value.0 .0, value.0 .1, value.0 .2))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tcolor::Fg<tcolor::Rgb>> for Style {
|
||||
fn from(value: tcolor::Fg<tcolor::Rgb>) -> Self {
|
||||
Style::default().fg(Color::Rgb(value.0 .0, value.0 .1, value.0 .2))
|
||||
Self::default().fg(Color::Rgb(value.0 .0, value.0 .1, value.0 .2))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -438,7 +435,7 @@ from_termion_for_modifier!(Blink, SLOW_BLINK);
|
||||
|
||||
impl From<termion::style::Reset> for Modifier {
|
||||
fn from(_: termion::style::Reset) -> Self {
|
||||
Modifier::empty()
|
||||
Self::empty()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user