Compare commits
213 Commits
v0.24.1-al
...
v0.26.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
1b8b6261e2 | ||
|
|
5bf4f52119 | ||
|
|
f4c8de041d | ||
|
|
910ad00059 | ||
|
|
b282a06932 | ||
|
|
b8f71c0d6e | ||
|
|
113b4b7a4e | ||
|
|
b82451fb33 | ||
|
|
4be18aba8b | ||
|
|
ebf1f42942 | ||
|
|
2169a0da01 | ||
|
|
d118565ef6 | ||
|
|
aaeba2709c | ||
|
|
d19b266e0e | ||
|
|
f767ea7d37 | ||
|
|
0576a8aa32 | ||
|
|
03401cd46e | ||
|
|
f69d57c3b5 | ||
|
|
2a87251152 | ||
|
|
aef495604c | ||
|
|
8bfd6661e2 | ||
|
|
3ec4e24d00 | ||
|
|
7ced7c0aa3 | ||
|
|
dd22e721e3 | ||
|
|
4424637af2 | ||
|
|
37c70dbb8e | ||
|
|
91c67eb100 | ||
|
|
e49385b78c | ||
|
|
6b2efd0f6c | ||
|
|
34d099c99a | ||
|
|
987f7eed4c | ||
|
|
e4579f0db2 | ||
|
|
6a6e9dde9d | ||
|
|
28ac55bc62 | ||
|
|
458fa90362 | ||
|
|
56fc410105 | ||
|
|
753e246531 | ||
|
|
211160ca16 | ||
|
|
1229b96e42 | ||
|
|
fe632d70cb | ||
|
|
c862aa5e9e | ||
|
|
18e19f6ce6 | ||
|
|
7ef0afcb62 | ||
|
|
1e2f0be75a | ||
|
|
a58cce2dba | ||
|
|
ffa78aa67c | ||
|
|
7cbb1060ac | ||
|
|
a05541358e |
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
|
||||
* @orhun @mindoodoo @sayanarijit @joshka @kdheepak @Valentin271
|
||||
|
||||
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}"
|
||||
22
.github/workflows/cd.yml
vendored
22
.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
|
||||
|
||||
8
.github/workflows/check-pr.yml
vendored
8
.github/workflows/check-pr.yml
vendored
@@ -46,20 +46,24 @@ jobs:
|
||||
|
||||
check-breaking-change-label:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
# use an environment variable to pass untrusted input to the script
|
||||
# see https://securitylab.github.com/research/github-actions-untrusted-input/
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
steps:
|
||||
- name: Check breaking change label
|
||||
id: check_breaking_change
|
||||
run: |
|
||||
pattern='^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\(\w+\))?!:'
|
||||
# Check if pattern matches
|
||||
if echo "${{ github.event.pull_request.title }}" | grep -qE "$pattern"; then
|
||||
if echo "${PR_TITLE}" | grep -qE "$pattern"; then
|
||||
echo "breaking_change=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "breaking_change=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
- name: Add label
|
||||
if: steps.check_breaking_change.outputs.breaking_change == 'true'
|
||||
uses: actions/github-script@v6
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
|
||||
28
.github/workflows/ci.yml
vendored
28
.github/workflows/ci.yml
vendored
@@ -42,6 +42,8 @@ jobs:
|
||||
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
|
||||
@@ -67,6 +69,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 +87,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 +101,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.70.0", "stable"]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -107,6 +113,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 +124,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 +133,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 +144,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.70.0", "stable"]
|
||||
backend: [crossterm, termion, termwiz]
|
||||
exclude:
|
||||
# termion is not supported on windows
|
||||
- os: windows-latest
|
||||
@@ -151,6 +161,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,9 +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`
|
||||
@@ -35,9 +47,206 @@ 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)
|
||||
|
||||
### `Table::widths()` now accepts `AsRef<[Constraint]>` ([#628])
|
||||
### `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`
|
||||
- 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
|
||||
[`Axis::title`]: https://docs.rs/ratatui/latest/ratatui/widgets/struct.Axis.html#method.title
|
||||
[`Buffer::set_style`]: https://docs.rs/ratatui/latest/ratatui/buffer/struct.Buffer.html#method.set_style
|
||||
|
||||
### `List::new()` now accepts `IntoIterator<Item = Into<ListItem<'a>>>` ([#672])
|
||||
|
||||
[#672]: https://github.com/ratatui-org/ratatui/pull/672
|
||||
|
||||
Previously `List::new()` took `Into<Vec<ListItem<'a>>>`. This change will throw a compilation
|
||||
error for `IntoIterator`s with an indeterminate item (e.g. empty vecs).
|
||||
|
||||
E.g.
|
||||
|
||||
```diff
|
||||
- let list = List::new(vec![]);
|
||||
// becomes
|
||||
+ let list = List::default();
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
### 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])
|
||||
|
||||
[#664]: https://github.com/ratatui-org/ratatui/pull/664
|
||||
|
||||
Previously `Table`s could be constructed without widths. In almost all cases this is an error.
|
||||
A new widths parameter is now mandatory on `Table::new()`. Existing code of the form:
|
||||
|
||||
```diff
|
||||
- Table::new(rows).widths(widths)
|
||||
```
|
||||
|
||||
Should be updated to:
|
||||
|
||||
```diff
|
||||
+ Table::new(rows, widths)
|
||||
```
|
||||
|
||||
For ease of automated replacement in cases where the amount of code broken by this change is large
|
||||
or complex, it may be convenient to replace `Table::new` with `Table::default().rows`.
|
||||
|
||||
```diff
|
||||
- Table::new(rows).block(block).widths(widths);
|
||||
// becomes
|
||||
+ Table::default().rows(rows).widths(widths)
|
||||
```
|
||||
|
||||
### `Table::widths()` now accepts `IntoIterator<Item = AsRef<Constraint>>` ([#663])
|
||||
|
||||
[#663]: https://github.com/ratatui-org/ratatui/pull/663
|
||||
|
||||
Previously `Table::widths()` took a slice (`&'a [Constraint]`). This change will introduce clippy
|
||||
`needless_borrow` warnings for places where slices are passed to this method. To fix these, remove
|
||||
@@ -45,13 +254,30 @@ the `&`.
|
||||
|
||||
E.g.
|
||||
|
||||
```rust
|
||||
let table = Table::new(rows).widths(&[Constraint::Length(1)]);
|
||||
```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)]);
|
||||
```
|
||||
|
||||
[#628]: https://github.com/ratatui-org/ratatui/pull/628
|
||||
### Layout::new() now accepts direction and constraint parameters ([#557])
|
||||
|
||||
[#557]: https://github.com/ratatui-org/ratatui/pull/557
|
||||
|
||||
Previously layout new took no parameters. Existing code should either use `Layout::default()` or
|
||||
the new constructor.
|
||||
|
||||
```rust
|
||||
let layout = layout::new()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Min(1), Constraint::Max(2)]);
|
||||
// becomes either
|
||||
let layout = layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Min(1), Constraint::Max(2)]);
|
||||
// or
|
||||
let layout = layout::new(Direction::Vertical, [Constraint::Min(1), Constraint::Max(2)]);
|
||||
```
|
||||
|
||||
## [v0.24.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.24.0)
|
||||
|
||||
@@ -70,10 +296,10 @@ Applications can now set custom borders on a `Block` by calling `border_set()`.
|
||||
`BorderType::line_symbols()` is renamed to `border_symbols()` and now returns a new struct
|
||||
`symbols::border::Set`. E.g.:
|
||||
|
||||
```rust
|
||||
let line_set: symbols::line::Set = BorderType::line_symbols(BorderType::Plain);
|
||||
```diff
|
||||
- let line_set: symbols::line::Set = BorderType::line_symbols(BorderType::Plain);
|
||||
// becomes
|
||||
let border_set: symbols::border::Set = BorderType::border_symbols(BorderType::Plain);
|
||||
+ let border_set: symbols::border::Set = BorderType::border_symbols(BorderType::Plain);
|
||||
```
|
||||
|
||||
### Generic `Backend` parameter removed from `Frame` ([#530])
|
||||
@@ -84,10 +310,10 @@ let border_set: symbols::border::Set = BorderType::border_symbols(BorderType::Pl
|
||||
accept `Frame`. To migrate existing code, remove any generic parameters from code that uses an
|
||||
instance of a Frame. E.g.:
|
||||
|
||||
```rust
|
||||
fn ui<B: Backend>(frame: &mut Frame<B>) { ... }
|
||||
```diff
|
||||
- fn ui<B: Backend>(frame: &mut Frame<B>) { ... }
|
||||
// becomes
|
||||
fn ui(frame: Frame) { ... }
|
||||
+ fn ui(frame: Frame) { ... }
|
||||
```
|
||||
|
||||
### `Stylize` shorthands now consume rather than borrow `String` ([#466])
|
||||
@@ -99,13 +325,13 @@ new implementation of `Stylize` was added that returns a `Span<'static>`. This c
|
||||
be consumed rather than borrowed. Existing code that expects to use the string after a call will no
|
||||
longer compile. E.g.
|
||||
|
||||
```rust
|
||||
let s = String::new("foo");
|
||||
let span1 = s.red();
|
||||
let span2 = s.blue(); // will no longer compile as s is consumed by the previous line
|
||||
```diff
|
||||
- let s = String::new("foo");
|
||||
- let span1 = s.red();
|
||||
- let span2 = s.blue(); // will no longer compile as s is consumed by the previous line
|
||||
// becomes
|
||||
let span1 = s.clone().red();
|
||||
let span2 = s.blue();
|
||||
+ let span1 = s.clone().red();
|
||||
+ let span2 = s.blue();
|
||||
```
|
||||
|
||||
### Deprecated `Spans` type removed (replaced with `Line`) ([#426])
|
||||
@@ -115,12 +341,12 @@ let span2 = s.blue();
|
||||
`Spans` was replaced with `Line` in 0.21.0. `Buffer::set_spans` was replaced with
|
||||
`Buffer::set_line`.
|
||||
|
||||
```rust
|
||||
let spans = Spans::from(some_string_str_span_or_vec_span);
|
||||
buffer.set_spans(0, 0, spans, 10);
|
||||
```diff
|
||||
- let spans = Spans::from(some_string_str_span_or_vec_span);
|
||||
- buffer.set_spans(0, 0, spans, 10);
|
||||
// becomes
|
||||
let line - Line::from(some_string_str_span_or_vec_span);
|
||||
buffer.set_line(0, 0, line, 10);
|
||||
+ let line - Line::from(some_string_str_span_or_vec_span);
|
||||
+ buffer.set_line(0, 0, line, 10);
|
||||
```
|
||||
|
||||
## [v0.23.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.23.0)
|
||||
@@ -131,10 +357,10 @@ buffer.set_line(0, 0, line, 10);
|
||||
|
||||
The track symbol of `Scrollbar` is now optional, this method now takes an optional value.
|
||||
|
||||
```rust
|
||||
let scrollbar = Scrollbar::default().track_symbol("|");
|
||||
```diff
|
||||
- let scrollbar = Scrollbar::default().track_symbol("|");
|
||||
// becomes
|
||||
let scrollbar = Scrollbar::default().track_symbol(Some("|"));
|
||||
+ let scrollbar = Scrollbar::default().track_symbol(Some("|"));
|
||||
```
|
||||
|
||||
### `Scrollbar` symbols moved to `symbols::scrollbar` and `widgets::scrollbar` module is private ([#330])
|
||||
@@ -145,10 +371,10 @@ The symbols for defining scrollbars have been moved to the `symbols` module from
|
||||
`widgets::scrollbar` module which is no longer public. To update your code update any imports to the
|
||||
new module locations. E.g.:
|
||||
|
||||
```rust
|
||||
use ratatui::{widgets::scrollbar::{Scrollbar, Set}};
|
||||
```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])
|
||||
@@ -169,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
|
||||
|
||||
@@ -180,15 +406,15 @@ 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.
|
||||
|
||||
```rust
|
||||
```diff
|
||||
let terminal = Terminal::with_options(backend, TerminalOptions {
|
||||
viewport: Viewport::fixed(area),
|
||||
- viewport: Viewport::fixed(area),
|
||||
});
|
||||
// becomes
|
||||
let terminal = Terminal::with_options(backend, TerminalOptions {
|
||||
viewport: Viewport::Fixed(area),
|
||||
+ viewport: Viewport::Fixed(area),
|
||||
});
|
||||
```
|
||||
|
||||
@@ -197,13 +423,13 @@ 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.:
|
||||
|
||||
```rust
|
||||
let paragraph = Paragraph::new("".as_ref());
|
||||
```diff
|
||||
- let paragraph = Paragraph::new("".as_ref());
|
||||
// becomes
|
||||
let paragraph = Paragraph::new("".as_str());
|
||||
+ let paragraph = Paragraph::new("".as_str());
|
||||
```
|
||||
|
||||
### `Marker::Block` now renders as a block rather than a bar character ([#133])
|
||||
@@ -214,10 +440,10 @@ Code using the `Block` marker that previously rendered using a half block charac
|
||||
renders using the full block character (`'█'`). A new marker variant`Bar` is introduced to replace
|
||||
the existing code.
|
||||
|
||||
```rust
|
||||
let canvas = Canvas::default().marker(Marker::Block);
|
||||
```diff
|
||||
- let canvas = Canvas::default().marker(Marker::Block);
|
||||
// becomes
|
||||
let canvas = Canvas::default().marker(Marker::Bar);
|
||||
+ let canvas = Canvas::default().marker(Marker::Bar);
|
||||
```
|
||||
|
||||
## [v0.20.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.20.0)
|
||||
|
||||
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,
|
||||
@@ -171,6 +163,12 @@ struct Foo {}
|
||||
- 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
|
||||
|
||||
64
Cargo.toml
64
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/"
|
||||
@@ -24,21 +24,23 @@ rust-version = "1.70.0"
|
||||
|
||||
[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"
|
||||
cassowary = "0.3"
|
||||
indoc = "2.0"
|
||||
itertools = "0.11"
|
||||
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"
|
||||
@@ -47,11 +49,17 @@ better-panic = "0.3.0"
|
||||
cargo-husky = { version = "1.5.0", default-features = false, features = [
|
||||
"user-hooks",
|
||||
] }
|
||||
color-eyre = "0.6.2"
|
||||
criterion = { version = "0.5.1", features = ["html_reports"] }
|
||||
derive_builder = "0.13.0"
|
||||
fakeit = "1.1"
|
||||
rand = "0.8.5"
|
||||
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"
|
||||
|
||||
[features]
|
||||
#! The crate provides a set of optional features that can be enabled in your `cargo.toml` file.
|
||||
@@ -71,7 +79,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 = []
|
||||
@@ -89,6 +97,21 @@ widget-calendar = ["dep:time"]
|
||||
## enables the backend code that sets the underline color.
|
||||
underline-color = ["dep:crossterm"]
|
||||
|
||||
#! The following features are unstable and may change in the future:
|
||||
|
||||
## Enable all unstable features.
|
||||
unstable = ["unstable-rendered-line-info", "unstable-widget-ref"]
|
||||
|
||||
## Enables the [`Paragraph::line_count`](crate::widgets::Paragraph::line_count)
|
||||
## [`Paragraph::line_width`](crate::widgets::Paragraph::line_width) methods
|
||||
## which are experimental and may change in the future.
|
||||
## See [Issue 293](https://github.com/ratatui-org/ratatui/issues/293) for more details.
|
||||
unstable-rendered-line-info = []
|
||||
|
||||
## Enables the `WidgetRef` and `StatefulWidgetRef` traits which are experimental and may change in
|
||||
## the future.
|
||||
unstable-widget-ref = []
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
# see https://doc.rust-lang.org/nightly/rustdoc/scraped-examples.html
|
||||
@@ -107,6 +130,9 @@ harness = false
|
||||
name = "list"
|
||||
harness = false
|
||||
|
||||
[lib]
|
||||
bench = false
|
||||
|
||||
[[bench]]
|
||||
name = "paragraph"
|
||||
harness = false
|
||||
@@ -187,6 +213,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"]
|
||||
@@ -213,6 +254,11 @@ name = "popup"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "ratatui-logo"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "scrollbar"
|
||||
required-features = ["crossterm"]
|
||||
@@ -242,3 +288,7 @@ doc-scrape-examples = true
|
||||
name = "inline"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[test]]
|
||||
name = "state_serde"
|
||||
required-features = ["serde"]
|
||||
|
||||
@@ -11,7 +11,7 @@ ALL_FEATURES = "all-widgets,macros,serde"
|
||||
# sets of flags, one for Windows and one for other platforms.
|
||||
# Windows: --features=all-widgets,macros,serde,crossterm,termwiz,underline-color
|
||||
# Other: --features=all-widgets,macros,serde,crossterm,termion,termwiz,underline-color
|
||||
ALL_FEATURES_FLAG = { source = "${CARGO_MAKE_RUST_TARGET_OS}", default_value = "--features=all-widgets,macros,serde,crossterm,termion,termwiz", mapping = { "windows" = "--features=all-widgets,macros,serde,crossterm,termwiz" } }
|
||||
ALL_FEATURES_FLAG = { source = "${CARGO_MAKE_RUST_TARGET_OS}", default_value = "--features=all-widgets,macros,serde,crossterm,termion,termwiz,unstable", mapping = { "windows" = "--features=all-widgets,macros,serde,crossterm,termwiz,unstable" } }
|
||||
|
||||
[tasks.default]
|
||||
alias = "ci"
|
||||
@@ -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",
|
||||
|
||||
166
README.md
166
README.md
@@ -21,31 +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>
|
||||
[Documentation](https://docs.rs/ratatui) · [Ratatui Book](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)
|
||||
[![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>
|
||||
|
||||
[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
|
||||
|
||||
@@ -56,7 +50,7 @@ cargo add ratatui crossterm
|
||||
```
|
||||
|
||||
Ratatui uses [Crossterm] by default as it works on most platforms. See the [Installation]
|
||||
section of the [Ratatui Book] for more details on how to use other backends ([Termion] /
|
||||
section of the [Ratatui Website] for more details on how to use other backends ([Termion] /
|
||||
[Termwiz]).
|
||||
|
||||
## Introduction
|
||||
@@ -64,29 +58,29 @@ section of the [Ratatui Book] for more details on how to use other backends ([Te
|
||||
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 Book] 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 Book] - explains the library's concepts and provides step-by-step tutorials
|
||||
- [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 Book] and the various
|
||||
[Examples]. There are also several starter templates available:
|
||||
|
||||
- [rust-tui-template]
|
||||
- [ratatui-async-template] (book and template)
|
||||
- [simple-tui-rs]
|
||||
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:
|
||||
|
||||
@@ -110,30 +104,31 @@ implements the [`Backend`] trait which has implementations for [Crossterm], [Ter
|
||||
|
||||
Most applications should enter the Alternate Screen when starting and leave it when exiting and
|
||||
also enable raw mode to disable line buffering and enable reading key events. See the [`backend`
|
||||
module] and the [Backends] section of the [Ratatui Book] for more info.
|
||||
module] and the [Backends] section of the [Ratatui Website] for more info.
|
||||
|
||||
### Drawing the UI
|
||||
|
||||
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 Book] for
|
||||
more info.
|
||||
using the provided [`render_widget`] method. See the [Widgets] section of the [Ratatui Website]
|
||||
for more info.
|
||||
|
||||
### Handling events
|
||||
|
||||
Ratatui does not include any input handling. Instead event handling can be implemented by
|
||||
calling backend library methods directly. See the [Handling Events] section of the [Ratatui
|
||||
Book] for more info. For example, if you are using [Crossterm], you can use the
|
||||
Website] for more info. For example, if you are using [Crossterm], you can use the
|
||||
[`crossterm::event`] module to handle events.
|
||||
|
||||
### Example
|
||||
|
||||
```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::*};
|
||||
|
||||
@@ -159,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)
|
||||
}
|
||||
@@ -182,20 +177,21 @@ Running this example produces the following output:
|
||||
The library comes with a basic yet useful layout management object called [`Layout`] which
|
||||
allows you to split the available space into multiple areas and then render widgets in each
|
||||
area. This lets you describe a responsive terminal UI by nesting layouts. See the [Layout]
|
||||
section of the [Ratatui Book] for more info.
|
||||
section of the [Ratatui Website] for more info.
|
||||
|
||||
```rust
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
fn ui(frame: &mut Frame) {
|
||||
let main_layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
let main_layout = Layout::new(
|
||||
Direction::Vertical,
|
||||
[
|
||||
Constraint::Length(1),
|
||||
Constraint::Min(0),
|
||||
Constraint::Length(1),
|
||||
])
|
||||
.split(frame.size());
|
||||
],
|
||||
)
|
||||
.split(frame.size());
|
||||
frame.render_widget(
|
||||
Block::new().borders(Borders::TOP).title("Title Bar"),
|
||||
main_layout[0],
|
||||
@@ -205,10 +201,11 @@ fn ui(frame: &mut Frame) {
|
||||
main_layout[2],
|
||||
);
|
||||
|
||||
let inner_layout = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
.split(main_layout[1]);
|
||||
let inner_layout = Layout::new(
|
||||
Direction::Horizontal,
|
||||
[Constraint::Percentage(50), Constraint::Percentage(50)],
|
||||
)
|
||||
.split(main_layout[1]);
|
||||
frame.render_widget(
|
||||
Block::default().borders(Borders::ALL).title("Left"),
|
||||
inner_layout[0],
|
||||
@@ -234,22 +231,23 @@ The [`style` module] provides types that represent the various styling options.
|
||||
important one is [`Style`] which represents the foreground and background colors and the text
|
||||
attributes of a [`Span`]. The [`style` module] also provides a [`Stylize`] trait that allows
|
||||
short-hand syntax to apply a style to widgets and text. See the [Styling Text] section of the
|
||||
[Ratatui Book] for more info.
|
||||
[Ratatui Website] for more info.
|
||||
|
||||
```rust
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
fn ui(frame: &mut Frame) {
|
||||
let areas = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
let areas = Layout::new(
|
||||
Direction::Vertical,
|
||||
[
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
Constraint::Min(0),
|
||||
])
|
||||
.split(frame.size());
|
||||
],
|
||||
)
|
||||
.split(frame.size());
|
||||
|
||||
let span1 = Span::raw("Hello ");
|
||||
let span2 = Span::styled(
|
||||
@@ -285,26 +283,28 @@ Running this example produces the following output:
|
||||
|
||||
![docsrs-styling]
|
||||
|
||||
[Ratatui Book]: https://ratatui.rs
|
||||
[Installation]: https://ratatui.rs/installation.html
|
||||
[Rendering]: https://ratatui.rs/concepts/rendering/index.html
|
||||
[Application Patterns]: https://ratatui.rs/concepts/application_patterns/index.html
|
||||
[Hello World tutorial]: https://ratatui.rs/tutorial/hello_world.html
|
||||
[Backends]: https://ratatui.rs/concepts/backends/index.html
|
||||
[Widgets]: https://ratatui.rs/how-to/widgets/index.html
|
||||
[Handling Events]: https://ratatui.rs/concepts/event_handling.html
|
||||
[Layout]: https://ratatui.rs/how-to/layout/index.html
|
||||
[Styling Text]: https://ratatui.rs/how-to/render/style-text.html
|
||||
[rust-tui-template]: https://github.com/ratatui-org/rust-tui-template
|
||||
[ratatui-async-template]: https://ratatui-org.github.io/ratatui-async-template/
|
||||
[simple-tui-rs]: https://github.com/pmsanford/simple-tui-rs
|
||||
[Examples]: https://github.com/ratatui-org/ratatui/tree/main/examples
|
||||
[git-cliff]: https://github.com/orhun/git-cliff
|
||||
[Ratatui Website]: https://ratatui.rs/
|
||||
[Installation]: https://ratatui.rs/installation/
|
||||
[Rendering]: https://ratatui.rs/concepts/rendering/
|
||||
[Application Patterns]: https://ratatui.rs/concepts/application-patterns/
|
||||
[Hello World tutorial]: https://ratatui.rs/tutorials/hello-world/
|
||||
[Backends]: https://ratatui.rs/concepts/backends/
|
||||
[Widgets]: https://ratatui.rs/how-to/widgets/
|
||||
[Handling Events]: https://ratatui.rs/concepts/event-handling/
|
||||
[Layout]: https://ratatui.rs/how-to/layout/
|
||||
[Styling Text]: https://ratatui.rs/how-to/render/style-text/
|
||||
[templates]: https://github.com/ratatui-org/templates/
|
||||
[Examples]: https://github.com/ratatui-org/ratatui/tree/main/examples/README.md
|
||||
[Report a bug]: https://github.com/ratatui-org/ratatui/issues/new?labels=bug&projects=&template=bug_report.md
|
||||
[Request a Feature]: https://github.com/ratatui-org/ratatui/issues/new?labels=enhancement&projects=&template=feature_request.md
|
||||
[Create a Pull Request]: https://github.com/ratatui-org/ratatui/compare
|
||||
[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
|
||||
@@ -321,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 crate]: https://crates.io/crates/tui
|
||||
[hello_world.rs]: https://github.com/ratatui-org/ratatui/blob/main/examples/hello_world.rs
|
||||
[tui-rs]: https://crates.io/crates/tui
|
||||
[GitHub Sponsors]: https://github.com/sponsors/ratatui-org
|
||||
[Crate Badge]: https://img.shields.io/crates/v/ratatui?logo=rust&style=flat-square
|
||||
[License Badge]: https://img.shields.io/crates/l/ratatui?style=flat-square
|
||||
[CI Badge]:
|
||||
https://img.shields.io/github/actions/workflow/status/ratatui-org/ratatui/ci.yml?style=flat-square&logo=github
|
||||
[CI Workflow]: https://github.com/ratatui-org/ratatui/actions/workflows/ci.yml
|
||||
[Codecov Badge]:
|
||||
https://img.shields.io/codecov/c/github/ratatui-org/ratatui?logo=codecov&style=flat-square&token=BAQ8SOKEST
|
||||
[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 -->
|
||||
|
||||
@@ -387,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`).
|
||||
@@ -400,9 +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
|
||||
- [simple-tui-rs](https://github.com/pmsanford/simple-tui-rs) — A simple example tui-rs app
|
||||
- [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
|
||||
@@ -425,8 +429,8 @@ be installed with `cargo install cargo-make`).
|
||||
|
||||
## Apps
|
||||
|
||||
Check out the list of more than 50 [Apps using
|
||||
`Ratatui`](https://github.com/ratatui-org/ratatui/wiki/Apps-using-Ratatui)!
|
||||
Check out [awesome-ratatui](https://github.com/ratatui-org/awesome-ratatui) for a curated list of
|
||||
awesome apps/libraries built with `ratatui`!
|
||||
|
||||
## Alternatives
|
||||
|
||||
|
||||
9
SECURITY.md
Normal file
9
SECURITY.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
We only support the latest version of this crate.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
To report secuirity vulnerability, please use the form at https://github.com/ratatui-org/ratatui/security/advisories/new
|
||||
14
bacon.toml
14
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",
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -117,7 +136,10 @@ two square-ish pixels in the space of a single rectangular terminal cell.
|
||||
cargo run --example=colors_rgb --features=crossterm
|
||||
```
|
||||
|
||||
![Colors RGB][colors_rgb.png]
|
||||
Note: VHs renders full screen animations poorly, so this is a screen capture rather than the output
|
||||
of the VHS tape.
|
||||
|
||||
<https://github.com/ratatui-org/ratatui/assets/381361/485e775a-e0b5-4133-899b-1e8aeb56e774>
|
||||
|
||||
## Custom Widget
|
||||
|
||||
@@ -223,6 +245,18 @@ cargo run --example=popup --features=crossterm
|
||||
|
||||
![Popup][popup.gif]
|
||||
|
||||
## Ratatui-logo
|
||||
|
||||
A fun example of using half blocks to render graphics Source:
|
||||
[ratatui-logo.rs](./ratatui-logo.rs).
|
||||
|
||||
>
|
||||
```shell
|
||||
cargo run --example=ratatui-logo --features=crossterm
|
||||
```
|
||||
|
||||
![Ratatui Logo][ratatui-logo.gif]
|
||||
|
||||
## Scrollbar
|
||||
|
||||
Demonstrates the [`Scrollbar`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Scrollbar.html)
|
||||
@@ -271,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
|
||||
@@ -280,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
|
||||
```
|
||||
-->
|
||||
|
||||
@@ -296,7 +338,6 @@ examples/generate.bash
|
||||
[canvas.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/canvas.gif?raw=true
|
||||
[chart.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/chart.gif?raw=true
|
||||
[colors.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/colors.gif?raw=true
|
||||
[colors_rgb.png]: https://github.com/ratatui-org/ratatui/blob/images/examples/colors_rgb.png?raw=true
|
||||
[custom_widget.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/custom_widget.gif?raw=true
|
||||
[demo.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/demo.gif?raw=true
|
||||
[demo2.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/demo2.gif?raw=true
|
||||
@@ -309,8 +350,12 @@ examples/generate.bash
|
||||
[panic.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/panic.gif?raw=true
|
||||
[paragraph.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/paragraph.gif?raw=true
|
||||
[popup.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/popup.gif?raw=true
|
||||
[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,
|
||||
@@ -134,11 +149,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,15 +161,10 @@ 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);
|
||||
}
|
||||
|
||||
fn create_groups<'a>(app: &'a App, combine_values_and_labels: bool) -> Vec<BarGroup<'a>> {
|
||||
@@ -190,7 +200,7 @@ 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()
|
||||
|
||||
@@ -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},
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
@@ -1,4 +1,19 @@
|
||||
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
|
||||
|
||||
use std::{error::Error, io};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
@@ -55,43 +70,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,7 +166,7 @@ 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> {
|
||||
pub(super) fn get_cal<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
|
||||
use Month::*;
|
||||
match m {
|
||||
May => example1(m, y, es),
|
||||
@@ -188,7 +179,7 @@ mod cals {
|
||||
}
|
||||
}
|
||||
|
||||
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,18 @@
|
||||
//! # [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
|
||||
|
||||
use std::{
|
||||
io::{self, stdout, Stdout},
|
||||
time::{Duration, Instant},
|
||||
@@ -59,10 +74,10 @@ impl App {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => break,
|
||||
KeyCode::Down => app.y += 1.0,
|
||||
KeyCode::Up => app.y -= 1.0,
|
||||
KeyCode::Right => app.x += 1.0,
|
||||
KeyCode::Left => app.x -= 1.0,
|
||||
KeyCode::Down | KeyCode::Char('j') => app.y += 1.0,
|
||||
KeyCode::Up | KeyCode::Char('k') => app.y -= 1.0,
|
||||
KeyCode::Right | KeyCode::Char('l') => app.x += 1.0,
|
||||
KeyCode::Left | KeyCode::Char('h') => app.x -= 1.0,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -107,19 +122,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 + '_ {
|
||||
|
||||
@@ -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,18 +24,10 @@ 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, *},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SinSignal {
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,61 +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]);
|
||||
|
||||
let datasets = vec![Dataset::default()
|
||||
.name("data")
|
||||
.marker(symbols::Marker::Braille)
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.graph_type(GraphType::Line)
|
||||
.data(&DATA)];
|
||||
let chart = Chart::new(datasets)
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Chart 2".cyan().bold())
|
||||
.borders(Borders::ALL),
|
||||
)
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
.title("X Axis")
|
||||
.style(Style::default().fg(Color::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))
|
||||
.bounds([0.0, 5.0])
|
||||
.labels(vec!["0".bold(), "2.5".into(), "5.0".bold()]),
|
||||
);
|
||||
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()]),
|
||||
);
|
||||
f.render_widget(chart, chunks[2]);
|
||||
f.render_widget(chart, area);
|
||||
}
|
||||
|
||||
fn render_line_chart(f: &mut Frame, area: Rect) {
|
||||
let datasets = vec![Dataset::default()
|
||||
.name("Line from only 2 points".italic())
|
||||
.marker(symbols::Marker::Braille)
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.graph_type(GraphType::Line)
|
||||
.data(&[(1., 1.), (4., 4.)])];
|
||||
|
||||
let chart = Chart::new(datasets)
|
||||
.block(
|
||||
Block::default()
|
||||
.title(
|
||||
Title::default()
|
||||
.content("Line chart".cyan().bold())
|
||||
.alignment(Alignment::Center),
|
||||
)
|
||||
.borders(Borders::ALL),
|
||||
)
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
.title("X Axis")
|
||||
.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().gray())
|
||||
.bounds([0.0, 5.0])
|
||||
.labels(vec!["0".bold(), "2.5".into(), "5.0".bold()]),
|
||||
)
|
||||
.legend_position(Some(LegendPosition::TopLeft))
|
||||
.hidden_legend_constraints((Constraint::Ratio(1, 2), Constraint::Ratio(1, 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., 177900.),
|
||||
(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., 118500.),
|
||||
(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,3 +1,18 @@
|
||||
//! # [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::{
|
||||
@@ -42,14 +57,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 +89,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 +109,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 +130,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 +151,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 +193,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 +235,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,67 +1,131 @@
|
||||
//! # [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::{
|
||||
error::Error,
|
||||
io::{stdout, Stdout},
|
||||
rc::Rc,
|
||||
time::Duration,
|
||||
io::stdout,
|
||||
panic,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
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, IntoColorUnclamped},
|
||||
Okhsv, Srgb,
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
use palette::{convert::FromColorUnclamped, Okhsv, Srgb};
|
||||
use ratatui::prelude::*;
|
||||
|
||||
type Result<T> = std::result::Result<T, Box<dyn Error>>;
|
||||
#[derive(Debug, Default)]
|
||||
struct App {
|
||||
/// The current state of the app (running or quit)
|
||||
state: AppState,
|
||||
|
||||
fn main() -> Result<()> {
|
||||
install_panic_hook();
|
||||
App::new()?.run()
|
||||
/// 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,
|
||||
}
|
||||
|
||||
struct App {
|
||||
terminal: Terminal<CrosstermBackend<Stdout>>,
|
||||
should_quit: bool,
|
||||
#[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 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>,
|
||||
}
|
||||
|
||||
/// 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>>,
|
||||
|
||||
/// 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 new() -> Result<Self> {
|
||||
Ok(Self {
|
||||
terminal: Terminal::new(CrosstermBackend::new(stdout()))?,
|
||||
should_quit: false,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn run(mut self) -> Result<()> {
|
||||
init_terminal()?;
|
||||
self.terminal.clear()?;
|
||||
while !self.should_quit {
|
||||
self.draw()?;
|
||||
/// 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 draw(&mut self) -> Result<()> {
|
||||
self.terminal.draw(|frame| {
|
||||
frame.render_widget(RgbColors, frame.size());
|
||||
})?;
|
||||
Ok(())
|
||||
fn is_running(&self) -> bool {
|
||||
matches!(self.state, AppState::Running)
|
||||
}
|
||||
|
||||
/// 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<()> {
|
||||
if event::poll(Duration::from_millis(100))? {
|
||||
// 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;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -69,75 +133,151 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for App {
|
||||
fn drop(&mut self) {
|
||||
let _ = restore_terminal();
|
||||
}
|
||||
}
|
||||
|
||||
struct RgbColors;
|
||||
|
||||
impl Widget for RgbColors {
|
||||
/// 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) {
|
||||
let layout = Self::layout(area);
|
||||
Self::render_title(layout[0], buf);
|
||||
Self::render_colors(layout[1], buf);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
impl RgbColors {
|
||||
fn layout(area: Rect) -> Rc<[Rect]> {
|
||||
Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Length(1), Constraint::Min(0)])
|
||||
.split(area)
|
||||
}
|
||||
|
||||
fn render_title(area: Rect, buf: &mut Buffer) {
|
||||
Paragraph::new("colors_rgb example. Press q to quit")
|
||||
.dark_gray()
|
||||
.alignment(Alignment::Center)
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
/// Render a colored grid of half block characters (`"▀"`) each with a different RGB color.
|
||||
fn render_colors(area: Rect, buf: &mut Buffer) {
|
||||
for (xi, x) in (area.left()..area.right()).enumerate() {
|
||||
for (yi, y) in (area.top()..area.bottom()).enumerate() {
|
||||
let hue = xi as f32 * 360.0 / area.width as f32;
|
||||
|
||||
let value_fg = (yi as f32) / (area.height as f32 - 0.5);
|
||||
let fg = Okhsv::<f32>::new(hue, Okhsv::max_saturation(), value_fg);
|
||||
let fg: Srgb = fg.into_color_unclamped();
|
||||
let fg: Srgb<u8> = fg.into_format();
|
||||
let fg = Color::Rgb(fg.red, fg.green, fg.blue);
|
||||
|
||||
let value_bg = (yi as f32 + 0.5) / (area.height as f32 - 0.5);
|
||||
let bg = Okhsv::new(hue, Okhsv::max_saturation(), value_bg);
|
||||
let bg = Srgb::<f32>::from_color_unclamped(bg);
|
||||
let bg: Srgb<u8> = bg.into_format();
|
||||
let bg = Color::Rgb(bg.red, bg.green, bg.blue);
|
||||
|
||||
buf.get_mut(x, y).set_char('▀').set_fg(fg).set_bg(bg);
|
||||
}
|
||||
/// 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Install a panic hook that restores the terminal before panicking.
|
||||
fn install_panic_hook() {
|
||||
better_panic::install();
|
||||
let prev_hook = std::panic::take_hook();
|
||||
std::panic::set_hook(Box::new(move |info| {
|
||||
let _ = restore_terminal();
|
||||
prev_hook(info);
|
||||
}));
|
||||
/// 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!("{:.1} fps", fps);
|
||||
Text::from(text).render(area, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn init_terminal() -> Result<()> {
|
||||
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.
|
||||
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.
|
||||
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 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::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;
|
||||
let saturation = Okhsv::max_saturation();
|
||||
let color = Okhsv::new(hue, saturation, value);
|
||||
let color = Srgb::<f32>::from_color_unclamped(color);
|
||||
let color: Srgb<u8> = color.into_format();
|
||||
let color = Color::Rgb(color.red, color.green, color.blue);
|
||||
row.push(color);
|
||||
}
|
||||
self.colors.push(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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();
|
||||
eyre::set_hook(Box::new(move |e| {
|
||||
let _ = restore_terminal();
|
||||
error(e)
|
||||
}))?;
|
||||
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)?;
|
||||
Ok(())
|
||||
let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
|
||||
terminal.clear()?;
|
||||
terminal.hide_cursor()?;
|
||||
Ok(terminal)
|
||||
}
|
||||
|
||||
fn restore_terminal() -> Result<()> {
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/colors_rgb.tape`
|
||||
Output "target/colors_rgb.gif"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 1200
|
||||
Set Height 800
|
||||
Hide
|
||||
Type "cargo run --example=colors_rgb --features=crossterm"
|
||||
Enter
|
||||
Sleep 2s
|
||||
Screenshot "target/colors_rgb.png"
|
||||
Show
|
||||
Sleep 1s
|
||||
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
|
||||
|
||||
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::*,
|
||||
};
|
||||
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 {
|
||||
selected: bool,
|
||||
legend: bool,
|
||||
constraint: Constraint,
|
||||
}
|
||||
|
||||
/// 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(())
|
||||
}
|
||||
|
||||
/// select the next block with wrap around
|
||||
fn increment_value(&mut self) {
|
||||
if self.constraints.is_empty() {
|
||||
return;
|
||||
}
|
||||
self.constraints[self.selected_index] = match self.constraints[self.selected_index] {
|
||||
Constraint::Length(v) => Constraint::Length(v.saturating_add(1)),
|
||||
Constraint::Min(v) => Constraint::Min(v.saturating_add(1)),
|
||||
Constraint::Max(v) => Constraint::Max(v.saturating_add(1)),
|
||||
Constraint::Fill(v) => Constraint::Fill(v.saturating_add(1)),
|
||||
Constraint::Percentage(v) => Constraint::Percentage(v.saturating_add(1)),
|
||||
Constraint::Ratio(n, d) => Constraint::Ratio(n, d.saturating_add(1)),
|
||||
};
|
||||
}
|
||||
|
||||
fn decrement_value(&mut self) {
|
||||
if self.constraints.is_empty() {
|
||||
return;
|
||||
}
|
||||
self.constraints[self.selected_index] = match self.constraints[self.selected_index] {
|
||||
Constraint::Length(v) => Constraint::Length(v.saturating_sub(1)),
|
||||
Constraint::Min(v) => Constraint::Min(v.saturating_sub(1)),
|
||||
Constraint::Max(v) => Constraint::Max(v.saturating_sub(1)),
|
||||
Constraint::Fill(v) => Constraint::Fill(v.saturating_sub(1)),
|
||||
Constraint::Percentage(v) => Constraint::Percentage(v.saturating_sub(1)),
|
||||
Constraint::Ratio(n, d) => Constraint::Ratio(n, 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, self.value as u32 / 4), // for balance
|
||||
};
|
||||
self.constraints[self.selected_index] = constraint;
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Constraint> for ConstraintName {
|
||||
fn from(constraint: Constraint) -> Self {
|
||||
use Constraint::*;
|
||||
match constraint {
|
||||
Length(_) => ConstraintName::Length,
|
||||
Percentage(_) => ConstraintName::Percentage,
|
||||
Ratio(_, _) => ConstraintName::Ratio,
|
||||
Min(_) => ConstraintName::Min,
|
||||
Max(_) => ConstraintName::Max,
|
||||
Fill(_) => ConstraintName::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);
|
||||
|
||||
self.header().render(header_area, buf);
|
||||
self.instructions().render(instructions_area, buf);
|
||||
self.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(&self) -> impl Widget {
|
||||
let text = "Constraint Explorer";
|
||||
text.bold().fg(Self::HEADER_COLOR).to_centered_line()
|
||||
}
|
||||
|
||||
fn instructions(&self) -> 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(&self) -> 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!("{} px", width)
|
||||
};
|
||||
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;
|
||||
|
||||
fn new(constraint: Constraint, selected: bool, legend: bool) -> Self {
|
||||
Self {
|
||||
constraint,
|
||||
selected,
|
||||
legend,
|
||||
}
|
||||
}
|
||||
|
||||
fn label(&self, width: u16) -> String {
|
||||
let long_width = format!("{} px", width);
|
||||
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 {
|
||||
"".to_string()
|
||||
};
|
||||
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(SpacerBlock::TEXT_COLOR).to_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 {
|
||||
"".to_string()
|
||||
};
|
||||
Line::styled(label, Self::TEXT_COLOR).centered()
|
||||
}
|
||||
|
||||
fn render_2px(&self, area: Rect, buf: &mut Buffer) {
|
||||
if area.width > 1 {
|
||||
Self::block().render(area, buf);
|
||||
} else {
|
||||
Self::line().render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_3px(&self, 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(&self, 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 {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
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([Constraint::Length(3), Constraint::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(&self, area: Rect, buf: &mut Buffer) {
|
||||
let width = area.width as usize;
|
||||
// a bar like `<----- 80 px ----->`
|
||||
let width_label = format!("{} px", width);
|
||||
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.
|
||||
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)
|
||||
}
|
||||
|
||||
fn get_example_count(&self) -> u16 {
|
||||
use SelectedTab::*;
|
||||
match self {
|
||||
Length => 4,
|
||||
Percentage => 5,
|
||||
Ratio => 4,
|
||||
Fill => 2,
|
||||
Min => 5,
|
||||
Max => 5,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_tab_title(value: SelectedTab) -> Line<'static> {
|
||||
use SelectedTab::*;
|
||||
let text = format!(" {value} ");
|
||||
let color = match value {
|
||||
Length => LENGTH_COLOR,
|
||||
Percentage => PERCENTAGE_COLOR,
|
||||
Ratio => RATIO_COLOR,
|
||||
Fill => FILL_COLOR,
|
||||
Min => MIN_COLOR,
|
||||
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 {
|
||||
SelectedTab::Length => self.render_length_example(area, buf),
|
||||
SelectedTab::Percentage => self.render_percentage_example(area, buf),
|
||||
SelectedTab::Ratio => self.render_ratio_example(area, buf),
|
||||
SelectedTab::Fill => self.render_fill_example(area, buf),
|
||||
SelectedTab::Min => self.render_min_example(area, buf),
|
||||
SelectedTab::Max => self.render_max_example(area, buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SelectedTab {
|
||||
fn render_length_example(&self, 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(&self, 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(&self, 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(&self, 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(&self, 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(&self, 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(&self, constraint: Constraint, width: u16) -> Paragraph {
|
||||
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::{
|
||||
@@ -171,42 +186,34 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
|
||||
}
|
||||
|
||||
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());
|
||||
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]);
|
||||
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(
|
||||
@@ -216,12 +223,12 @@ fn handle_key_event(
|
||||
) -> ControlFlow<()> {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return ControlFlow::Break(()),
|
||||
KeyCode::Left => {
|
||||
KeyCode::Left | KeyCode::Char('h') => {
|
||||
button_states[*selected_button] = State::Normal;
|
||||
*selected_button = selected_button.saturating_sub(1);
|
||||
button_states[*selected_button] = State::Selected;
|
||||
}
|
||||
KeyCode::Right => {
|
||||
KeyCode::Right | KeyCode::Char('l') => {
|
||||
button_states[*selected_button] = State::Normal;
|
||||
*selected_button = selected_button.saturating_add(1).min(2);
|
||||
button_states[*selected_button] = State::Selected;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -55,11 +55,11 @@ fn run_app<B: Backend>(
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
KeyCode::Left | KeyCode::Char('h') => app.on_left(),
|
||||
KeyCode::Up | KeyCode::Char('k') => app.on_up(),
|
||||
KeyCode::Right | KeyCode::Char('l') => app.on_right(),
|
||||
KeyCode::Down | KeyCode::Char('j') => app.on_down(),
|
||||
KeyCode::Char(c) => app.on_key(c),
|
||||
KeyCode::Left => app.on_left(),
|
||||
KeyCode::Up => app.on_up(),
|
||||
KeyCode::Right => app.on_right(),
|
||||
KeyCode::Down => app.on_down(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -39,11 +39,11 @@ fn run_app<B: Backend>(
|
||||
|
||||
match events.recv()? {
|
||||
Event::Input(key) => match key {
|
||||
Key::Up | Key::Char('k') => app.on_up(),
|
||||
Key::Down | Key::Char('j') => app.on_down(),
|
||||
Key::Left | Key::Char('h') => app.on_left(),
|
||||
Key::Right | Key::Char('l') => app.on_right(),
|
||||
Key::Char(c) => app.on_key(c),
|
||||
Key::Up => app.on_up(),
|
||||
Key::Down => app.on_down(),
|
||||
Key::Left => app.on_left(),
|
||||
Key::Right => app.on_right(),
|
||||
_ => {}
|
||||
},
|
||||
Event::Tick => app.on_tick(),
|
||||
|
||||
@@ -45,10 +45,10 @@ fn run_app(
|
||||
{
|
||||
match input {
|
||||
InputEvent::Key(key_code) => match key_code.key {
|
||||
KeyCode::UpArrow => app.on_up(),
|
||||
KeyCode::DownArrow => app.on_down(),
|
||||
KeyCode::LeftArrow => app.on_left(),
|
||||
KeyCode::RightArrow => app.on_right(),
|
||||
KeyCode::UpArrow | KeyCode::Char('k') => app.on_up(),
|
||||
KeyCode::DownArrow | KeyCode::Char('j') => app.on_down(),
|
||||
KeyCode::LeftArrow | KeyCode::Char('h') => app.on_left(),
|
||||
KeyCode::RightArrow | KeyCode::Char('l') => app.on_right(),
|
||||
KeyCode::Char(c) => app.on_key(c),
|
||||
_ => {}
|
||||
},
|
||||
|
||||
@@ -6,16 +6,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 +26,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);
|
||||
|
||||
@@ -96,19 +91,14 @@ fn draw_charts(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
} 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 +263,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)
|
||||
@@ -289,18 +277,20 @@ fn draw_second_tab(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
};
|
||||
Row::new(vec![s.name, s.location, s.status]).style(style)
|
||||
});
|
||||
let table = Table::new(rows)
|
||||
.header(
|
||||
Row::new(vec!["Server", "Location", "Status"])
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.bottom_margin(1),
|
||||
)
|
||||
.block(Block::default().title("Servers").borders(Borders::ALL))
|
||||
.widths([
|
||||
let table = Table::new(
|
||||
rows,
|
||||
[
|
||||
Constraint::Length(15),
|
||||
Constraint::Length(15),
|
||||
Constraint::Length(10),
|
||||
]);
|
||||
],
|
||||
)
|
||||
.header(
|
||||
Row::new(vec!["Server", "Location", "Status"])
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.bottom_margin(1),
|
||||
)
|
||||
.block(Block::default().title("Servers").borders(Borders::ALL));
|
||||
f.render_widget(table, chunks[0]);
|
||||
|
||||
let map = Canvas::default()
|
||||
@@ -359,10 +349,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,
|
||||
@@ -393,12 +380,14 @@ fn draw_third_tab(f: &mut Frame, _app: &mut App, area: Rect) {
|
||||
Row::new(cells)
|
||||
})
|
||||
.collect();
|
||||
let table = Table::new(items)
|
||||
.block(Block::default().title("Colors").borders(Borders::ALL))
|
||||
.widths([
|
||||
let table = Table::new(
|
||||
items,
|
||||
[
|
||||
Constraint::Ratio(1, 3),
|
||||
Constraint::Ratio(1, 3),
|
||||
Constraint::Ratio(1, 3),
|
||||
]);
|
||||
],
|
||||
)
|
||||
.block(Block::default().title("Colors").borders(Borders::ALL));
|
||||
f.render_widget(table, chunks[0]);
|
||||
}
|
||||
|
||||
@@ -1,99 +1,223 @@
|
||||
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))?)
|
||||
}
|
||||
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) -> Result<()> {
|
||||
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);
|
||||
self.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| 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(&self, 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 {
|
||||
Tab::About => "".to_string(),
|
||||
tab => format!(" {} ", tab),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
815
examples/demo2/big_text.rs
Normal file
815
examples/demo2/big_text.rs
Normal file
@@ -0,0 +1,815 @@
|
||||
//! [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, widgets::Widget};
|
||||
|
||||
#[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
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
fn get_symbol_half_size(top_left: u8, top_right: u8, bottom_left: u8, bottom_right: u8) -> char {
|
||||
let top_left = if top_left > 0 { 1 } else { 0 };
|
||||
let top_right = if top_right > 0 { 1 } else { 0 };
|
||||
let bottom_left = if bottom_left > 0 { 1 } else { 0 };
|
||||
let bottom_right = if bottom_right > 0 { 1 } else { 0 };
|
||||
|
||||
const QUADRANT_SYMBOLS: [char; 16] = [
|
||||
' ', '▘', '▝', '▀', '▖', '▌', '▞', '▛', '▗', '▚', '▐', '▜', '▄', '▙', '▟', '█',
|
||||
];
|
||||
QUADRANT_SYMBOLS[top_left + (top_right << 1) + (bottom_left << 2) + (bottom_right << 3)]
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
|
||||
131
examples/demo2/destroy.rs
Normal file
131
examples/demo2/destroy.rs
Normal file
@@ -0,0 +1,131 @@
|
||||
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.
|
||||
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 / ramp_frames as f64;
|
||||
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
|
||||
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 red = mask_red as f64 * percentage + cell_red as f64 * (1.0 - percentage);
|
||||
let green = mask_green as f64 * percentage + cell_green as f64 * (1.0 - percentage);
|
||||
let blue = mask_blue as f64 * percentage + cell_blue as f64 * (1.0 - percentage);
|
||||
|
||||
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,37 @@
|
||||
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
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +122,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 {
|
||||
@@ -84,16 +84,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 +121,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,13 +148,12 @@ 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().cloned();
|
||||
let theme = THEME.recipe;
|
||||
StatefulWidget::render(
|
||||
Table::new(rows)
|
||||
Table::new(rows, [Constraint::Length(7), Constraint::Length(30)])
|
||||
.block(Block::new().style(theme.ingredients))
|
||||
.header(Row::new(vec!["Qty", "Ingredient"]).style(theme.ingredients_header))
|
||||
.widths([Constraint::Length(7), Constraint::Length(30)])
|
||||
.highlight_style(Style::new().light_yellow()),
|
||||
area,
|
||||
buf,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,9 +54,8 @@ fn render_hops(selected_row: usize, area: Rect, buf: &mut Buffer) {
|
||||
.title_alignment(Alignment::Center)
|
||||
.padding(Padding::new(1, 1, 1, 1));
|
||||
StatefulWidget::render(
|
||||
Table::new(rows)
|
||||
Table::new(rows, [Constraint::Max(100), Constraint::Length(15)])
|
||||
.header(Row::new(vec!["Host", "Address"]).set_style(THEME.traceroute.header))
|
||||
.widths([Constraint::Max(100), Constraint::Length(15)])
|
||||
.highlight_style(THEME.traceroute.selected)
|
||||
.block(block),
|
||||
area,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,7 +128,7 @@ fn render_horizontal_barchart(area: Rect, buf: &mut Buffer) {
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
pub fn render_gauges(progress: usize, area: Rect, buf: &mut Buffer) {
|
||||
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);
|
||||
|
||||
@@ -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,3 +1,18 @@
|
||||
//! # [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::{
|
||||
@@ -6,7 +21,7 @@ use crossterm::{
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
/// 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.
|
||||
@@ -53,48 +68,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(
|
||||
|
||||
560
examples/flex.rs
Normal file
560
examples/flex.rs
Normal file
@@ -0,0 +1,560 @@
|
||||
//! # [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
|
||||
|
||||
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(|(desc, _)| get_description_height(desc) + 4)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// 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(&self, 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!("{} px (gap: {} px)", width, spacing)
|
||||
} else {
|
||||
format!("{} px", width)
|
||||
};
|
||||
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
|
||||
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: SelectedTab) -> Line<'static> {
|
||||
use tailwind::*;
|
||||
use SelectedTab::*;
|
||||
let text = value.to_string();
|
||||
let color = match value {
|
||||
Legacy => ORANGE.c400,
|
||||
Start => SKY.c400,
|
||||
Center => SKY.c300,
|
||||
End => SKY.c200,
|
||||
SpaceAround => INDIGO.c400,
|
||||
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 {
|
||||
SelectedTab::Legacy => self.render_examples(area, buf, Flex::Legacy, spacing),
|
||||
SelectedTab::Start => self.render_examples(area, buf, Flex::Start, spacing),
|
||||
SelectedTab::Center => self.render_examples(area, buf, Flex::Center, spacing),
|
||||
SelectedTab::End => self.render_examples(area, buf, Flex::End, spacing),
|
||||
SelectedTab::SpaceAround => self.render_examples(area, buf, Flex::SpaceAround, spacing),
|
||||
SelectedTab::SpaceBetween => {
|
||||
self.render_examples(area, buf, Flex::SpaceBetween, spacing)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SelectedTab {
|
||||
fn render_examples(&self, 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(&self, 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 {
|
||||
"".to_string()
|
||||
};
|
||||
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(&self, constraint: Constraint, width: u16) -> Paragraph {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
fn get_description_height(s: &str) -> u16 {
|
||||
if s.is_empty() {
|
||||
0
|
||||
} else {
|
||||
s.split('\n').count() as u16
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,21 @@
|
||||
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;
|
||||
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 +23,85 @@ use crossterm::{
|
||||
};
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
style::palette::tailwind,
|
||||
widgets::{block::Title, *},
|
||||
};
|
||||
|
||||
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 = self.progress_columns as f64 * 100.0 / terminal_width as f64;
|
||||
|
||||
// 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 +109,132 @@ 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 {
|
||||
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);
|
||||
|
||||
self.render_header(header_area, buf);
|
||||
self.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);
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn render_header(&self, area: Rect, buf: &mut Buffer) {
|
||||
Paragraph::new("Ratatui Gauge Example")
|
||||
.bold()
|
||||
.alignment(Alignment::Center)
|
||||
.fg(CUSTOM_LABEL_COLOR)
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
fn render_footer(&self, area: Rect, buf: &mut Buffer) {
|
||||
Paragraph::new("Press ENTER to start")
|
||||
.alignment(Alignment::Center)
|
||||
.fg(CUSTOM_LABEL_COLOR)
|
||||
.bold()
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
//! # [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
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, VecDeque},
|
||||
error::Error,
|
||||
@@ -215,15 +230,15 @@ 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();
|
||||
@@ -231,12 +246,7 @@ fn ui(f: &mut Frame, downloads: &Downloads) {
|
||||
.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 +269,21 @@ 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);
|
||||
|
||||
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,18 @@
|
||||
//! # [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
|
||||
|
||||
use std::{error::Error, io};
|
||||
|
||||
use crossterm::{
|
||||
@@ -48,56 +63,49 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
|
||||
}
|
||||
|
||||
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([
|
||||
Constraint::Length(14),
|
||||
Constraint::Length(14),
|
||||
Constraint::Length(14),
|
||||
Constraint::Length(14),
|
||||
Constraint::Length(14),
|
||||
Constraint::Min(0), // fills remaining space
|
||||
])
|
||||
.split(*area)
|
||||
.iter()
|
||||
.copied()
|
||||
.take(5) // ignore Min(0)
|
||||
.collect_vec()
|
||||
})
|
||||
.collect_vec();
|
||||
|
||||
@@ -182,10 +190,7 @@ 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);
|
||||
let layout = Layout::vertical(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)]);
|
||||
}
|
||||
@@ -199,13 +204,11 @@ 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 {
|
||||
@@ -214,6 +217,7 @@ fn constraint_label(constraint: Constraint) -> String {
|
||||
Min(n) => format!("{n}"),
|
||||
Max(n) => format!("{n}"),
|
||||
Percentage(n) => format!("{n}"),
|
||||
Fill(n) => format!("{n}"),
|
||||
Ratio(a, b) => format!("{a}:{b}"),
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
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 App<'_> {
|
||||
fn new<'a>() -> App<'a> {
|
||||
App {
|
||||
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);
|
||||
|
||||
self.render_title(header_area, buf);
|
||||
self.render_todo(upper_item_list_area, buf);
|
||||
self.render_info(lower_item_list_area, buf);
|
||||
self.render_footer(footer_area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl App<'_> {
|
||||
fn render_title(&self, area: Rect, buf: &mut Buffer) {
|
||||
Paragraph::new("Ratatui List Example")
|
||||
.bold()
|
||||
.centered()
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
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_footer(&self, 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 => app.items.unselect(),
|
||||
KeyCode::Down => app.items.next(),
|
||||
KeyCode::Up => 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"))
|
||||
.start_corner(Corner::BottomLeft);
|
||||
f.render_widget(events_list, chunks[1]);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
//! # [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.
|
||||
@@ -44,24 +59,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()
|
||||
})
|
||||
|
||||
@@ -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.
|
||||
//!
|
||||
@@ -128,7 +143,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,
|
||||
@@ -90,15 +105,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 +136,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,20 @@
|
||||
//! # [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::{
|
||||
@@ -62,11 +79,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 +90,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 +110,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]
|
||||
}
|
||||
|
||||
86
examples/ratatui-logo.rs
Normal file
86
examples/ratatui-logo.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
//! # [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,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
||||
use indoc::indoc;
|
||||
use itertools::izip;
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
/// A fun example of using half block characters to draw a logo
|
||||
fn main() -> io::Result<()> {
|
||||
let r = indoc! {"
|
||||
▄▄▄
|
||||
█▄▄▀
|
||||
█ █
|
||||
"}
|
||||
.lines();
|
||||
let a = indoc! {"
|
||||
▄▄
|
||||
█▄▄█
|
||||
█ █
|
||||
"}
|
||||
.lines();
|
||||
let t = indoc! {"
|
||||
▄▄▄
|
||||
█
|
||||
█
|
||||
"}
|
||||
.lines();
|
||||
let u = indoc! {"
|
||||
▄ ▄
|
||||
█ █
|
||||
▀▄▄▀
|
||||
"}
|
||||
.lines();
|
||||
let i = indoc! {"
|
||||
▄
|
||||
█
|
||||
█
|
||||
"}
|
||||
.lines();
|
||||
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());
|
||||
})?;
|
||||
sleep(Duration::from_secs(5));
|
||||
restore()?;
|
||||
println!();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn init() -> io::Result<Terminal<impl Backend>> {
|
||||
enable_raw_mode()?;
|
||||
let options = TerminalOptions {
|
||||
viewport: Viewport::Inline(3),
|
||||
};
|
||||
Terminal::with_options(CrosstermBackend::new(stdout()), options)
|
||||
}
|
||||
|
||||
pub fn restore() -> io::Result<()> {
|
||||
disable_raw_mode()?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,3 +1,18 @@
|
||||
//! # [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
|
||||
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
@@ -62,22 +77,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);
|
||||
@@ -100,19 +115,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 "),
|
||||
@@ -145,18 +155,10 @@ fn ui(f: &mut Frame, app: &mut App) {
|
||||
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("Use h j k l or ◄ ▲ ▼ ► to scroll ".bold())
|
||||
.title_alignment(Alignment::Center);
|
||||
f.render_widget(title, chunks[0]);
|
||||
|
||||
|
||||
@@ -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,
|
||||
@@ -125,14 +140,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,18 @@
|
||||
//! # [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
|
||||
|
||||
use std::{error::Error, io};
|
||||
|
||||
use crossterm::{
|
||||
@@ -5,38 +20,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> {
|
||||
impl TableColors {
|
||||
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 {
|
||||
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() -> App {
|
||||
let data_vec = generate_fake_names();
|
||||
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"],
|
||||
],
|
||||
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 +119,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 +134,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 +210,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 => app.next(),
|
||||
KeyCode::Up => 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,38 +225,143 @@ 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)
|
||||
});
|
||||
let t = Table::new(rows)
|
||||
.header(header)
|
||||
.block(Block::default().borders(Borders::ALL).title("Table"))
|
||||
.highlight_style(selected_style)
|
||||
.highlight_symbol(">> ")
|
||||
.widths([
|
||||
Constraint::Percentage(50),
|
||||
Constraint::Max(30),
|
||||
Constraint::Min(10),
|
||||
]);
|
||||
f.render_stateful_widget(t, rects[0], &mut app.state);
|
||||
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"]
|
||||
.iter()
|
||||
.cloned()
|
||||
.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.iter()
|
||||
.cloned()
|
||||
.map(|content| Cell::from(Text::from(format!("\n{}\n", content))))
|
||||
.collect::<Row>()
|
||||
.style(Style::new().fg(app.colors.row_fg).bg(color))
|
||||
.height(4)
|
||||
});
|
||||
let bar = " █ ";
|
||||
let t = Table::new(
|
||||
rows,
|
||||
[
|
||||
// + 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)
|
||||
.highlight_style(selected_style)
|
||||
.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);
|
||||
|
||||
(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: &mut 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);
|
||||
}
|
||||
}
|
||||
|
||||
310
examples/tabs.rs
310
examples/tabs.rs
@@ -1,112 +1,250 @@
|
||||
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
|
||||
|
||||
use std::{io, 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) -> Result<(), io::Error> {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
use KeyCode::*;
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Right => app.next(),
|
||||
KeyCode::Left => 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);
|
||||
|
||||
self.render_title(title_area, buf);
|
||||
self.render_tabs(tabs_area, buf);
|
||||
self.selected_tab.render(inner_area, buf);
|
||||
self.render_footer(footer_area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn render_title(&self, area: Rect, buf: &mut Buffer) {
|
||||
"Ratatui Tabs Example".bold().render(area, buf);
|
||||
}
|
||||
|
||||
fn render_tabs(&self, area: Rect, buf: &mut Buffer) {
|
||||
let titles = SelectedTab::iter().map(|tab| tab.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_footer(&self, 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 {
|
||||
SelectedTab::Tab1 => self.render_tab0(area, buf),
|
||||
SelectedTab::Tab2 => self.render_tab1(area, buf),
|
||||
SelectedTab::Tab3 => self.render_tab2(area, buf),
|
||||
SelectedTab::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)
|
||||
}
|
||||
|
||||
fn palette(&self) -> tailwind::Palette {
|
||||
match self {
|
||||
SelectedTab::Tab1 => tailwind::BLUE,
|
||||
SelectedTab::Tab2 => tailwind::EMERALD,
|
||||
SelectedTab::Tab3 => tailwind::INDIGO,
|
||||
SelectedTab::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,19 +1,34 @@
|
||||
//! # [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
|
||||
|
||||
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.
|
||||
/// 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 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.
|
||||
/// * 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 crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
@@ -172,14 +187,12 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
|
||||
}
|
||||
|
||||
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 +216,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 +226,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
|
||||
@@ -226,9 +238,9 @@ fn ui(f: &mut Frame, app: &App) {
|
||||
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 +256,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);
|
||||
}
|
||||
|
||||
21
examples/vhs/colors_rgb.tape
Normal file
21
examples/vhs/colors_rgb.tape
Normal file
@@ -0,0 +1,21 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/colors_rgb.tape`
|
||||
|
||||
# note that this script sometimes results in the gif having screen tearing
|
||||
# issues. I'm not sure why, but it's not a problem with the library.
|
||||
Output "target/colors_rgb.gif"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 1200
|
||||
Set Height 1200
|
||||
|
||||
# unsure if these help the screen tearing issue, but they don't hurt
|
||||
Set Framerate 60
|
||||
Set CursorBlink false
|
||||
|
||||
Hide
|
||||
Type "cargo run --example=colors_rgb --features=crossterm --release"
|
||||
Enter
|
||||
Sleep 2s
|
||||
# Screenshot "target/colors_rgb.png"
|
||||
Show
|
||||
Sleep 10s
|
||||
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
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
|
||||
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
|
||||
12
examples/vhs/ratatui-logo.tape
Normal file
12
examples/vhs/ratatui-logo.tape
Normal file
@@ -0,0 +1,12 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/popup.tape`
|
||||
Output "target/ratatui-logo.gif"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 550
|
||||
Set Height 220
|
||||
Hide
|
||||
Type "cargo run --example=ratatui-logo --features=crossterm"
|
||||
Enter
|
||||
Sleep 2s
|
||||
Show
|
||||
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
|
||||
@@ -3,3 +3,4 @@ group_imports = "StdExternalCrate"
|
||||
imports_granularity = "Crate"
|
||||
wrap_comments = true
|
||||
comment_width = 100
|
||||
format_code_in_doc_comments = true
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
//!
|
||||
//! Additionally, a [`TestBackend`] is provided for testing purposes.
|
||||
//!
|
||||
//! See the [Backend Comparison] section of the [Ratatui Book] for more details on the different
|
||||
//! See the [Backend Comparison] section of the [Ratatui Website] for more details on the different
|
||||
//! backends.
|
||||
//!
|
||||
//! Each backend supports a number of features, such as [raw mode](#raw-mode), [alternate
|
||||
@@ -26,6 +26,7 @@
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use std::io::stdout;
|
||||
//!
|
||||
//! use ratatui::prelude::*;
|
||||
//!
|
||||
//! let backend = CrosstermBackend::new(stdout());
|
||||
@@ -37,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
|
||||
//!
|
||||
@@ -95,10 +96,10 @@
|
||||
//! [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
|
||||
//! [Ratatui Book]: https://ratatui-org.github.io/ratatui-book
|
||||
//! https://ratatui.rs/concepts/backends/comparison/
|
||||
//! [Ratatui Website]: https://ratatui-org.github.io/ratatui-book
|
||||
use std::io;
|
||||
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
@@ -10,8 +10,8 @@ use crossterm::{
|
||||
cursor::{Hide, MoveTo, Show},
|
||||
execute, queue,
|
||||
style::{
|
||||
Attribute as CAttribute, Color as CColor, Print, SetAttribute, SetBackgroundColor,
|
||||
SetForegroundColor,
|
||||
Attribute as CAttribute, Attributes as CAttributes, Color as CColor, ContentStyle, Print,
|
||||
SetAttribute, SetBackgroundColor, SetForegroundColor,
|
||||
},
|
||||
terminal::{self, Clear},
|
||||
};
|
||||
@@ -21,7 +21,7 @@ use crate::{
|
||||
buffer::Cell,
|
||||
layout::Size,
|
||||
prelude::Rect,
|
||||
style::{Color, Modifier},
|
||||
style::{Color, Modifier, Style},
|
||||
};
|
||||
|
||||
/// A [`Backend`] implementation that uses [Crossterm] to render to the terminal.
|
||||
@@ -43,7 +43,8 @@ use crate::{
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use std::io::{stdout, stderr};
|
||||
/// use std::io::{stderr, stdout};
|
||||
///
|
||||
/// use crossterm::{
|
||||
/// terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
/// ExecutableCommand,
|
||||
@@ -69,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.
|
||||
@@ -274,6 +275,32 @@ impl From<Color> for CColor {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CColor> for Color {
|
||||
fn from(value: CColor) -> Self {
|
||||
match value {
|
||||
CColor::Reset => Self::Reset,
|
||||
CColor::Black => Self::Black,
|
||||
CColor::DarkRed => Self::Red,
|
||||
CColor::DarkGreen => Self::Green,
|
||||
CColor::DarkYellow => Self::Yellow,
|
||||
CColor::DarkBlue => Self::Blue,
|
||||
CColor::DarkMagenta => Self::Magenta,
|
||||
CColor::DarkCyan => Self::Cyan,
|
||||
CColor::Grey => Self::Gray,
|
||||
CColor::DarkGrey => Self::DarkGray,
|
||||
CColor::Red => Self::LightRed,
|
||||
CColor::Green => Self::LightGreen,
|
||||
CColor::Blue => Self::LightBlue,
|
||||
CColor::Yellow => Self::LightYellow,
|
||||
CColor::Magenta => Self::LightMagenta,
|
||||
CColor::Cyan => Self::LightCyan,
|
||||
CColor::White => Self::White,
|
||||
CColor::Rgb { r, g, b } => Self::Rgb(r, g, b),
|
||||
CColor::AnsiValue(v) => Self::Indexed(v),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The `ModifierDiff` struct is used to calculate the difference between two `Modifier`
|
||||
/// values. This is useful when updating the terminal display, as it allows for more
|
||||
/// efficient updates by only sending the necessary changes.
|
||||
@@ -344,3 +371,303 @@ impl ModifierDiff {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CAttribute> for Modifier {
|
||||
fn from(value: CAttribute) -> Self {
|
||||
// `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))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CAttributes> for Modifier {
|
||||
fn from(value: CAttributes) -> Self {
|
||||
let mut res = Modifier::empty();
|
||||
|
||||
if value.has(CAttribute::Bold) {
|
||||
res |= Modifier::BOLD;
|
||||
}
|
||||
if value.has(CAttribute::Dim) {
|
||||
res |= Modifier::DIM;
|
||||
}
|
||||
if value.has(CAttribute::Italic) {
|
||||
res |= Modifier::ITALIC;
|
||||
}
|
||||
if value.has(CAttribute::Underlined)
|
||||
|| value.has(CAttribute::DoubleUnderlined)
|
||||
|| value.has(CAttribute::Undercurled)
|
||||
|| value.has(CAttribute::Underdotted)
|
||||
|| value.has(CAttribute::Underdashed)
|
||||
{
|
||||
res |= Modifier::UNDERLINED;
|
||||
}
|
||||
if value.has(CAttribute::SlowBlink) {
|
||||
res |= Modifier::SLOW_BLINK;
|
||||
}
|
||||
if value.has(CAttribute::RapidBlink) {
|
||||
res |= Modifier::RAPID_BLINK;
|
||||
}
|
||||
if value.has(CAttribute::Reverse) {
|
||||
res |= Modifier::REVERSED;
|
||||
}
|
||||
if value.has(CAttribute::Hidden) {
|
||||
res |= Modifier::HIDDEN;
|
||||
}
|
||||
if value.has(CAttribute::CrossedOut) {
|
||||
res |= Modifier::CROSSED_OUT;
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ContentStyle> for Style {
|
||||
fn from(value: ContentStyle) -> Self {
|
||||
let mut sub_modifier = Modifier::empty();
|
||||
|
||||
if value.attributes.has(CAttribute::NoBold) {
|
||||
sub_modifier |= Modifier::BOLD;
|
||||
}
|
||||
if value.attributes.has(CAttribute::NoItalic) {
|
||||
sub_modifier |= Modifier::ITALIC;
|
||||
}
|
||||
if value.attributes.has(CAttribute::NotCrossedOut) {
|
||||
sub_modifier |= Modifier::CROSSED_OUT;
|
||||
}
|
||||
if value.attributes.has(CAttribute::NoUnderline) {
|
||||
sub_modifier |= Modifier::UNDERLINED;
|
||||
}
|
||||
if value.attributes.has(CAttribute::NoHidden) {
|
||||
sub_modifier |= Modifier::HIDDEN;
|
||||
}
|
||||
if value.attributes.has(CAttribute::NoBlink) {
|
||||
sub_modifier |= Modifier::RAPID_BLINK | Modifier::SLOW_BLINK;
|
||||
}
|
||||
if value.attributes.has(CAttribute::NoReverse) {
|
||||
sub_modifier |= Modifier::REVERSED;
|
||||
}
|
||||
|
||||
Self {
|
||||
fg: value.foreground_color.map(|c| c.into()),
|
||||
bg: value.background_color.map(|c| c.into()),
|
||||
#[cfg(feature = "underline-color")]
|
||||
underline_color: value.underline_color.map(|c| c.into()),
|
||||
add_modifier: value.attributes.into(),
|
||||
sub_modifier,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn from_crossterm_color() {
|
||||
assert_eq!(Color::from(CColor::Reset), Color::Reset);
|
||||
assert_eq!(Color::from(CColor::Black), Color::Black);
|
||||
assert_eq!(Color::from(CColor::DarkGrey), Color::DarkGray);
|
||||
assert_eq!(Color::from(CColor::Red), Color::LightRed);
|
||||
assert_eq!(Color::from(CColor::DarkRed), Color::Red);
|
||||
assert_eq!(Color::from(CColor::Green), Color::LightGreen);
|
||||
assert_eq!(Color::from(CColor::DarkGreen), Color::Green);
|
||||
assert_eq!(Color::from(CColor::Yellow), Color::LightYellow);
|
||||
assert_eq!(Color::from(CColor::DarkYellow), Color::Yellow);
|
||||
assert_eq!(Color::from(CColor::Blue), Color::LightBlue);
|
||||
assert_eq!(Color::from(CColor::DarkBlue), Color::Blue);
|
||||
assert_eq!(Color::from(CColor::Magenta), Color::LightMagenta);
|
||||
assert_eq!(Color::from(CColor::DarkMagenta), Color::Magenta);
|
||||
assert_eq!(Color::from(CColor::Cyan), Color::LightCyan);
|
||||
assert_eq!(Color::from(CColor::DarkCyan), Color::Cyan);
|
||||
assert_eq!(Color::from(CColor::White), Color::White);
|
||||
assert_eq!(Color::from(CColor::Grey), Color::Gray);
|
||||
assert_eq!(
|
||||
Color::from(CColor::Rgb { r: 0, g: 0, b: 0 }),
|
||||
Color::Rgb(0, 0, 0)
|
||||
);
|
||||
assert_eq!(
|
||||
Color::from(CColor::Rgb {
|
||||
r: 10,
|
||||
g: 20,
|
||||
b: 30
|
||||
}),
|
||||
Color::Rgb(10, 20, 30)
|
||||
);
|
||||
assert_eq!(Color::from(CColor::AnsiValue(32)), Color::Indexed(32));
|
||||
assert_eq!(Color::from(CColor::AnsiValue(37)), Color::Indexed(37));
|
||||
}
|
||||
|
||||
mod modifier {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn from_crossterm_attribute() {
|
||||
assert_eq!(Modifier::from(CAttribute::Reset), Modifier::empty());
|
||||
assert_eq!(Modifier::from(CAttribute::Bold), Modifier::BOLD);
|
||||
assert_eq!(Modifier::from(CAttribute::Italic), Modifier::ITALIC);
|
||||
assert_eq!(Modifier::from(CAttribute::Underlined), Modifier::UNDERLINED);
|
||||
assert_eq!(
|
||||
Modifier::from(CAttribute::DoubleUnderlined),
|
||||
Modifier::UNDERLINED
|
||||
);
|
||||
assert_eq!(
|
||||
Modifier::from(CAttribute::Underdotted),
|
||||
Modifier::UNDERLINED
|
||||
);
|
||||
assert_eq!(Modifier::from(CAttribute::Dim), Modifier::DIM);
|
||||
assert_eq!(
|
||||
Modifier::from(CAttribute::NormalIntensity),
|
||||
Modifier::empty()
|
||||
);
|
||||
assert_eq!(
|
||||
Modifier::from(CAttribute::CrossedOut),
|
||||
Modifier::CROSSED_OUT
|
||||
);
|
||||
assert_eq!(Modifier::from(CAttribute::NoUnderline), Modifier::empty());
|
||||
assert_eq!(Modifier::from(CAttribute::OverLined), Modifier::empty());
|
||||
assert_eq!(Modifier::from(CAttribute::SlowBlink), Modifier::SLOW_BLINK);
|
||||
assert_eq!(
|
||||
Modifier::from(CAttribute::RapidBlink),
|
||||
Modifier::RAPID_BLINK
|
||||
);
|
||||
assert_eq!(Modifier::from(CAttribute::Hidden), Modifier::HIDDEN);
|
||||
assert_eq!(Modifier::from(CAttribute::NoHidden), Modifier::empty());
|
||||
assert_eq!(Modifier::from(CAttribute::Reverse), Modifier::REVERSED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_crossterm_attributes() {
|
||||
assert_eq!(
|
||||
Modifier::from(CAttributes::from(CAttribute::Bold)),
|
||||
Modifier::BOLD
|
||||
);
|
||||
assert_eq!(
|
||||
Modifier::from(CAttributes::from(
|
||||
[CAttribute::Bold, CAttribute::Italic].as_ref()
|
||||
)),
|
||||
Modifier::BOLD | Modifier::ITALIC
|
||||
);
|
||||
assert_eq!(
|
||||
Modifier::from(CAttributes::from(
|
||||
[CAttribute::Bold, CAttribute::NotCrossedOut].as_ref()
|
||||
)),
|
||||
Modifier::BOLD
|
||||
);
|
||||
assert_eq!(
|
||||
Modifier::from(CAttributes::from(
|
||||
[CAttribute::Dim, CAttribute::Underdotted].as_ref()
|
||||
)),
|
||||
Modifier::DIM | Modifier::UNDERLINED
|
||||
);
|
||||
assert_eq!(
|
||||
Modifier::from(CAttributes::from(
|
||||
[CAttribute::Dim, CAttribute::SlowBlink, CAttribute::Italic].as_ref()
|
||||
)),
|
||||
Modifier::DIM | Modifier::SLOW_BLINK | Modifier::ITALIC
|
||||
);
|
||||
assert_eq!(
|
||||
Modifier::from(CAttributes::from(
|
||||
[
|
||||
CAttribute::Hidden,
|
||||
CAttribute::NoUnderline,
|
||||
CAttribute::NotCrossedOut
|
||||
]
|
||||
.as_ref()
|
||||
)),
|
||||
Modifier::HIDDEN
|
||||
);
|
||||
assert_eq!(
|
||||
Modifier::from(CAttributes::from(CAttribute::Reverse)),
|
||||
Modifier::REVERSED
|
||||
);
|
||||
assert_eq!(
|
||||
Modifier::from(CAttributes::from(CAttribute::Reset)),
|
||||
Modifier::empty()
|
||||
);
|
||||
assert_eq!(
|
||||
Modifier::from(CAttributes::from(
|
||||
[CAttribute::RapidBlink, CAttribute::CrossedOut].as_ref()
|
||||
)),
|
||||
Modifier::RAPID_BLINK | Modifier::CROSSED_OUT
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_crossterm_content_style() {
|
||||
assert_eq!(Style::from(ContentStyle::default()), Style::default());
|
||||
assert_eq!(
|
||||
Style::from(ContentStyle {
|
||||
foreground_color: Some(CColor::DarkYellow),
|
||||
..Default::default()
|
||||
}),
|
||||
Style::default().fg(Color::Yellow)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from(ContentStyle {
|
||||
background_color: Some(CColor::DarkYellow),
|
||||
..Default::default()
|
||||
}),
|
||||
Style::default().bg(Color::Yellow)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from(ContentStyle {
|
||||
attributes: CAttributes::from(CAttribute::Bold),
|
||||
..Default::default()
|
||||
}),
|
||||
Style::default().add_modifier(Modifier::BOLD)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from(ContentStyle {
|
||||
attributes: CAttributes::from(CAttribute::NoBold),
|
||||
..Default::default()
|
||||
}),
|
||||
Style::default().remove_modifier(Modifier::BOLD)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from(ContentStyle {
|
||||
attributes: CAttributes::from(CAttribute::Italic),
|
||||
..Default::default()
|
||||
}),
|
||||
Style::default().add_modifier(Modifier::ITALIC)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from(ContentStyle {
|
||||
attributes: CAttributes::from(CAttribute::NoItalic),
|
||||
..Default::default()
|
||||
}),
|
||||
Style::default().remove_modifier(Modifier::ITALIC)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from(ContentStyle {
|
||||
attributes: CAttributes::from([CAttribute::Bold, CAttribute::Italic].as_ref()),
|
||||
..Default::default()
|
||||
}),
|
||||
Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.add_modifier(Modifier::ITALIC)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from(ContentStyle {
|
||||
attributes: CAttributes::from([CAttribute::NoBold, CAttribute::NoItalic].as_ref()),
|
||||
..Default::default()
|
||||
}),
|
||||
Style::default()
|
||||
.remove_modifier(Modifier::BOLD)
|
||||
.remove_modifier(Modifier::ITALIC)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "underline-color")]
|
||||
fn from_crossterm_content_style_underline() {
|
||||
assert_eq!(
|
||||
Style::from(ContentStyle {
|
||||
underline_color: Some(CColor::DarkRed),
|
||||
..Default::default()
|
||||
}),
|
||||
Style::default().underline_color(Color::Red)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,11 +9,13 @@ use std::{
|
||||
io::{self, Write},
|
||||
};
|
||||
|
||||
use termion::{color as tcolor, style as tstyle};
|
||||
|
||||
use crate::{
|
||||
backend::{Backend, ClearType, WindowSize},
|
||||
buffer::Cell,
|
||||
prelude::Rect,
|
||||
style::{Color, Modifier},
|
||||
style::{Color, Modifier, Style},
|
||||
};
|
||||
|
||||
/// A [`Backend`] implementation that uses [Termion] to render to the terminal.
|
||||
@@ -36,7 +38,8 @@ use crate::{
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use std::io::{stdout, stderr};
|
||||
/// use std::io::{stderr, stdout};
|
||||
///
|
||||
/// use ratatui::prelude::*;
|
||||
/// use termion::{raw::IntoRawMode, screen::IntoAlternateScreen};
|
||||
///
|
||||
@@ -274,6 +277,82 @@ impl fmt::Display for Bg {
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! from_termion_for_color {
|
||||
($termion_color:ident, $color: ident) => {
|
||||
impl From<tcolor::$termion_color> for Color {
|
||||
fn from(_: tcolor::$termion_color) -> Self {
|
||||
Color::$color
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tcolor::Bg<tcolor::$termion_color>> for Style {
|
||||
fn from(_: tcolor::Bg<tcolor::$termion_color>) -> Self {
|
||||
Style::default().bg(Color::$color)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tcolor::Fg<tcolor::$termion_color>> for Style {
|
||||
fn from(_: tcolor::Fg<tcolor::$termion_color>) -> Self {
|
||||
Style::default().fg(Color::$color)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
from_termion_for_color!(Reset, Reset);
|
||||
from_termion_for_color!(Black, Black);
|
||||
from_termion_for_color!(Red, Red);
|
||||
from_termion_for_color!(Green, Green);
|
||||
from_termion_for_color!(Yellow, Yellow);
|
||||
from_termion_for_color!(Blue, Blue);
|
||||
from_termion_for_color!(Magenta, Magenta);
|
||||
from_termion_for_color!(Cyan, Cyan);
|
||||
from_termion_for_color!(White, Gray);
|
||||
from_termion_for_color!(LightBlack, DarkGray);
|
||||
from_termion_for_color!(LightRed, LightRed);
|
||||
from_termion_for_color!(LightGreen, LightGreen);
|
||||
from_termion_for_color!(LightBlue, LightBlue);
|
||||
from_termion_for_color!(LightYellow, LightYellow);
|
||||
from_termion_for_color!(LightMagenta, LightMagenta);
|
||||
from_termion_for_color!(LightCyan, LightCyan);
|
||||
from_termion_for_color!(LightWhite, White);
|
||||
|
||||
impl From<tcolor::AnsiValue> for Color {
|
||||
fn from(value: tcolor::AnsiValue) -> Self {
|
||||
Color::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))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tcolor::Fg<tcolor::AnsiValue>> for Style {
|
||||
fn from(value: tcolor::Fg<tcolor::AnsiValue>) -> Self {
|
||||
Style::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)
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ModifierDiff {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let remove = self.from - self.to;
|
||||
@@ -338,3 +417,147 @@ impl fmt::Display for ModifierDiff {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! from_termion_for_modifier {
|
||||
($termion_modifier:ident, $modifier: ident) => {
|
||||
impl From<tstyle::$termion_modifier> for Modifier {
|
||||
fn from(_: tstyle::$termion_modifier) -> Self {
|
||||
Modifier::$modifier
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
from_termion_for_modifier!(Invert, REVERSED);
|
||||
from_termion_for_modifier!(Bold, BOLD);
|
||||
from_termion_for_modifier!(Italic, ITALIC);
|
||||
from_termion_for_modifier!(Underline, UNDERLINED);
|
||||
from_termion_for_modifier!(Faint, DIM);
|
||||
from_termion_for_modifier!(CrossedOut, CROSSED_OUT);
|
||||
from_termion_for_modifier!(Blink, SLOW_BLINK);
|
||||
|
||||
impl From<termion::style::Reset> for Modifier {
|
||||
fn from(_: termion::style::Reset) -> Self {
|
||||
Modifier::empty()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::style::Stylize;
|
||||
|
||||
#[test]
|
||||
fn from_termion_color() {
|
||||
assert_eq!(Color::from(tcolor::Reset), Color::Reset);
|
||||
assert_eq!(Color::from(tcolor::Black), Color::Black);
|
||||
assert_eq!(Color::from(tcolor::Red), Color::Red);
|
||||
assert_eq!(Color::from(tcolor::Green), Color::Green);
|
||||
assert_eq!(Color::from(tcolor::Yellow), Color::Yellow);
|
||||
assert_eq!(Color::from(tcolor::Blue), Color::Blue);
|
||||
assert_eq!(Color::from(tcolor::Magenta), Color::Magenta);
|
||||
assert_eq!(Color::from(tcolor::Cyan), Color::Cyan);
|
||||
assert_eq!(Color::from(tcolor::White), Color::Gray);
|
||||
assert_eq!(Color::from(tcolor::LightBlack), Color::DarkGray);
|
||||
assert_eq!(Color::from(tcolor::LightRed), Color::LightRed);
|
||||
assert_eq!(Color::from(tcolor::LightGreen), Color::LightGreen);
|
||||
assert_eq!(Color::from(tcolor::LightBlue), Color::LightBlue);
|
||||
assert_eq!(Color::from(tcolor::LightYellow), Color::LightYellow);
|
||||
assert_eq!(Color::from(tcolor::LightMagenta), Color::LightMagenta);
|
||||
assert_eq!(Color::from(tcolor::LightCyan), Color::LightCyan);
|
||||
assert_eq!(Color::from(tcolor::LightWhite), Color::White);
|
||||
assert_eq!(Color::from(tcolor::AnsiValue(31)), Color::Indexed(31));
|
||||
assert_eq!(Color::from(tcolor::Rgb(1, 2, 3)), Color::Rgb(1, 2, 3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_termion_bg() {
|
||||
use tc::Bg;
|
||||
use tcolor as tc;
|
||||
|
||||
assert_eq!(Style::from(Bg(tc::Reset)), Style::new().bg(Color::Reset));
|
||||
assert_eq!(Style::from(Bg(tc::Black)), Style::new().on_black());
|
||||
assert_eq!(Style::from(Bg(tc::Red)), Style::new().on_red());
|
||||
assert_eq!(Style::from(Bg(tc::Green)), Style::new().on_green());
|
||||
assert_eq!(Style::from(Bg(tc::Yellow)), Style::new().on_yellow());
|
||||
assert_eq!(Style::from(Bg(tc::Blue)), Style::new().on_blue());
|
||||
assert_eq!(Style::from(Bg(tc::Magenta)), Style::new().on_magenta());
|
||||
assert_eq!(Style::from(Bg(tc::Cyan)), Style::new().on_cyan());
|
||||
assert_eq!(Style::from(Bg(tc::White)), Style::new().on_gray());
|
||||
assert_eq!(Style::from(Bg(tc::LightBlack)), Style::new().on_dark_gray());
|
||||
assert_eq!(Style::from(Bg(tc::LightRed)), Style::new().on_light_red());
|
||||
assert_eq!(
|
||||
Style::from(Bg(tc::LightGreen)),
|
||||
Style::new().on_light_green()
|
||||
);
|
||||
assert_eq!(Style::from(Bg(tc::LightBlue)), Style::new().on_light_blue());
|
||||
assert_eq!(
|
||||
Style::from(Bg(tc::LightYellow)),
|
||||
Style::new().on_light_yellow()
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from(Bg(tc::LightMagenta)),
|
||||
Style::new().on_light_magenta()
|
||||
);
|
||||
assert_eq!(Style::from(Bg(tc::LightCyan)), Style::new().on_light_cyan());
|
||||
assert_eq!(Style::from(Bg(tc::LightWhite)), Style::new().on_white());
|
||||
assert_eq!(
|
||||
Style::from(Bg(tc::AnsiValue(31))),
|
||||
Style::new().bg(Color::Indexed(31))
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from(Bg(tc::Rgb(1, 2, 3))),
|
||||
Style::new().bg(Color::Rgb(1, 2, 3))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_termion_fg() {
|
||||
use tc::Fg;
|
||||
use tcolor as tc;
|
||||
|
||||
assert_eq!(Style::from(Fg(tc::Reset)), Style::new().fg(Color::Reset));
|
||||
assert_eq!(Style::from(Fg(tc::Black)), Style::new().black());
|
||||
assert_eq!(Style::from(Fg(tc::Red)), Style::new().red());
|
||||
assert_eq!(Style::from(Fg(tc::Green)), Style::new().green());
|
||||
assert_eq!(Style::from(Fg(tc::Yellow)), Style::new().yellow());
|
||||
assert_eq!(Style::from(Fg(tc::Blue)), Style::default().blue());
|
||||
assert_eq!(Style::from(Fg(tc::Magenta)), Style::default().magenta());
|
||||
assert_eq!(Style::from(Fg(tc::Cyan)), Style::default().cyan());
|
||||
assert_eq!(Style::from(Fg(tc::White)), Style::default().gray());
|
||||
assert_eq!(Style::from(Fg(tc::LightBlack)), Style::new().dark_gray());
|
||||
assert_eq!(Style::from(Fg(tc::LightRed)), Style::new().light_red());
|
||||
assert_eq!(Style::from(Fg(tc::LightGreen)), Style::new().light_green());
|
||||
assert_eq!(Style::from(Fg(tc::LightBlue)), Style::new().light_blue());
|
||||
assert_eq!(
|
||||
Style::from(Fg(tc::LightYellow)),
|
||||
Style::new().light_yellow()
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from(Fg(tc::LightMagenta)),
|
||||
Style::new().light_magenta()
|
||||
);
|
||||
assert_eq!(Style::from(Fg(tc::LightCyan)), Style::new().light_cyan());
|
||||
assert_eq!(Style::from(Fg(tc::LightWhite)), Style::new().white());
|
||||
assert_eq!(
|
||||
Style::from(Fg(tc::AnsiValue(31))),
|
||||
Style::default().fg(Color::Indexed(31))
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from(Fg(tc::Rgb(1, 2, 3))),
|
||||
Style::default().fg(Color::Rgb(1, 2, 3))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_termion_style() {
|
||||
assert_eq!(Modifier::from(tstyle::Invert), Modifier::REVERSED);
|
||||
assert_eq!(Modifier::from(tstyle::Bold), Modifier::BOLD);
|
||||
assert_eq!(Modifier::from(tstyle::Italic), Modifier::ITALIC);
|
||||
assert_eq!(Modifier::from(tstyle::Underline), Modifier::UNDERLINED);
|
||||
assert_eq!(Modifier::from(tstyle::Faint), Modifier::DIM);
|
||||
assert_eq!(Modifier::from(tstyle::CrossedOut), Modifier::CROSSED_OUT);
|
||||
assert_eq!(Modifier::from(tstyle::Blink), Modifier::SLOW_BLINK);
|
||||
assert_eq!(Modifier::from(tstyle::Reset), Modifier::empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ use std::{error::Error, io};
|
||||
|
||||
use termwiz::{
|
||||
caps::Capabilities,
|
||||
cell::{AttributeChange, Blink, Intensity, Underline},
|
||||
color::{AnsiColor, ColorAttribute, SrgbaTuple},
|
||||
cell::{AttributeChange, Blink, CellAttributes, Intensity, Underline},
|
||||
color::{AnsiColor, ColorAttribute, ColorSpec, LinearRgba, RgbColor, SrgbaTuple},
|
||||
surface::{Change, CursorVisibility, Position},
|
||||
terminal::{buffered::BufferedTerminal, ScreenSize, SystemTerminal, Terminal},
|
||||
};
|
||||
@@ -20,7 +20,7 @@ use crate::{
|
||||
buffer::Cell,
|
||||
layout::Size,
|
||||
prelude::Rect,
|
||||
style::{Color, Modifier},
|
||||
style::{Color, Modifier, Style},
|
||||
};
|
||||
|
||||
/// A [`Backend`] implementation that uses [Termwiz] to render to the terminal.
|
||||
@@ -52,14 +52,14 @@ use crate::{
|
||||
/// # std::result::Result::Ok::<(), Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// [`backend`]: crate::backend
|
||||
/// [`Terminal`]: crate::terminal::Terminal
|
||||
/// [`BufferedTerminal`]: termwiz::terminal::buffered::BufferedTerminal
|
||||
/// [Termwiz]: https://crates.io/crates/termwiz
|
||||
/// [examples]: https://github.com/ratatui-org/ratatui/tree/main/examples#readme
|
||||
/// [Examples]: https://github.com/ratatui-org/ratatui/tree/main/examples/README.md
|
||||
pub struct TermwizBackend {
|
||||
buffered_terminal: BufferedTerminal<SystemTerminal>,
|
||||
}
|
||||
@@ -249,12 +249,73 @@ impl Backend for TermwizBackend {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CellAttributes> for Style {
|
||||
fn from(value: CellAttributes) -> Self {
|
||||
let mut style = Style::new()
|
||||
.add_modifier(value.intensity().into())
|
||||
.add_modifier(value.underline().into())
|
||||
.add_modifier(value.blink().into());
|
||||
|
||||
if value.italic() {
|
||||
style.add_modifier |= Modifier::ITALIC;
|
||||
}
|
||||
if value.reverse() {
|
||||
style.add_modifier |= Modifier::REVERSED;
|
||||
}
|
||||
if value.strikethrough() {
|
||||
style.add_modifier |= Modifier::CROSSED_OUT;
|
||||
}
|
||||
if value.invisible() {
|
||||
style.add_modifier |= Modifier::HIDDEN;
|
||||
}
|
||||
|
||||
style.fg = Some(value.foreground().into());
|
||||
style.bg = Some(value.background().into());
|
||||
#[cfg(feature = "underline_color")]
|
||||
{
|
||||
style.underline_color = Some(value.underline_color().into());
|
||||
}
|
||||
|
||||
style
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Intensity> for Modifier {
|
||||
fn from(value: Intensity) -> Self {
|
||||
match value {
|
||||
Intensity::Normal => Modifier::empty(),
|
||||
Intensity::Bold => Modifier::BOLD,
|
||||
Intensity::Half => Modifier::DIM,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Underline> for Modifier {
|
||||
fn from(value: Underline) -> Self {
|
||||
match value {
|
||||
Underline::None => Modifier::empty(),
|
||||
_ => Modifier::UNDERLINED,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Blink> for Modifier {
|
||||
fn from(value: Blink) -> Self {
|
||||
match value {
|
||||
Blink::None => Modifier::empty(),
|
||||
Blink::Slow => Modifier::SLOW_BLINK,
|
||||
Blink::Rapid => Modifier::RAPID_BLINK,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color> for ColorAttribute {
|
||||
fn from(color: Color) -> ColorAttribute {
|
||||
match color {
|
||||
Color::Reset => ColorAttribute::Default,
|
||||
Color::Black => AnsiColor::Black.into(),
|
||||
Color::Gray | Color::DarkGray => AnsiColor::Grey.into(),
|
||||
Color::DarkGray => AnsiColor::Grey.into(),
|
||||
Color::Gray => AnsiColor::Silver.into(),
|
||||
Color::Red => AnsiColor::Maroon.into(),
|
||||
Color::LightRed => AnsiColor::Red.into(),
|
||||
Color::Green => AnsiColor::Green.into(),
|
||||
@@ -276,7 +337,326 @@ impl From<Color> for ColorAttribute {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AnsiColor> for Color {
|
||||
fn from(value: AnsiColor) -> Self {
|
||||
match value {
|
||||
AnsiColor::Black => Color::Black,
|
||||
AnsiColor::Grey => Color::DarkGray,
|
||||
AnsiColor::Silver => Color::Gray,
|
||||
AnsiColor::Maroon => Color::Red,
|
||||
AnsiColor::Red => Color::LightRed,
|
||||
AnsiColor::Green => Color::Green,
|
||||
AnsiColor::Lime => Color::LightGreen,
|
||||
AnsiColor::Olive => Color::Yellow,
|
||||
AnsiColor::Yellow => Color::LightYellow,
|
||||
AnsiColor::Purple => Color::Magenta,
|
||||
AnsiColor::Fuchsia => Color::LightMagenta,
|
||||
AnsiColor::Teal => Color::Cyan,
|
||||
AnsiColor::Aqua => Color::LightCyan,
|
||||
AnsiColor::White => Color::White,
|
||||
AnsiColor::Navy => Color::Blue,
|
||||
AnsiColor::Blue => Color::LightBlue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ColorAttribute> for Color {
|
||||
fn from(value: ColorAttribute) -> Self {
|
||||
match value {
|
||||
ColorAttribute::TrueColorWithDefaultFallback(srgba)
|
||||
| ColorAttribute::TrueColorWithPaletteFallback(srgba, _) => srgba.into(),
|
||||
ColorAttribute::PaletteIndex(i) => Color::Indexed(i),
|
||||
ColorAttribute::Default => Color::Reset,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ColorSpec> for Color {
|
||||
fn from(value: ColorSpec) -> Self {
|
||||
match value {
|
||||
ColorSpec::Default => Color::Reset,
|
||||
ColorSpec::PaletteIndex(i) => Color::Indexed(i),
|
||||
ColorSpec::TrueColor(srgba) => srgba.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SrgbaTuple> for Color {
|
||||
fn from(value: SrgbaTuple) -> Self {
|
||||
let (r, g, b, _) = value.to_srgb_u8();
|
||||
Color::Rgb(r, g, b)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RgbColor> for Color {
|
||||
fn from(value: RgbColor) -> Self {
|
||||
let (r, g, b) = value.to_tuple_rgb8();
|
||||
Color::Rgb(r, g, b)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LinearRgba> for Color {
|
||||
fn from(value: LinearRgba) -> Self {
|
||||
value.to_srgb().into()
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn u16_max(i: usize) -> u16 {
|
||||
u16::try_from(i).unwrap_or(u16::MAX)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::style::Stylize;
|
||||
|
||||
mod into_color {
|
||||
use Color as C;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn from_linear_rgba() {
|
||||
// full black + opaque
|
||||
assert_eq!(C::from(LinearRgba(0., 0., 0., 1.)), Color::Rgb(0, 0, 0));
|
||||
// full black + transparent
|
||||
assert_eq!(C::from(LinearRgba(0., 0., 0., 0.)), Color::Rgb(0, 0, 0));
|
||||
|
||||
// full white + opaque
|
||||
assert_eq!(C::from(LinearRgba(1., 1., 1., 1.)), C::Rgb(254, 254, 254));
|
||||
// full white + transparent
|
||||
assert_eq!(C::from(LinearRgba(1., 1., 1., 0.)), C::Rgb(254, 254, 254));
|
||||
|
||||
// full red
|
||||
assert_eq!(C::from(LinearRgba(1., 0., 0., 1.)), C::Rgb(254, 0, 0));
|
||||
// full green
|
||||
assert_eq!(C::from(LinearRgba(0., 1., 0., 1.)), C::Rgb(0, 254, 0));
|
||||
// full blue
|
||||
assert_eq!(C::from(LinearRgba(0., 0., 1., 1.)), C::Rgb(0, 0, 254));
|
||||
|
||||
// See https://stackoverflow.com/questions/12524623/what-are-the-practical-differences-when-working-with-colors-in-a-linear-vs-a-no
|
||||
// for an explanation
|
||||
|
||||
// half red
|
||||
assert_eq!(C::from(LinearRgba(0.214, 0., 0., 1.)), C::Rgb(127, 0, 0));
|
||||
// half green
|
||||
assert_eq!(C::from(LinearRgba(0., 0.214, 0., 1.)), C::Rgb(0, 127, 0));
|
||||
// half blue
|
||||
assert_eq!(C::from(LinearRgba(0., 0., 0.214, 1.)), C::Rgb(0, 0, 127));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_srgba() {
|
||||
// full black + opaque
|
||||
assert_eq!(C::from(SrgbaTuple(0., 0., 0., 1.)), Color::Rgb(0, 0, 0));
|
||||
// full black + transparent
|
||||
assert_eq!(C::from(SrgbaTuple(0., 0., 0., 0.)), Color::Rgb(0, 0, 0));
|
||||
|
||||
// full white + opaque
|
||||
assert_eq!(C::from(SrgbaTuple(1., 1., 1., 1.)), C::Rgb(255, 255, 255));
|
||||
// full white + transparent
|
||||
assert_eq!(C::from(SrgbaTuple(1., 1., 1., 0.)), C::Rgb(255, 255, 255));
|
||||
|
||||
// full red
|
||||
assert_eq!(C::from(SrgbaTuple(1., 0., 0., 1.)), C::Rgb(255, 0, 0));
|
||||
// full green
|
||||
assert_eq!(C::from(SrgbaTuple(0., 1., 0., 1.)), C::Rgb(0, 255, 0));
|
||||
// full blue
|
||||
assert_eq!(C::from(SrgbaTuple(0., 0., 1., 1.)), C::Rgb(0, 0, 255));
|
||||
|
||||
// half red
|
||||
assert_eq!(C::from(SrgbaTuple(0.5, 0., 0., 1.)), C::Rgb(127, 0, 0));
|
||||
// half green
|
||||
assert_eq!(C::from(SrgbaTuple(0., 0.5, 0., 1.)), C::Rgb(0, 127, 0));
|
||||
// half blue
|
||||
assert_eq!(C::from(SrgbaTuple(0., 0., 0.5, 1.)), C::Rgb(0, 0, 127));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_rgbcolor() {
|
||||
// full black
|
||||
assert_eq!(C::from(RgbColor::new_8bpc(0, 0, 0)), Color::Rgb(0, 0, 0));
|
||||
// full white
|
||||
assert_eq!(
|
||||
C::from(RgbColor::new_8bpc(255, 255, 255)),
|
||||
C::Rgb(255, 255, 255)
|
||||
);
|
||||
|
||||
// full red
|
||||
assert_eq!(C::from(RgbColor::new_8bpc(255, 0, 0)), C::Rgb(255, 0, 0));
|
||||
// full green
|
||||
assert_eq!(C::from(RgbColor::new_8bpc(0, 255, 0)), C::Rgb(0, 255, 0));
|
||||
// full blue
|
||||
assert_eq!(C::from(RgbColor::new_8bpc(0, 0, 255)), C::Rgb(0, 0, 255));
|
||||
|
||||
// half red
|
||||
assert_eq!(C::from(RgbColor::new_8bpc(127, 0, 0)), C::Rgb(127, 0, 0));
|
||||
// half green
|
||||
assert_eq!(C::from(RgbColor::new_8bpc(0, 127, 0)), C::Rgb(0, 127, 0));
|
||||
// half blue
|
||||
assert_eq!(C::from(RgbColor::new_8bpc(0, 0, 127)), C::Rgb(0, 0, 127));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_colorspec() {
|
||||
assert_eq!(C::from(ColorSpec::Default), C::Reset);
|
||||
assert_eq!(C::from(ColorSpec::PaletteIndex(33)), C::Indexed(33));
|
||||
assert_eq!(
|
||||
C::from(ColorSpec::TrueColor(SrgbaTuple(0., 0., 0., 1.))),
|
||||
C::Rgb(0, 0, 0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_colorattribute() {
|
||||
assert_eq!(C::from(ColorAttribute::Default), C::Reset);
|
||||
assert_eq!(C::from(ColorAttribute::PaletteIndex(32)), C::Indexed(32));
|
||||
assert_eq!(
|
||||
C::from(ColorAttribute::TrueColorWithDefaultFallback(SrgbaTuple(
|
||||
0., 0., 0., 1.
|
||||
))),
|
||||
C::Rgb(0, 0, 0)
|
||||
);
|
||||
assert_eq!(
|
||||
C::from(ColorAttribute::TrueColorWithPaletteFallback(
|
||||
SrgbaTuple(0., 0., 0., 1.),
|
||||
31
|
||||
)),
|
||||
C::Rgb(0, 0, 0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_ansicolor() {
|
||||
assert_eq!(C::from(AnsiColor::Black), Color::Black);
|
||||
assert_eq!(C::from(AnsiColor::Grey), Color::DarkGray);
|
||||
assert_eq!(C::from(AnsiColor::Silver), Color::Gray);
|
||||
assert_eq!(C::from(AnsiColor::Maroon), Color::Red);
|
||||
assert_eq!(C::from(AnsiColor::Red), Color::LightRed);
|
||||
assert_eq!(C::from(AnsiColor::Green), Color::Green);
|
||||
assert_eq!(C::from(AnsiColor::Lime), Color::LightGreen);
|
||||
assert_eq!(C::from(AnsiColor::Olive), Color::Yellow);
|
||||
assert_eq!(C::from(AnsiColor::Yellow), Color::LightYellow);
|
||||
assert_eq!(C::from(AnsiColor::Purple), Color::Magenta);
|
||||
assert_eq!(C::from(AnsiColor::Fuchsia), Color::LightMagenta);
|
||||
assert_eq!(C::from(AnsiColor::Teal), Color::Cyan);
|
||||
assert_eq!(C::from(AnsiColor::Aqua), Color::LightCyan);
|
||||
assert_eq!(C::from(AnsiColor::White), Color::White);
|
||||
assert_eq!(C::from(AnsiColor::Navy), Color::Blue);
|
||||
assert_eq!(C::from(AnsiColor::Blue), Color::LightBlue);
|
||||
}
|
||||
}
|
||||
|
||||
mod into_modifier {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn from_intensity() {
|
||||
assert_eq!(Modifier::from(Intensity::Normal), Modifier::empty());
|
||||
assert_eq!(Modifier::from(Intensity::Bold), Modifier::BOLD);
|
||||
assert_eq!(Modifier::from(Intensity::Half), Modifier::DIM);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_underline() {
|
||||
assert_eq!(Modifier::from(Underline::None), Modifier::empty());
|
||||
assert_eq!(Modifier::from(Underline::Single), Modifier::UNDERLINED);
|
||||
assert_eq!(Modifier::from(Underline::Double), Modifier::UNDERLINED);
|
||||
assert_eq!(Modifier::from(Underline::Curly), Modifier::UNDERLINED);
|
||||
assert_eq!(Modifier::from(Underline::Dashed), Modifier::UNDERLINED);
|
||||
assert_eq!(Modifier::from(Underline::Dotted), Modifier::UNDERLINED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_blink() {
|
||||
assert_eq!(Modifier::from(Blink::None), Modifier::empty());
|
||||
assert_eq!(Modifier::from(Blink::Slow), Modifier::SLOW_BLINK);
|
||||
assert_eq!(Modifier::from(Blink::Rapid), Modifier::RAPID_BLINK);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_cell_attribute_for_style() {
|
||||
// default
|
||||
assert_eq!(
|
||||
Style::from(CellAttributes::default()),
|
||||
Style::new().fg(Color::Reset).bg(Color::Reset)
|
||||
);
|
||||
// foreground color
|
||||
assert_eq!(
|
||||
Style::from(
|
||||
CellAttributes::default()
|
||||
.set_foreground(ColorAttribute::PaletteIndex(31))
|
||||
.to_owned()
|
||||
),
|
||||
Style::new().fg(Color::Indexed(31)).bg(Color::Reset)
|
||||
);
|
||||
// background color
|
||||
assert_eq!(
|
||||
Style::from(
|
||||
CellAttributes::default()
|
||||
.set_background(ColorAttribute::PaletteIndex(31))
|
||||
.to_owned()
|
||||
),
|
||||
Style::new().fg(Color::Reset).bg(Color::Indexed(31))
|
||||
);
|
||||
// underline color
|
||||
#[cfg(feature = "underline_color")]
|
||||
assert_eq!(
|
||||
Style::from(
|
||||
CellAttributes::default()
|
||||
.set_underline_color(AnsiColor::Red)
|
||||
.set
|
||||
.to_owned()
|
||||
),
|
||||
Style::new()
|
||||
.fg(Color::Reset)
|
||||
.bg(Color::Reset)
|
||||
.underline_color(Color::Red)
|
||||
);
|
||||
// underlined
|
||||
assert_eq!(
|
||||
Style::from(
|
||||
CellAttributes::default()
|
||||
.set_underline(Underline::Single)
|
||||
.to_owned()
|
||||
),
|
||||
Style::new().fg(Color::Reset).bg(Color::Reset).underlined()
|
||||
);
|
||||
// blink
|
||||
assert_eq!(
|
||||
Style::from(CellAttributes::default().set_blink(Blink::Slow).to_owned()),
|
||||
Style::new().fg(Color::Reset).bg(Color::Reset).slow_blink()
|
||||
);
|
||||
// intensity
|
||||
assert_eq!(
|
||||
Style::from(
|
||||
CellAttributes::default()
|
||||
.set_intensity(Intensity::Bold)
|
||||
.to_owned()
|
||||
),
|
||||
Style::new().fg(Color::Reset).bg(Color::Reset).bold()
|
||||
);
|
||||
// italic
|
||||
assert_eq!(
|
||||
Style::from(CellAttributes::default().set_italic(true).to_owned()),
|
||||
Style::new().fg(Color::Reset).bg(Color::Reset).italic()
|
||||
);
|
||||
// reversed
|
||||
assert_eq!(
|
||||
Style::from(CellAttributes::default().set_reverse(true).to_owned()),
|
||||
Style::new().fg(Color::Reset).bg(Color::Reset).reversed()
|
||||
);
|
||||
// strikethrough
|
||||
assert_eq!(
|
||||
Style::from(CellAttributes::default().set_strikethrough(true).to_owned()),
|
||||
Style::new().fg(Color::Reset).bg(Color::Reset).crossed_out()
|
||||
);
|
||||
// hidden
|
||||
assert_eq!(
|
||||
Style::from(CellAttributes::default().set_invisible(true).to_owned()),
|
||||
Style::new().fg(Color::Reset).bg(Color::Reset).hidden()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
1071
src/buffer.rs
1071
src/buffer.rs
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user