Compare commits
154 Commits
v0.24.1-al
...
kd/backpor
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13de3daf73 | ||
|
|
61e3ab5f99 | ||
|
|
942aad5796 | ||
|
|
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 | ||
|
|
1f88da7538 | ||
|
|
36d8c53645 | ||
|
|
ec7b3872b4 | ||
|
|
edacaf7ff4 | ||
|
|
df0eb1f8e9 | ||
|
|
59b9c32fbc |
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
|
||||
|
||||
26
.github/workflows/check-pr.yml
vendored
26
.github/workflows/check-pr.yml
vendored
@@ -44,22 +44,44 @@ jobs:
|
||||
header: pr-title-lint-error
|
||||
delete: true
|
||||
|
||||
check-signed:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
# Check commit signature and add comment if needed
|
||||
- name: Check signed commits in PR
|
||||
uses: 1Password/check-signed-commits-action@v1
|
||||
with:
|
||||
comment: |
|
||||
Thank you for opening this pull request!
|
||||
|
||||
We require commits to be signed and it looks like this PR contains unsigned commits.
|
||||
|
||||
Get help in the [CONTRIBUTING.md](https://github.com/ratatui-org/ratatui/blob/main/CONTRIBUTING.md#sign-your-commits)
|
||||
or on [Github doc](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits).
|
||||
|
||||
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: |
|
||||
|
||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -151,6 +151,8 @@ 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: 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,6 +10,20 @@ github with a [breaking change] label.
|
||||
|
||||
This is a quick summary of the sections below:
|
||||
|
||||
- [v0.26.0 (unreleased)](#v0260-unreleased)
|
||||
- `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`
|
||||
@@ -32,6 +46,217 @@ This is a quick summary of the sections below:
|
||||
- MSRV is now 1.63.0
|
||||
- `List` no longer ignores empty strings
|
||||
|
||||
## v0.26.0 (unreleased)
|
||||
|
||||
### `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::style` 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
|
||||
the `&`.
|
||||
|
||||
E.g.
|
||||
|
||||
```diff
|
||||
- let table = Table::new(rows).widths(&[Constraint::Length(1)]);
|
||||
// becomes
|
||||
+ let table = Table::new(rows, [Constraint::Length(1)]);
|
||||
```
|
||||
|
||||
### 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)
|
||||
|
||||
### ScrollbarState field type changed from `u16` to `usize` ([#456])
|
||||
@@ -49,10 +274,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])
|
||||
@@ -63,10 +288,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])
|
||||
@@ -78,13 +303,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])
|
||||
@@ -94,12 +319,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)
|
||||
@@ -110,10 +335,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])
|
||||
@@ -124,10 +349,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])
|
||||
@@ -148,7 +373,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
|
||||
|
||||
@@ -159,15 +384,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),
|
||||
});
|
||||
```
|
||||
|
||||
@@ -176,13 +401,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])
|
||||
@@ -193,10 +418,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)
|
||||
|
||||
487
CHANGELOG.md
487
CHANGELOG.md
@@ -2,6 +2,491 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [0.25.0](https://github.com/ratatui-org/ratatui/releases/tag/0.25.0) - 2023-12-18
|
||||
|
||||
We are thrilled to announce the new version of `ratatui` - a Rust library that's all about cooking up TUIs 🐭
|
||||
|
||||
In this version, we made improvements on widgets such as List, Table and Layout and changed some of the defaults for a better user experience.
|
||||
Also, we renewed our website and updated our documentation/tutorials to get started with `ratatui`: <https://ratatui.rs> 🚀
|
||||
|
||||
✨ **Release highlights**: <https://ratatui.rs/highlights/v025/>
|
||||
|
||||
⚠️ List of breaking changes can be found [here](https://github.com/ratatui-org/ratatui/blob/main/BREAKING-CHANGES.md).
|
||||
|
||||
💖 We also enabled GitHub Sponsors for our organization, consider sponsoring us if you like `ratatui`: <https://github.com/sponsors/ratatui-org>
|
||||
|
||||
### Features
|
||||
|
||||
- [aef4956](https://github.com/ratatui-org/ratatui/commit/aef495604c52e563fbacfb1a6e730cd441a99129)
|
||||
*(list)* `List::new` now accepts `IntoIterator<Item = Into<ListItem>>` ([#672](https://github.com/ratatui-org/ratatui/issues/672)) [**breaking**]
|
||||
|
||||
````text
|
||||
This allows to build list like
|
||||
|
||||
```
|
||||
List::new(["Item 1", "Item 2"])
|
||||
```
|
||||
````
|
||||
|
||||
- [8bfd666](https://github.com/ratatui-org/ratatui/commit/8bfd6661e251b6943f74bda626e4708b2e9f4b51)
|
||||
*(paragraph)* Add `line_count` and `line_width` unstable helper methods
|
||||
|
||||
````text
|
||||
This is an unstable feature that may be removed in the future
|
||||
````
|
||||
|
||||
- [1229b96](https://github.com/ratatui-org/ratatui/commit/1229b96e428df880a951ef57f53ca73e74ef1ea2)
|
||||
*(rect)* Add `offset` method ([#533](https://github.com/ratatui-org/ratatui/issues/533))
|
||||
|
||||
````text
|
||||
The offset method creates a new Rect that is moved by the amount
|
||||
specified in the x and y direction. These values can be positive or
|
||||
negative. This is useful for manual layout tasks.
|
||||
|
||||
```rust
|
||||
let rect = area.offset(Offset { x: 10, y -10 });
|
||||
```
|
||||
````
|
||||
|
||||
- [edacaf7](https://github.com/ratatui-org/ratatui/commit/edacaf7ff4e4b14702f6361af5a6da713b7dc564)
|
||||
*(buffer)* Deprecate `Cell::symbol` field ([#624](https://github.com/ratatui-org/ratatui/issues/624))
|
||||
|
||||
````text
|
||||
The Cell::symbol field is now accessible via a getter method (`symbol()`). This will
|
||||
allow us to make future changes to the Cell internals such as replacing `String` with
|
||||
`compact_str`.
|
||||
````
|
||||
|
||||
- [6b2efd0](https://github.com/ratatui-org/ratatui/commit/6b2efd0f6c3bf56dc06bbf042db40c0c66de577e)
|
||||
*(layout)* Accept IntoIterator for constraints ([#663](https://github.com/ratatui-org/ratatui/issues/663))
|
||||
|
||||
````text
|
||||
Layout and Table now accept IntoIterator for constraints with an Item
|
||||
that is AsRef<Constraint>. This allows pretty much any collection of
|
||||
constraints to be passed to the layout functions including arrays,
|
||||
vectors, slices, and iterators (without having to call collect() on
|
||||
them).
|
||||
````
|
||||
|
||||
- [753e246](https://github.com/ratatui-org/ratatui/commit/753e246531e1e9e2ea558911f8d03e738901d85f)
|
||||
*(layout)* Allow configuring layout fill ([#633](https://github.com/ratatui-org/ratatui/issues/633))
|
||||
|
||||
````text
|
||||
The layout split will generally fill the remaining area when `split()`
|
||||
is called. This change allows the caller to configure how any extra
|
||||
space is allocated to the `Rect`s. This is useful for cases where the
|
||||
caller wants to have a fixed size for one of the `Rect`s, and have the
|
||||
other `Rect`s fill the remaining space.
|
||||
|
||||
For now, the method and enum are marked as unstable because the exact
|
||||
name is still being bikeshedded. To enable this functionality, add the
|
||||
`unstable-segment-size` feature flag in your `Cargo.toml`.
|
||||
|
||||
To configure the layout to fill the remaining space evenly, use
|
||||
`Layout::segment_size(SegmentSize::EvenDistribution)`. The default
|
||||
behavior is `SegmentSize::LastTakesRemainder`, which gives the last
|
||||
segment the remaining space. `SegmentSize::None` will disable this
|
||||
behavior. See the docs for `Layout::segment_size()` and
|
||||
`layout::SegmentSize` for more information.
|
||||
|
||||
Fixes https://github.com/ratatui-org/ratatui/issues/536
|
||||
````
|
||||
|
||||
- [1e2f0be](https://github.com/ratatui-org/ratatui/commit/1e2f0be75ac3fb3d6500c1de291bd49972b808e4)
|
||||
*(layout)* Add parameters to Layout::new() ([#557](https://github.com/ratatui-org/ratatui/issues/557)) [**breaking**]
|
||||
|
||||
````text
|
||||
Adds a convenience function to create a layout with a direction and a
|
||||
list of constraints which are the most common parameters that would be
|
||||
generally configured using the builder pattern. The constraints can be
|
||||
passed in as any iterator of constraints.
|
||||
|
||||
```rust
|
||||
let layout = Layout::new(Direction::Horizontal, [
|
||||
Constraint::Percentage(50),
|
||||
Constraint::Percentage(50),
|
||||
]);
|
||||
```
|
||||
````
|
||||
|
||||
- [c862aa5](https://github.com/ratatui-org/ratatui/commit/c862aa5e9ef4dbf494b5151214ac87f5c71e76d4)
|
||||
*(list)* Support line alignment ([#599](https://github.com/ratatui-org/ratatui/issues/599))
|
||||
|
||||
````text
|
||||
The `List` widget now respects the alignment of `Line`s and renders them as expected.
|
||||
````
|
||||
|
||||
- [4424637](https://github.com/ratatui-org/ratatui/commit/4424637af252dc2f227fe4956eac71135e60fb02)
|
||||
*(span)* Add setters for content and style ([#647](https://github.com/ratatui-org/ratatui/issues/647))
|
||||
|
||||
- [ebf1f42](https://github.com/ratatui-org/ratatui/commit/ebf1f4294211d478b8633a06576ec269a50db588)
|
||||
*(style)* Implement `From` trait for crossterm to `Style` related structs ([#686](https://github.com/ratatui-org/ratatui/issues/686))
|
||||
|
||||
- [e49385b](https://github.com/ratatui-org/ratatui/commit/e49385b78c8e01fe6381b19d15137346bc6eb8a1)
|
||||
*(table)* Add a Table::segment_size method ([#660](https://github.com/ratatui-org/ratatui/issues/660))
|
||||
|
||||
````text
|
||||
It controls how to distribute extra space to an underconstrained table.
|
||||
The default, legacy behavior is to leave the extra space unused. The
|
||||
new options are LastTakesRemainder which gets all space to the rightmost
|
||||
column that can used it, and EvenDistribution which divides it amongst
|
||||
all columns.
|
||||
````
|
||||
|
||||
- [b8f71c0](https://github.com/ratatui-org/ratatui/commit/b8f71c0d6eda3da272d29c7a9b3c47181049f76a)
|
||||
*(widgets/chart)* Add option to set the position of legend ([#378](https://github.com/ratatui-org/ratatui/issues/378))
|
||||
|
||||
- [5bf4f52](https://github.com/ratatui-org/ratatui/commit/5bf4f52119ab3e0e3a266af196058179dc1d18c3)
|
||||
*(uncategorized)* Implement `From` trait for termion to `Style` related structs ([#692](https://github.com/ratatui-org/ratatui/issues/692))
|
||||
|
||||
````text
|
||||
* feat(termion): implement from termion color
|
||||
|
||||
* feat(termion): implement from termion style
|
||||
|
||||
* feat(termion): implement from termion `Bg` and `Fg`
|
||||
````
|
||||
|
||||
- [d19b266](https://github.com/ratatui-org/ratatui/commit/d19b266e0eabdb0fb00660439a1818239c94024b)
|
||||
*(uncategorized)* Add Constraint helpers (e.g. from_lengths) ([#641](https://github.com/ratatui-org/ratatui/issues/641))
|
||||
|
||||
````text
|
||||
Adds helper methods that convert from iterators of u16 values to the
|
||||
specific Constraint type. This makes it easy to create constraints like:
|
||||
|
||||
```rust
|
||||
// a fixed layout
|
||||
let constraints = Constraint::from_lengths([10, 20, 10]);
|
||||
|
||||
// a centered layout
|
||||
let constraints = Constraint::from_ratios([(1, 4), (1, 2), (1, 4)]);
|
||||
let constraints = Constraint::from_percentages([25, 50, 25]);
|
||||
|
||||
// a centered layout with a minimum size
|
||||
let constraints = Constraint::from_mins([0, 100, 0]);
|
||||
|
||||
// a sidebar / main layout with maximum sizes
|
||||
let constraints = Constraint::from_maxes([30, 200]);
|
||||
```
|
||||
````
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- [f69d57c](https://github.com/ratatui-org/ratatui/commit/f69d57c3b59e27b517a5ca1a002af808fee47970)
|
||||
*(rect)* Fix underflow in the `Rect::intersection` method ([#678](https://github.com/ratatui-org/ratatui/issues/678))
|
||||
|
||||
- [56fc410](https://github.com/ratatui-org/ratatui/commit/56fc4101056e0f631f563f8f2c07646063e650d3)
|
||||
*(block)* Make `inner` aware of title positions ([#657](https://github.com/ratatui-org/ratatui/issues/657))
|
||||
|
||||
````text
|
||||
Previously, when computing the inner rendering area of a block, all
|
||||
titles were assumed to be positioned at the top, which caused the
|
||||
height of the inner area to be miscalculated.
|
||||
````
|
||||
|
||||
- [ec7b387](https://github.com/ratatui-org/ratatui/commit/ec7b3872b46c6828c88ce7f72308dc67731fca25)
|
||||
*(doc)* Do not access deprecated `Cell::symbol` field in doc example ([#626](https://github.com/ratatui-org/ratatui/issues/626))
|
||||
|
||||
- [37c70db](https://github.com/ratatui-org/ratatui/commit/37c70dbb8e19c0fb35ced16b29751933514a441e)
|
||||
*(table)* Add widths parameter to new() ([#664](https://github.com/ratatui-org/ratatui/issues/664)) [**breaking**]
|
||||
|
||||
````text
|
||||
This prevents creating a table that doesn't actually render anything.
|
||||
````
|
||||
|
||||
- [1f88da7](https://github.com/ratatui-org/ratatui/commit/1f88da75383f6de76e64e9258fbf38d02ec77af9)
|
||||
*(table)* Fix new clippy lint which triggers on table widths tests ([#630](https://github.com/ratatui-org/ratatui/issues/630))
|
||||
|
||||
````text
|
||||
* fix(table): new clippy lint in 1.74.0 triggers on table widths tests
|
||||
````
|
||||
|
||||
- [36d8c53](https://github.com/ratatui-org/ratatui/commit/36d8c5364590a559913c40ee5f021b5d8e3466e6)
|
||||
*(table)* Widths() now accepts AsRef<[Constraint]> ([#628](https://github.com/ratatui-org/ratatui/issues/628))
|
||||
|
||||
````text
|
||||
This allows passing an array, slice or Vec of constraints, which is more
|
||||
ergonomic than requiring this to always be a slice.
|
||||
|
||||
The following calls now all succeed:
|
||||
|
||||
```rust
|
||||
Table::new(rows).widths([Constraint::Length(5), Constraint::Length(5)]);
|
||||
Table::new(rows).widths(&[Constraint::Length(5), Constraint::Length(5)]);
|
||||
|
||||
// widths could also be computed at runtime
|
||||
let widths = vec![Constraint::Length(5), Constraint::Length(5)];
|
||||
Table::new(rows).widths(widths.clone());
|
||||
Table::new(rows).widths(&widths);
|
||||
```
|
||||
````
|
||||
|
||||
- [34d099c](https://github.com/ratatui-org/ratatui/commit/34d099c99af27eacfdde71f9ced255c29e1e001a)
|
||||
*(tabs)* Fixup tests broken by semantic merge conflict ([#665](https://github.com/ratatui-org/ratatui/issues/665))
|
||||
|
||||
````text
|
||||
Two changes without any line overlap caused the tabs tests to break
|
||||
````
|
||||
|
||||
- [e4579f0](https://github.com/ratatui-org/ratatui/commit/e4579f0db2b70b59590cae02e994e3736b19a1b3)
|
||||
*(tabs)* Set the default highlight_style ([#635](https://github.com/ratatui-org/ratatui/issues/635)) [**breaking**]
|
||||
|
||||
````text
|
||||
Previously the default highlight_style was set to `Style::default()`,
|
||||
which meant that the highlight style was the same as the normal style.
|
||||
This change sets the default highlight_style to reversed text.
|
||||
````
|
||||
|
||||
- [28ac55b](https://github.com/ratatui-org/ratatui/commit/28ac55bc62e4e14e3ace300633d56791a1d3dea0)
|
||||
*(tabs)* Tab widget now supports custom padding ([#629](https://github.com/ratatui-org/ratatui/issues/629))
|
||||
|
||||
````text
|
||||
The Tab widget now contains padding_left and and padding_right
|
||||
properties. Those values can be set with functions `padding_left()`,
|
||||
`padding_right()`, and `padding()` which all accept `Into<Line>`.
|
||||
|
||||
Fixes issue https://github.com/ratatui-org/ratatui/issues/502
|
||||
````
|
||||
|
||||
- [df0eb1f](https://github.com/ratatui-org/ratatui/commit/df0eb1f8e94752db542ff58e1453f4f8beab17e2)
|
||||
*(terminal)* Insert_before() now accepts lines > terminal height and doesn't add an extra blank line ([#596](https://github.com/ratatui-org/ratatui/issues/596))
|
||||
|
||||
````text
|
||||
Fixes issue with inserting content with height>viewport_area.height and adds
|
||||
the ability to insert content of height>terminal_height
|
||||
|
||||
- Adds TestBackend::append_lines() and TestBackend::clear_region() methods to
|
||||
support testing the changes
|
||||
````
|
||||
|
||||
- [aaeba27](https://github.com/ratatui-org/ratatui/commit/aaeba2709c09b7373f3781ecd4b0a96b22fc2764)
|
||||
*(uncategorized)* Truncate table when overflow ([#685](https://github.com/ratatui-org/ratatui/issues/685))
|
||||
|
||||
````text
|
||||
This prevents a panic when rendering an empty right aligned and rightmost table cell
|
||||
````
|
||||
|
||||
- [ffa78aa](https://github.com/ratatui-org/ratatui/commit/ffa78aa67ccd79b9aa1af0d7ccf56a2059d0f519)
|
||||
*(uncategorized)* Add #[must_use] to Style-moving methods ([#600](https://github.com/ratatui-org/ratatui/issues/600))
|
||||
|
||||
- [a2f2bd5](https://github.com/ratatui-org/ratatui/commit/a2f2bd5df53a796c0f2a57bb1b22151e52b5ef03)
|
||||
*(uncategorized)* MSRV is now `1.70.0` ([#593](https://github.com/ratatui-org/ratatui/issues/593))
|
||||
|
||||
### Refactor
|
||||
|
||||
- [f767ea7](https://github.com/ratatui-org/ratatui/commit/f767ea7d3766887cb79145103b5aa92e0eabf8f6)
|
||||
*(list)* `start_corner` is now `direction` ([#673](https://github.com/ratatui-org/ratatui/issues/673))
|
||||
|
||||
````text
|
||||
The previous name `start_corner` did not communicate clearly the intent of the method.
|
||||
A new method `direction` and a new enum `ListDirection` were added.
|
||||
|
||||
`start_corner` is now deprecated
|
||||
````
|
||||
|
||||
- [b82451f](https://github.com/ratatui-org/ratatui/commit/b82451fb33f35ae0323a56bb6f962404b076a262)
|
||||
*(examples)* Add vim binding ([#688](https://github.com/ratatui-org/ratatui/issues/688))
|
||||
|
||||
- [0576a8a](https://github.com/ratatui-org/ratatui/commit/0576a8aa3212c57d288c67592337a3870ae6dafc)
|
||||
*(layout)* To natural reading order ([#681](https://github.com/ratatui-org/ratatui/issues/681))
|
||||
|
||||
````text
|
||||
Structs and enums at the top of the file helps show the interaction
|
||||
between the types without having to find each type in between longer
|
||||
impl sections.
|
||||
|
||||
Also moved the try_split function into the Layout impl as an associated
|
||||
function and inlined the `layout::split()` which just called try_split.
|
||||
This makes the code a bit more contained.
|
||||
````
|
||||
|
||||
- [4be18ab](https://github.com/ratatui-org/ratatui/commit/4be18aba8b535165f03d15450276b2e95a7970eb)
|
||||
*(readme)* Reference awesome-ratatui instead of wiki ([#689](https://github.com/ratatui-org/ratatui/issues/689))
|
||||
|
||||
````text
|
||||
* refactor(readme): link awesome-ratatui instead of wiki
|
||||
|
||||
The apps wiki moved to awesome-ratatui
|
||||
|
||||
* docs(readme): Update README.md
|
||||
````
|
||||
|
||||
- [7ef0afc](https://github.com/ratatui-org/ratatui/commit/7ef0afcb62198f76321e84d9bb19a8a590a3b649)
|
||||
*(widgets)* Remove unnecessary dynamic dispatch and heap allocation ([#597](https://github.com/ratatui-org/ratatui/issues/597))
|
||||
|
||||
- [b282a06](https://github.com/ratatui-org/ratatui/commit/b282a0693289d9d2602b54b639d3701d8c8cc8a8)
|
||||
*(uncategorized)* Remove items deprecated since 0.10 ([#691](https://github.com/ratatui-org/ratatui/issues/691)) [**breaking**]
|
||||
|
||||
````text
|
||||
Remove `Axis::title_style` and `Buffer::set_background` which are deprecated since 0.10
|
||||
````
|
||||
|
||||
- [7ced7c0](https://github.com/ratatui-org/ratatui/commit/7ced7c0aa3acdaa63ed6add59711614993210ba3)
|
||||
*(uncategorized)* Define struct WrappedLine instead of anonymous tuple ([#608](https://github.com/ratatui-org/ratatui/issues/608))
|
||||
|
||||
````text
|
||||
It makes the type easier to document, and more obvious for users
|
||||
````
|
||||
|
||||
### Documentation
|
||||
|
||||
- [fe632d7](https://github.com/ratatui-org/ratatui/commit/fe632d70cb150264d9af2f79145a1d14a3637f3e)
|
||||
*(sparkline)* Add documentation ([#648](https://github.com/ratatui-org/ratatui/issues/648))
|
||||
|
||||
- [f4c8de0](https://github.com/ratatui-org/ratatui/commit/f4c8de041d48cec5ea9b3e1f540f57af5a09d7a4)
|
||||
*(chart)* Document chart module ([#696](https://github.com/ratatui-org/ratatui/issues/696))
|
||||
|
||||
- [1b8b626](https://github.com/ratatui-org/ratatui/commit/1b8b6261e2de29a37b2cd7d6ee8659fb46d3beff)
|
||||
*(examples)* Add animation and FPS counter to colors_rgb ([#583](https://github.com/ratatui-org/ratatui/issues/583))
|
||||
|
||||
- [2169a0d](https://github.com/ratatui-org/ratatui/commit/2169a0da01e3bd6272e33b9de26a033fcb5f55f2)
|
||||
*(examples)* Add example of half block rendering ([#687](https://github.com/ratatui-org/ratatui/issues/687))
|
||||
|
||||
````text
|
||||
This is a fun example of how to render big text using half blocks
|
||||
````
|
||||
|
||||
- [41c44a4](https://github.com/ratatui-org/ratatui/commit/41c44a4af66ba791959f3a298d1b544330b9a164)
|
||||
*(frame)* Add docs about resize events ([#697](https://github.com/ratatui-org/ratatui/issues/697))
|
||||
|
||||
- [91c67eb](https://github.com/ratatui-org/ratatui/commit/91c67eb1009449e0dfdd29e6ef0132c5254cfbde)
|
||||
*(github)* Update code owners ([#666](https://github.com/ratatui-org/ratatui/issues/666))
|
||||
|
||||
````text
|
||||
onboard @Valentin271 as maintainer
|
||||
````
|
||||
|
||||
- [458fa90](https://github.com/ratatui-org/ratatui/commit/458fa9036281e0e6e88bd2ec90c633e499ce547c)
|
||||
*(lib)* Tweak the crate documentation ([#659](https://github.com/ratatui-org/ratatui/issues/659))
|
||||
|
||||
- [3ec4e24](https://github.com/ratatui-org/ratatui/commit/3ec4e24d00e118a12c8fea888e16ce19b75cf45f)
|
||||
*(list)* Add documentation to the List widget ([#669](https://github.com/ratatui-org/ratatui/issues/669))
|
||||
|
||||
````text
|
||||
Adds documentation to the List widget and all its sub components like `ListState` and `ListItem`
|
||||
````
|
||||
|
||||
- [9f37100](https://github.com/ratatui-org/ratatui/commit/9f371000968044e09545d66068c4ed4ea4b35d8a)
|
||||
*(readme)* Update README.md and fix the bug that demo2 cannot run ([#595](https://github.com/ratatui-org/ratatui/issues/595))
|
||||
|
||||
````text
|
||||
Fixes https://github.com/ratatui-org/ratatui/issues/594
|
||||
````
|
||||
|
||||
- [2a87251](https://github.com/ratatui-org/ratatui/commit/2a87251152432fd99c18864f32874fed2cab2f99)
|
||||
*(security)* Add security policy ([#676](https://github.com/ratatui-org/ratatui/issues/676))
|
||||
|
||||
````text
|
||||
* docs: Create SECURITY.md
|
||||
|
||||
* Update SECURITY.md
|
||||
````
|
||||
|
||||
- [987f7ee](https://github.com/ratatui-org/ratatui/commit/987f7eed4c8bd09e319b504e587eb1f3667ee64b)
|
||||
*(website)* Rename book to website ([#661](https://github.com/ratatui-org/ratatui/issues/661))
|
||||
|
||||
- [a15c3b2](https://github.com/ratatui-org/ratatui/commit/a15c3b2660bf4102bc881a5bc11959bc136f4a17)
|
||||
*(uncategorized)* Remove deprecated table constructor from breaking changes ([#698](https://github.com/ratatui-org/ratatui/issues/698))
|
||||
|
||||
- [113b4b7](https://github.com/ratatui-org/ratatui/commit/113b4b7a4ea841fe2ca7b1c153243fec781c3cc0)
|
||||
*(uncategorized)* Rename template links to remove ratatui from name 📚 ([#690](https://github.com/ratatui-org/ratatui/issues/690))
|
||||
|
||||
- [211160c](https://github.com/ratatui-org/ratatui/commit/211160ca165e2ad23b3d4cd9382c6e4869644a9c)
|
||||
*(uncategorized)* Remove simple-tui-rs ([#651](https://github.com/ratatui-org/ratatui/issues/651))
|
||||
|
||||
````text
|
||||
This has not been recently and doesn't lead to good code
|
||||
````
|
||||
|
||||
### Styling
|
||||
|
||||
- [6a6e9dd](https://github.com/ratatui-org/ratatui/commit/6a6e9dde9dc66ecb6f47f858fd0a67d7dc9eb7d1)
|
||||
*(tabs)* Fix doc formatting ([#662](https://github.com/ratatui-org/ratatui/issues/662))
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- [910ad00](https://github.com/ratatui-org/ratatui/commit/910ad00059c3603ba6b1751c95783f974fde88a1)
|
||||
*(rustfmt)* Enable format_code_in_doc_comments ([#695](https://github.com/ratatui-org/ratatui/issues/695))
|
||||
|
||||
````text
|
||||
This enables more consistently formatted code in doc comments,
|
||||
especially since ratatui heavily uses fluent setters.
|
||||
|
||||
See https://rust-lang.github.io/rustfmt/?version=v1.6.0#format_code_in_doc_comments
|
||||
````
|
||||
|
||||
- [d118565](https://github.com/ratatui-org/ratatui/commit/d118565ef60480fba8f2906ede81f875a562cb61)
|
||||
*(table)* Cleanup docs and builder methods ([#638](https://github.com/ratatui-org/ratatui/issues/638))
|
||||
|
||||
````text
|
||||
- Refactor the `table` module for better top to bottom readability by
|
||||
putting types first and arranging them in a logical order (Table, Row,
|
||||
Cell, other).
|
||||
|
||||
- Adds new methods for:
|
||||
- `Table::rows`
|
||||
- `Row::cells`
|
||||
- `Cell::new`
|
||||
- `Cell::content`
|
||||
- `TableState::new`
|
||||
- `TableState::selected_mut`
|
||||
|
||||
- Makes `HighlightSpacing::should_add` pub(crate) since it's an internal
|
||||
detail.
|
||||
|
||||
- Adds tests for all the new methods and simple property tests for all
|
||||
the other setter methods.
|
||||
````
|
||||
|
||||
- [dd22e72](https://github.com/ratatui-org/ratatui/commit/dd22e721e3aed24538eb08e46e40339cec636bcb)
|
||||
*(uncategorized)* Correct "builder methods" in docs and add `must_use` on widgets setters ([#655](https://github.com/ratatui-org/ratatui/issues/655))
|
||||
|
||||
- [18e19f6](https://github.com/ratatui-org/ratatui/commit/18e19f6ce6ae3ce9bd52110ab6cbd4ed4bcca5e6)
|
||||
*(uncategorized)* Fix breaking changes doc versions ([#639](https://github.com/ratatui-org/ratatui/issues/639))
|
||||
|
||||
````text
|
||||
Moves the layout::new change to unreleasedd section and adds the table change
|
||||
````
|
||||
|
||||
- [a58cce2](https://github.com/ratatui-org/ratatui/commit/a58cce2dba404fe394bbb298645bf3c40518fe1f)
|
||||
*(uncategorized)* Disable default benchmarking ([#598](https://github.com/ratatui-org/ratatui/issues/598))
|
||||
|
||||
````text
|
||||
Disables the default benchmarking behaviour for the lib target to fix unrecognized
|
||||
criterion benchmark arguments.
|
||||
|
||||
See https://bheisler.github.io/criterion.rs/book/faq.html#cargo-bench-gives-unrecognized-option-errors-for-valid-command-line-options for details
|
||||
````
|
||||
|
||||
### Continuous Integration
|
||||
|
||||
- [59b9c32](https://github.com/ratatui-org/ratatui/commit/59b9c32fbc2bc6725bdec42e63216024fab71493)
|
||||
*(codecov)* Adjust threshold and noise settings ([#615](https://github.com/ratatui-org/ratatui/issues/615))
|
||||
|
||||
````text
|
||||
Fixes https://github.com/ratatui-org/ratatui/issues/612
|
||||
````
|
||||
|
||||
- [03401cd](https://github.com/ratatui-org/ratatui/commit/03401cd46e6566af4d063bac11efc30f28b5358a)
|
||||
*(uncategorized)* Fix untrusted input in pr check workflow ([#680](https://github.com/ratatui-org/ratatui/issues/680))
|
||||
|
||||
### Contributors
|
||||
|
||||
Thank you so much to everyone that contributed to this release!
|
||||
|
||||
Here is the list of contributors who have contributed to `ratatui` for the first time!
|
||||
|
||||
* @rikonaka
|
||||
* @danny-burrows
|
||||
* @SOF3
|
||||
* @jan-ferdinand
|
||||
* @rhaskia
|
||||
* @asomers
|
||||
* @progval
|
||||
* @TylerBloom
|
||||
* @YeungKC
|
||||
* @lyuha
|
||||
|
||||
## [0.24.0](https://github.com/ratatui-org/ratatui/releases/tag/0.24.0) - 2023-10-23
|
||||
|
||||
We are excited to announce the new version of `ratatui` - a Rust library that's all about cooking up TUIs 🐭
|
||||
@@ -10,7 +495,7 @@ In this version, we've introduced features like window size API, enhanced chart
|
||||
The list of \*breaking changes\* can be found [here](https://github.com/ratatui-org/ratatui/blob/main/BREAKING-CHANGES.md) ⚠️.
|
||||
Also, we created various tutorials and walkthroughs in [Ratatui Book](https://github.com/ratatui-org/ratatui-book) which is available at <https://ratatui.rs> 🚀
|
||||
|
||||
✨ **Release highlights**: <https://ratatui.rs/highlights/v0.24.html>
|
||||
✨ **Release highlights**: <https://ratatui.rs/highlights/v024>
|
||||
|
||||
### Features
|
||||
|
||||
|
||||
@@ -171,6 +171,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
|
||||
|
||||
55
Cargo.toml
55
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ratatui"
|
||||
version = "0.24.0" # crate version
|
||||
version = "0.25.0" # 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,14 +24,14 @@ rust-version = "1.70.0"
|
||||
|
||||
[dependencies]
|
||||
crossterm = { version = "0.27", optional = true }
|
||||
termion = { version = "2.0", optional = true }
|
||||
termion = { version = "3.0", optional = true }
|
||||
termwiz = { version = "0.20.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"] }
|
||||
time = { version = "0.3.11", optional = true, features = ["local-offset"] }
|
||||
@@ -39,6 +39,8 @@ 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.12.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-segment-size", "unstable-rendered-line-info"]
|
||||
|
||||
## Enables the [`Layout::segment_size`](crate::layout::Layout::segment_size) method which is experimental and may change in the
|
||||
## future. See [Issue #536](https://github.com/ratatui-org/ratatui/issues/536) for more details.
|
||||
unstable-segment-size = []
|
||||
|
||||
## 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 = []
|
||||
|
||||
[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,16 @@ 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 = "list"
|
||||
required-features = ["crossterm"]
|
||||
@@ -213,6 +249,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 +283,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",
|
||||
|
||||
160
README.md
160
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)<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,26 @@ 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.
|
||||
|
||||
## 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 +101,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 +151,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 +174,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 +198,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 +228,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,23 +280,24 @@ 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
|
||||
@@ -321,24 +317,28 @@ 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
|
||||
[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
|
||||
|
||||
<!-- cargo-rdme end -->
|
||||
|
||||
@@ -387,9 +387,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 +399,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 +423,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
|
||||
@@ -30,6 +30,13 @@ 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 | indent(prefix=" ") }}
|
||||
{%- endif %}
|
||||
{%- endfor %}
|
||||
{% endmacro -%}
|
||||
|
||||
{% for group, commits in commits | group_by(attribute="group") %}
|
||||
|
||||
11
codecov.yml
11
codecov.yml
@@ -1,3 +1,14 @@
|
||||
coverage: # https://docs.codecov.com/docs/codecovyml-reference#coverage
|
||||
precision: 1 # e.g. 89.1%
|
||||
round: down
|
||||
range: 85..100 # https://docs.codecov.com/docs/coverage-configuration#section-range
|
||||
status: # https://docs.codecov.com/docs/commit-status
|
||||
project:
|
||||
default:
|
||||
threshold: 1% # Avoid false negatives
|
||||
ignore:
|
||||
- "examples"
|
||||
- "benches"
|
||||
comment: # https://docs.codecov.com/docs/pull-request-comments
|
||||
# make the comments less noisy
|
||||
require_changes: true
|
||||
|
||||
@@ -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,11 +315,18 @@ 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
|
||||
```
|
||||
@@ -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
|
||||
|
||||
@@ -134,11 +134,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] = frame.size().split(&vertical);
|
||||
let [left, right] = bottom.split(&horizontal);
|
||||
|
||||
let barchart = BarChart::default()
|
||||
.block(Block::default().title("Data1").borders(Borders::ALL))
|
||||
@@ -146,15 +146,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>> {
|
||||
|
||||
@@ -103,20 +103,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] = area.split(&main_layout);
|
||||
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,4 @@
|
||||
use std::{error::Error, io, rc::Rc};
|
||||
use std::{error::Error, io};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
@@ -55,43 +55,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 +151,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 +164,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 +174,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 +185,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 +201,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 +217,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 +231,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);
|
||||
|
||||
@@ -59,10 +59,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 +107,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] = frame.size().split(&horizontal);
|
||||
let [pong, boxes] = right.split(&vertical);
|
||||
|
||||
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 + '_ {
|
||||
|
||||
@@ -9,18 +9,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 {
|
||||
@@ -140,16 +132,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] = area.split(&vertical);
|
||||
let [line_chart, scatter] = bottom.split(&horizontal);
|
||||
|
||||
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 +190,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.),
|
||||
];
|
||||
|
||||
@@ -42,14 +42,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 +74,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 +94,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 +115,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 +136,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 +178,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 +220,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);
|
||||
|
||||
@@ -2,63 +2,85 @@
|
||||
///
|
||||
/// Requires a terminal that supports 24-bit color (true color) and unicode.
|
||||
use std::{
|
||||
error::Error,
|
||||
io::{stdout, Stdout},
|
||||
rc::Rc,
|
||||
time::Duration,
|
||||
io::stdout,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use color_eyre::config::HookBuilder;
|
||||
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 palette::{convert::FromColorUnclamped, Okhsv, Srgb};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
type Result<T> = std::result::Result<T, Box<dyn Error>>;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
install_panic_hook();
|
||||
App::new()?.run()
|
||||
fn main() -> color_eyre::Result<()> {
|
||||
App::run()
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct App {
|
||||
terminal: Terminal<CrosstermBackend<Stdout>>,
|
||||
should_quit: bool,
|
||||
// a 2d vec of the colors to render, calculated when the size changes as this is expensive
|
||||
// to calculate every frame
|
||||
colors: Vec<Vec<Color>>,
|
||||
last_size: Rect,
|
||||
fps: Fps,
|
||||
frame_count: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Fps {
|
||||
frame_count: usize,
|
||||
last_instant: Instant,
|
||||
fps: Option<f32>,
|
||||
}
|
||||
|
||||
struct AppWidget<'a> {
|
||||
title: Paragraph<'a>,
|
||||
fps_widget: FpsWidget<'a>,
|
||||
rgb_colors_widget: RgbColorsWidget<'a>,
|
||||
}
|
||||
|
||||
struct FpsWidget<'a> {
|
||||
fps: &'a Fps,
|
||||
}
|
||||
|
||||
struct RgbColorsWidget<'a> {
|
||||
/// The colors to render - should be double the height of the area
|
||||
colors: &'a Vec<Vec<Color>>,
|
||||
/// the number of elapsed frames that have passed - used to animate the colors
|
||||
frame_count: usize,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new() -> Result<Self> {
|
||||
Ok(Self {
|
||||
terminal: Terminal::new(CrosstermBackend::new(stdout()))?,
|
||||
should_quit: false,
|
||||
})
|
||||
}
|
||||
pub fn run() -> color_eyre::Result<()> {
|
||||
install_panic_hook()?;
|
||||
|
||||
pub fn run(mut self) -> Result<()> {
|
||||
init_terminal()?;
|
||||
self.terminal.clear()?;
|
||||
while !self.should_quit {
|
||||
self.draw()?;
|
||||
self.handle_events()?;
|
||||
let mut terminal = init_terminal()?;
|
||||
let mut app = Self::default();
|
||||
|
||||
while !app.should_quit {
|
||||
app.tick();
|
||||
terminal.draw(|frame| {
|
||||
let size = frame.size();
|
||||
app.setup_colors(size);
|
||||
frame.render_widget(AppWidget::new(&app), size);
|
||||
})?;
|
||||
app.handle_events()?;
|
||||
}
|
||||
restore_terminal()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw(&mut self) -> Result<()> {
|
||||
self.terminal.draw(|frame| {
|
||||
frame.render_widget(RgbColors, frame.size());
|
||||
})?;
|
||||
Ok(())
|
||||
fn tick(&mut self) {
|
||||
self.frame_count += 1;
|
||||
self.fps.tick();
|
||||
}
|
||||
|
||||
fn handle_events(&mut self) -> Result<()> {
|
||||
if event::poll(Duration::from_millis(100))? {
|
||||
fn handle_events(&mut self) -> color_eyre::Result<()> {
|
||||
if event::poll(Duration::from_secs_f32(1.0 / 60.0))? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
|
||||
self.should_quit = true;
|
||||
@@ -67,80 +89,136 @@ impl App {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for App {
|
||||
fn drop(&mut self) {
|
||||
let _ = restore_terminal();
|
||||
fn setup_colors(&mut self, size: Rect) {
|
||||
// only update the colors if the size has changed since the last time we rendered
|
||||
if self.last_size.width == size.width && self.last_size.height == size.height {
|
||||
return;
|
||||
}
|
||||
self.last_size = size;
|
||||
let Rect { width, height, .. } = size;
|
||||
// double the height because each screen row has two rows of half block pixels
|
||||
let height = height * 2;
|
||||
self.colors.clear();
|
||||
for y in 0..height {
|
||||
let mut row = Vec::new();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct RgbColors;
|
||||
impl Fps {
|
||||
fn tick(&mut self) {
|
||||
self.frame_count += 1;
|
||||
let elapsed = self.last_instant.elapsed();
|
||||
// update the fps every second, but only if we've rendered at least 2 frames (to avoid
|
||||
// noise in the fps calculation)
|
||||
if elapsed > Duration::from_secs(1) && self.frame_count > 2 {
|
||||
self.fps = Some(self.frame_count as f32 / elapsed.as_secs_f32());
|
||||
self.frame_count = 0;
|
||||
self.last_instant = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for RgbColors {
|
||||
impl Default for Fps {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
frame_count: 0,
|
||||
last_instant: Instant::now(),
|
||||
fps: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> AppWidget<'a> {
|
||||
fn new(app: &'a App) -> Self {
|
||||
let title =
|
||||
Paragraph::new("colors_rgb example. Press q to quit").alignment(Alignment::Center);
|
||||
Self {
|
||||
title,
|
||||
fps_widget: FpsWidget { fps: &app.fps },
|
||||
rgb_colors_widget: RgbColorsWidget {
|
||||
colors: &app.colors,
|
||||
frame_count: app.frame_count,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for AppWidget<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let layout = Self::layout(area);
|
||||
Self::render_title(layout[0], buf);
|
||||
Self::render_colors(layout[1], buf);
|
||||
let vertical = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
|
||||
let horizontal = Layout::horizontal([Constraint::Min(0), Constraint::Length(8)]);
|
||||
let [top, colors] = area.split(&vertical);
|
||||
let [title, fps] = top.split(&horizontal);
|
||||
|
||||
self.title.render(title, buf);
|
||||
self.fps_widget.render(fps, buf);
|
||||
self.rgb_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) {
|
||||
impl Widget for RgbColorsWidget<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let colors = self.colors;
|
||||
for (xi, x) in (area.left()..area.right()).enumerate() {
|
||||
// animate the colors by shifting the x index by the frame number
|
||||
let xi = (xi + self.frame_count) % (area.width as usize);
|
||||
for (yi, y) in (area.top()..area.bottom()).enumerate() {
|
||||
let 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);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}));
|
||||
impl<'a> Widget for FpsWidget<'a> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
if let Some(fps) = self.fps.fps {
|
||||
let text = format!("{:.1} fps", fps);
|
||||
Paragraph::new(text).render(area, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn init_terminal() -> Result<()> {
|
||||
enable_raw_mode()?;
|
||||
stdout().execute(EnterAlternateScreen)?;
|
||||
/// Install a panic hook that restores the terminal before panicking.
|
||||
fn install_panic_hook() -> 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 restore_terminal() -> Result<()> {
|
||||
fn init_terminal() -> color_eyre::Result<Terminal<impl Backend>> {
|
||||
enable_raw_mode()?;
|
||||
stdout().execute(EnterAlternateScreen)?;
|
||||
let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
|
||||
terminal.clear()?;
|
||||
terminal.hide_cursor()?;
|
||||
Ok(terminal)
|
||||
}
|
||||
|
||||
fn restore_terminal() -> color_eyre::Result<()> {
|
||||
disable_raw_mode()?;
|
||||
stdout().execute(LeaveAlternateScreen)?;
|
||||
Ok(())
|
||||
|
||||
@@ -1,13 +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 800
|
||||
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"
|
||||
Type "cargo run --example=colors_rgb --features=crossterm --release"
|
||||
Enter
|
||||
Sleep 2s
|
||||
Screenshot "target/colors_rgb.png"
|
||||
# Screenshot "target/colors_rgb.png"
|
||||
Show
|
||||
Sleep 1s
|
||||
Sleep 10s
|
||||
|
||||
454
examples/constraints.rs
Normal file
454
examples/constraints.rs
Normal file
@@ -0,0 +1,454 @@
|
||||
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 1
|
||||
const FIXED_COLOR: Color = tailwind::RED.c900;
|
||||
// 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 PROPORTIONAL_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]
|
||||
Fixed,
|
||||
Min,
|
||||
Max,
|
||||
Length,
|
||||
Percentage,
|
||||
Ratio,
|
||||
Proportional,
|
||||
}
|
||||
|
||||
#[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] = area.split(&Layout::vertical([
|
||||
Constraint::Fixed(3),
|
||||
Constraint::Fixed(3),
|
||||
Proportional(0),
|
||||
]));
|
||||
|
||||
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())
|
||||
.alignment(Alignment::Center)
|
||||
.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 {
|
||||
Fixed => 4,
|
||||
Length => 4,
|
||||
Percentage => 5,
|
||||
Ratio => 4,
|
||||
Proportional => 2,
|
||||
Min => 5,
|
||||
Max => 5,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_tab_title(value: SelectedTab) -> Line<'static> {
|
||||
use SelectedTab::*;
|
||||
let text = format!(" {value} ");
|
||||
let color = match value {
|
||||
Fixed => FIXED_COLOR,
|
||||
Length => LENGTH_COLOR,
|
||||
Percentage => PERCENTAGE_COLOR,
|
||||
Ratio => RATIO_COLOR,
|
||||
Proportional => PROPORTIONAL_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::Fixed => self.render_fixed_example(area, buf),
|
||||
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::Proportional => self.render_proportional_example(area, buf),
|
||||
SelectedTab::Min => self.render_min_example(area, buf),
|
||||
SelectedTab::Max => self.render_max_example(area, buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SelectedTab {
|
||||
fn render_fixed_example(&self, area: Rect, buf: &mut Buffer) {
|
||||
let [example1, example2, example3, example4, _] =
|
||||
area.split(&Layout::vertical([Fixed(EXAMPLE_HEIGHT); 5]));
|
||||
|
||||
Example::new(&[Fixed(40), Proportional(0)]).render(example1, buf);
|
||||
Example::new(&[Fixed(20), Fixed(20), Proportional(0)]).render(example2, buf);
|
||||
Example::new(&[Fixed(20), Min(20), Max(20)]).render(example3, buf);
|
||||
Example::new(&[
|
||||
Length(20),
|
||||
Percentage(20),
|
||||
Ratio(1, 5),
|
||||
Proportional(1),
|
||||
Fixed(15),
|
||||
])
|
||||
.render(example4, buf);
|
||||
}
|
||||
|
||||
fn render_length_example(&self, area: Rect, buf: &mut Buffer) {
|
||||
let [example1, example2, example3, example4, _] =
|
||||
area.split(&Layout::vertical([Fixed(EXAMPLE_HEIGHT); 5]));
|
||||
|
||||
Example::new(&[Length(20), Fixed(20)]).render(example1, buf);
|
||||
Example::new(&[Length(20), Length(20)]).render(example2, buf);
|
||||
Example::new(&[Length(20), Min(20)]).render(example3, buf);
|
||||
Example::new(&[Length(20), Max(20)]).render(example4, buf);
|
||||
}
|
||||
|
||||
fn render_percentage_example(&self, area: Rect, buf: &mut Buffer) {
|
||||
let [example1, example2, example3, example4, example5, _] =
|
||||
area.split(&Layout::vertical([Fixed(EXAMPLE_HEIGHT); 6]));
|
||||
|
||||
Example::new(&[Percentage(75), Proportional(0)]).render(example1, buf);
|
||||
Example::new(&[Percentage(25), Proportional(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), Proportional(0)]).render(example5, buf);
|
||||
}
|
||||
|
||||
fn render_ratio_example(&self, area: Rect, buf: &mut Buffer) {
|
||||
let [example1, example2, example3, example4, _] =
|
||||
area.split(&Layout::vertical([Fixed(EXAMPLE_HEIGHT); 5]));
|
||||
|
||||
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_proportional_example(&self, area: Rect, buf: &mut Buffer) {
|
||||
let [example1, example2, _] = area.split(&Layout::vertical([Fixed(EXAMPLE_HEIGHT); 3]));
|
||||
|
||||
Example::new(&[Proportional(1), Proportional(2), Proportional(3)]).render(example1, buf);
|
||||
Example::new(&[Proportional(1), Percentage(50), Proportional(1)]).render(example2, buf);
|
||||
}
|
||||
|
||||
fn render_min_example(&self, area: Rect, buf: &mut Buffer) {
|
||||
let [example1, example2, example3, example4, example5, _] =
|
||||
area.split(&Layout::vertical([Fixed(EXAMPLE_HEIGHT); 6]));
|
||||
|
||||
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, _] =
|
||||
area.split(&Layout::vertical([Fixed(EXAMPLE_HEIGHT); 6]));
|
||||
|
||||
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, _] = area.split(&Layout::vertical([
|
||||
Fixed(ILLUSTRATION_HEIGHT),
|
||||
Fixed(SPACER_HEIGHT),
|
||||
]));
|
||||
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::Fixed(_) => FIXED_COLOR,
|
||||
Constraint::Length(_) => LENGTH_COLOR,
|
||||
Constraint::Percentage(_) => PERCENTAGE_COLOR,
|
||||
Constraint::Ratio(_, _) => RATIO_COLOR,
|
||||
Constraint::Proportional(_) => PROPORTIONAL_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)
|
||||
.alignment(Alignment::Center)
|
||||
.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(())
|
||||
}
|
||||
14
examples/constraints.tape
Normal file
14
examples/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
|
||||
@@ -171,42 +171,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, _] = frame.size().split(&vertical);
|
||||
|
||||
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, _] = area.split(&horizontal);
|
||||
|
||||
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 +208,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;
|
||||
|
||||
@@ -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(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
18
examples/demo2-destroy.tape
Normal file
18
examples/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
|
||||
@@ -2,15 +2,29 @@ use std::time::Duration;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
|
||||
use ratatui::prelude::Rect;
|
||||
use rand::Rng;
|
||||
use rand_chacha::rand_core::SeedableRng;
|
||||
use ratatui::{buffer::Cell, layout::Flex, prelude::*, widgets::Widget};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{Root, Term};
|
||||
use crate::{
|
||||
big_text::{BigTextBuilder, PixelSize},
|
||||
Root, Term,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct App {
|
||||
term: Term,
|
||||
should_quit: bool,
|
||||
context: AppContext,
|
||||
mode: Mode,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
enum Mode {
|
||||
#[default]
|
||||
Normal,
|
||||
Destroy,
|
||||
Quit,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
@@ -23,15 +37,15 @@ impl App {
|
||||
fn new() -> Result<Self> {
|
||||
Ok(Self {
|
||||
term: Term::start()?,
|
||||
should_quit: false,
|
||||
context: AppContext::default(),
|
||||
mode: Mode::Normal,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn run() -> Result<()> {
|
||||
install_panic_hook();
|
||||
let mut app = Self::new()?;
|
||||
while !app.should_quit {
|
||||
while !app.should_quit() {
|
||||
app.draw()?;
|
||||
app.handle_events()?;
|
||||
}
|
||||
@@ -41,13 +55,20 @@ impl App {
|
||||
|
||||
fn draw(&mut self) -> Result<()> {
|
||||
self.term
|
||||
.draw(|frame| frame.render_widget(Root::new(&self.context), frame.size()))
|
||||
.draw(|frame| {
|
||||
frame.render_widget(Root::new(&self.context), frame.size());
|
||||
if self.mode == Mode::Destroy {
|
||||
destroy(frame);
|
||||
}
|
||||
})
|
||||
.context("terminal.draw")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_events(&mut self) -> Result<()> {
|
||||
match Term::next_event(Duration::from_millis(16))? {
|
||||
// https://superuser.com/questions/1449366/do-60-fps-gifs-actually-exist-or-is-the-maximum-50-fps
|
||||
const GIF_FRAME_RATE: f64 = 50.0;
|
||||
match Term::next_event(Duration::from_secs_f64(1.0 / GIF_FRAME_RATE))? {
|
||||
Some(Event::Key(key)) => self.handle_key_event(key),
|
||||
Some(Event::Resize(width, height)) => {
|
||||
Ok(self.term.resize(Rect::new(0, 0, width, height))?)
|
||||
@@ -65,7 +86,7 @@ impl App {
|
||||
const TAB_COUNT: usize = 5;
|
||||
match key.code {
|
||||
KeyCode::Char('q') | KeyCode::Esc => {
|
||||
self.should_quit = true;
|
||||
self.mode = Mode::Quit;
|
||||
}
|
||||
KeyCode::Tab | KeyCode::BackTab if key.modifiers.contains(KeyModifiers::SHIFT) => {
|
||||
let tab_index = context.tab_index + TAB_COUNT; // to wrap around properly
|
||||
@@ -82,10 +103,142 @@ impl App {
|
||||
KeyCode::Down | KeyCode::Char('j') => {
|
||||
context.row_index = context.row_index.saturating_add(1);
|
||||
}
|
||||
KeyCode::Char('d') => {
|
||||
self.mode = Mode::Destroy;
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn should_quit(&self) -> bool {
|
||||
self.mode == Mode::Quit
|
||||
}
|
||||
}
|
||||
|
||||
/// 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`
|
||||
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] = area.split(&vertical);
|
||||
let [area] = area.split(&horizontal);
|
||||
area
|
||||
}
|
||||
|
||||
pub fn install_panic_hook() {
|
||||
|
||||
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(())
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ pub use term::*;
|
||||
pub use theme::*;
|
||||
|
||||
mod app;
|
||||
mod big_text;
|
||||
mod colors;
|
||||
mod root;
|
||||
mod tabs;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use itertools::Itertools;
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
@@ -18,25 +16,31 @@ impl<'a> Root<'a> {
|
||||
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);
|
||||
let vertical = Layout::vertical([
|
||||
Constraint::Length(1),
|
||||
Constraint::Min(0),
|
||||
Constraint::Length(1),
|
||||
]);
|
||||
let [title_bar, tab, bottom_bar] = area.split(&vertical);
|
||||
self.render_title_bar(title_bar, buf);
|
||||
self.render_selected_tab(tab, buf);
|
||||
self.render_bottom_bar(bottom_bar, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl Root<'_> {
|
||||
fn render_title_bar(&self, area: Rect, buf: &mut Buffer) {
|
||||
let area = layout(area, Direction::Horizontal, vec![0, 45]);
|
||||
let horizontal = Layout::horizontal([Constraint::Min(0), Constraint::Length(45)]);
|
||||
let [title, tabs] = area.split(&horizontal);
|
||||
|
||||
Paragraph::new(Span::styled("Ratatui", THEME.app_title)).render(area[0], buf);
|
||||
Paragraph::new(Span::styled("Ratatui", THEME.app_title)).render(title, 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);
|
||||
.render(tabs, buf);
|
||||
}
|
||||
|
||||
fn render_selected_tab(&self, area: Rect, buf: &mut Buffer) {
|
||||
@@ -73,21 +77,3 @@ impl Root<'_> {
|
||||
.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] = [
|
||||
" ███ ",
|
||||
@@ -51,9 +51,10 @@ impl AboutTab {
|
||||
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] = area.split(&horizontal);
|
||||
render_crate_description(description, buf);
|
||||
render_logo(self.selected_row, logo, buf);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +117,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 {
|
||||
@@ -60,20 +60,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] = area.split(&vertical);
|
||||
render_inbox(self.selected_index, inbox, buf);
|
||||
render_email(self.selected_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] = area.split(&vertical);
|
||||
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 +96,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 +108,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 +122,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] = inner.split(&vertical);
|
||||
let headers = vec![
|
||||
Line::from(vec![
|
||||
"From: ".set_style(theme.header),
|
||||
@@ -134,9 +137,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 {
|
||||
@@ -123,10 +123,13 @@ impl Widget for RecipeTab {
|
||||
horizontal: 2,
|
||||
vertical: 1,
|
||||
});
|
||||
let area = layout(area, Direction::Horizontal, vec![44, 0]);
|
||||
let [recipe, ingredients] = area.split(&Layout::horizontal([
|
||||
Constraint::Length(44),
|
||||
Constraint::Min(0),
|
||||
]));
|
||||
|
||||
render_recipe(area[0], buf);
|
||||
render_ingredients(self.selected_row, area[1], buf);
|
||||
render_recipe(recipe, buf);
|
||||
render_ingredients(self.selected_row, ingredients, buf);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,13 +146,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,7 +4,7 @@ use ratatui::{
|
||||
widgets::{canvas::*, *},
|
||||
};
|
||||
|
||||
use crate::{layout, RgbSwatch, THEME};
|
||||
use crate::{RgbSwatch, THEME};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TracerouteTab {
|
||||
@@ -28,14 +28,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] = area.split(&horizontal);
|
||||
let [hops, pings] = left.split(&vertical);
|
||||
|
||||
render_hops(self.selected_row, hops, buf);
|
||||
render_ping(self.selected_row, pings, buf);
|
||||
render_map(self.selected_row, map, buf);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,9 +50,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,7 +6,7 @@ use ratatui::{
|
||||
};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
use crate::{color_from_oklab, layout, RgbSwatch, THEME};
|
||||
use crate::{color_from_oklab, RgbSwatch, THEME};
|
||||
|
||||
pub struct WeatherTab {
|
||||
pub selected_row: usize,
|
||||
@@ -32,14 +32,24 @@ 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] = area.split(&Layout::vertical([
|
||||
Constraint::Min(0),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
]));
|
||||
let [calendar, charts] = main.split(&Layout::horizontal([
|
||||
Constraint::Length(23),
|
||||
Constraint::Min(0),
|
||||
]));
|
||||
let [simple, horizontal] = charts.split(&Layout::vertical([
|
||||
Constraint::Length(29),
|
||||
Constraint::Min(0),
|
||||
]));
|
||||
|
||||
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.selected_row, gauges, buf);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,7 +124,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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -53,48 +53,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] = frame.size().split(&vertical);
|
||||
let [left, right] = main_area.split(&horizontal);
|
||||
|
||||
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(
|
||||
|
||||
473
examples/flex.rs
Normal file
473
examples/flex.rs
Normal file
@@ -0,0 +1,473 @@
|
||||
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,
|
||||
widgets::{block::Title, *},
|
||||
};
|
||||
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
|
||||
|
||||
const EXAMPLE_DATA: &[(&str, &[Constraint])] = &[
|
||||
(
|
||||
"Min(u16) takes any excess space when using `Stretch` or `StretchLast`",
|
||||
&[Fixed(20), Min(20), Max(20)],
|
||||
),
|
||||
(
|
||||
"Proportional(u16) takes any excess space in all `Flex` layouts",
|
||||
&[Length(20), Percentage(20), Ratio(1, 5), Proportional(1)],
|
||||
),
|
||||
(
|
||||
"In `StretchLast`, last constraint of lowest priority takes excess space",
|
||||
&[Length(20), Fixed(20), Percentage(20)],
|
||||
),
|
||||
("", &[Fixed(20), Percentage(20), Length(20)]),
|
||||
("", &[Percentage(20), Length(20), Fixed(20)]),
|
||||
("", &[Length(20), Length(15)]),
|
||||
("Spacing has no effect in `SpaceAround` and `SpaceBetween`", &[Proportional(1), Proportional(1)]),
|
||||
("", &[Length(20), Fixed(20)]),
|
||||
(
|
||||
"When not using `Flex::Stretch` or `Flex::StretchLast`,\n`Min(u16)` and `Max(u16)` collapse to their lowest values",
|
||||
&[Min(20), Max(20)],
|
||||
),
|
||||
(
|
||||
"`SpaceBetween` stretches when there's only one constraint",
|
||||
&[Max(20)],
|
||||
),
|
||||
("", &[Min(20), Max(20), Length(20), Fixed(20)]),
|
||||
("`Proportional(u16)` always fills up space in every `Flex` layout", &[Proportional(0), Proportional(0)]),
|
||||
(
|
||||
"`Proportional(1)` can be to scale with respect to other `Proportional(2)`",
|
||||
&[Proportional(1), Proportional(2)],
|
||||
),
|
||||
(
|
||||
"`Proportional(0)` collapses if there are other non-zero `Proportional(_)`\nconstraints. e.g. `[Proportional(0), Proportional(0), Proportional(1)]`:",
|
||||
&[
|
||||
Proportional(0),
|
||||
Proportional(0),
|
||||
Proportional(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]
|
||||
StretchLast,
|
||||
Stretch,
|
||||
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()?;
|
||||
|
||||
// Each line in the example is a layout
|
||||
// so 13 examples * 7 = 91 currently
|
||||
// Plus additional layout for tabs ...
|
||||
Layout::init_cache(120);
|
||||
|
||||
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([Fixed(3), Fixed(1), Proportional(0)]);
|
||||
let [tabs, axis, demo] = area.split(&layout);
|
||||
self.tabs().render(tabs, buf);
|
||||
let scroll_needed = self.render_demo(demo, buf);
|
||||
let axis_width = if scroll_needed {
|
||||
axis.width - 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 - 2; // we want to `<` and `>` at the ends
|
||||
let width_bar = format!("<{label:-^bar_width$}>");
|
||||
Paragraph::new(width_bar.dark_gray()).alignment(Alignment::Center)
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
StretchLast => ORANGE.c400,
|
||||
Stretch => ORANGE.c300,
|
||||
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::StretchLast => self.render_examples(area, buf, Flex::StretchLast, spacing),
|
||||
SelectedTab::Stretch => self.render_examples(area, buf, Flex::Stretch, 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([Fixed(title_height), Proportional(0)]);
|
||||
let [title, illustrations] = area.split(&layout);
|
||||
let blocks = Layout::horizontal(&self.constraints)
|
||||
.flex(self.flex)
|
||||
.spacing(self.spacing)
|
||||
.split(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Example {
|
||||
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)
|
||||
.alignment(Alignment::Center)
|
||||
.block(block)
|
||||
}
|
||||
}
|
||||
|
||||
fn color_for_constraint(constraint: Constraint) -> Color {
|
||||
use tailwind::*;
|
||||
match constraint {
|
||||
Constraint::Fixed(_) => RED.c900,
|
||||
Constraint::Min(_) => BLUE.c900,
|
||||
Constraint::Max(_) => BLUE.c800,
|
||||
Constraint::Length(_) => SLATE.c700,
|
||||
Constraint::Percentage(_) => SLATE.c800,
|
||||
Constraint::Ratio(_, _) => SLATE.c900,
|
||||
Constraint::Proportional(_) => 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
|
||||
}
|
||||
}
|
||||
17
examples/flex.tape
Normal file
17
examples/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
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::{
|
||||
io::{self, stdout, Stdout},
|
||||
rc::Rc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
@@ -75,7 +74,7 @@ impl App {
|
||||
fn draw(&mut self) -> Result<()> {
|
||||
self.term.draw(|frame| {
|
||||
let state = self.state;
|
||||
let layout = Self::equal_layout(frame);
|
||||
let layout = Layout::vertical([Constraint::Ratio(1, 4); 4]).split(frame.size());
|
||||
Self::render_gauge1(state.progress1, frame, layout[0]);
|
||||
Self::render_gauge2(state.progress2, frame, layout[1]);
|
||||
Self::render_gauge3(state.progress3, frame, layout[2]);
|
||||
@@ -97,18 +96,6 @@ 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 render_gauge1(progress: u16, frame: &mut Frame, area: Rect) {
|
||||
let title = Self::title_block("Gauge with percentage progress");
|
||||
let gauge = Gauge::default()
|
||||
|
||||
@@ -215,15 +215,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] = area.split(&vertical);
|
||||
let [list_area, gauge_area] = main.split(&horizontal);
|
||||
|
||||
// total progress
|
||||
let done = NUM_DOWNLOADS - downloads.pending.len() - downloads.in_progress.len();
|
||||
@@ -231,12 +231,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 +254,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,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -48,14 +48,12 @@ 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, _] = frame.size().split(&vertical);
|
||||
|
||||
// title
|
||||
frame.render_widget(
|
||||
@@ -66,38 +64,34 @@ fn ui(frame: &mut Frame) {
|
||||
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 +176,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 +190,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] = area.split(&horizontal);
|
||||
frame.render_widget(red, r);
|
||||
frame.render_widget(blue, b);
|
||||
frame.render_widget(green, g);
|
||||
}
|
||||
|
||||
fn constraint_label(constraint: Constraint) -> String {
|
||||
@@ -214,6 +203,8 @@ fn constraint_label(constraint: Constraint) -> String {
|
||||
Min(n) => format!("{n}"),
|
||||
Max(n) => format!("{n}"),
|
||||
Percentage(n) => format!("{n}"),
|
||||
Proportional(n) => format!("{n}"),
|
||||
Fixed(n) => format!("{n}"),
|
||||
Ratio(a, b) => format!("{a}:{b}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ use ratatui::{prelude::*, widgets::*};
|
||||
struct StatefulList<T> {
|
||||
state: ListState,
|
||||
items: Vec<T>,
|
||||
last_selected: Option<usize>,
|
||||
}
|
||||
|
||||
impl<T> StatefulList<T> {
|
||||
@@ -21,6 +22,7 @@ impl<T> StatefulList<T> {
|
||||
StatefulList {
|
||||
state: ListState::default(),
|
||||
items,
|
||||
last_selected: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +35,7 @@ impl<T> StatefulList<T> {
|
||||
i + 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
None => self.last_selected.unwrap_or(0),
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
@@ -47,13 +49,16 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,9 +186,9 @@ fn run_app<B: Backend>(
|
||||
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(),
|
||||
KeyCode::Left | KeyCode::Char('h') => app.items.unselect(),
|
||||
KeyCode::Down | KeyCode::Char('j') => app.items.next(),
|
||||
KeyCode::Up | KeyCode::Char('k') => app.items.previous(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -198,10 +203,8 @@ fn run_app<B: Backend>(
|
||||
|
||||
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());
|
||||
let horizontal = Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]);
|
||||
let [item_list_area, event_list_area] = f.size().split(&horizontal);
|
||||
|
||||
// Iterate through all elements in the `items` app and append some debug text to it.
|
||||
let items: Vec<ListItem> = app
|
||||
@@ -209,7 +212,7 @@ fn ui(f: &mut Frame, app: &mut App) {
|
||||
.items
|
||||
.iter()
|
||||
.map(|i| {
|
||||
let mut lines = vec![Line::from(i.0)];
|
||||
let mut lines = vec![Line::from(i.0.bold()).alignment(Alignment::Center)];
|
||||
for _ in 0..i.1 {
|
||||
lines.push(
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit."
|
||||
@@ -232,7 +235,7 @@ fn ui(f: &mut Frame, app: &mut App) {
|
||||
.highlight_symbol(">> ");
|
||||
|
||||
// We can now render the item list
|
||||
f.render_stateful_widget(items, chunks[0], &mut app.items.state);
|
||||
f.render_stateful_widget(items, item_list_area, &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.
|
||||
@@ -264,7 +267,7 @@ fn ui(f: &mut Frame, app: &mut App) {
|
||||
// 3. Add a spacer line
|
||||
// 4. Add the actual event
|
||||
ListItem::new(vec![
|
||||
Line::from("-".repeat(chunks[1].width as usize)),
|
||||
Line::from("-".repeat(event_list_area.width as usize)),
|
||||
header,
|
||||
Line::from(""),
|
||||
log,
|
||||
@@ -273,6 +276,6 @@ fn ui(f: &mut Frame, app: &mut App) {
|
||||
.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]);
|
||||
.direction(ListDirection::BottomToTop);
|
||||
f.render_widget(events_list, event_list_area);
|
||||
}
|
||||
|
||||
@@ -44,24 +44,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] = frame.size().split(&vertical);
|
||||
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()
|
||||
})
|
||||
|
||||
@@ -90,15 +90,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,20 +121,20 @@ 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)
|
||||
.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))
|
||||
@@ -150,5 +142,5 @@ fn ui(f: &mut Frame, app: &App) {
|
||||
.alignment(Alignment::Center)
|
||||
.wrap(Wrap { trim: true })
|
||||
.scroll((app.scroll, 0));
|
||||
f.render_widget(paragraph, chunks[3]);
|
||||
f.render_widget(paragraph, layout[3]);
|
||||
}
|
||||
|
||||
@@ -62,11 +62,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] = area.split(&vertical);
|
||||
|
||||
let text = if app.show_popup {
|
||||
"Press p to close the popup"
|
||||
@@ -76,17 +75,17 @@ fn ui(f: &mut Frame, app: &App) {
|
||||
let paragraph = Paragraph::new(text.slow_blink())
|
||||
.alignment(Alignment::Center)
|
||||
.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 +93,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]
|
||||
}
|
||||
|
||||
71
examples/ratatui-logo.rs
Normal file
71
examples/ratatui-logo.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
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(())
|
||||
}
|
||||
12
examples/ratatui-logo.tape
Normal file
12
examples/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
|
||||
@@ -62,22 +62,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 +100,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 +140,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]);
|
||||
|
||||
|
||||
@@ -125,14 +125,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()
|
||||
|
||||
@@ -5,38 +5,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 +104,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 +119,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 +195,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 +210,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))
|
||||
.alignment(Alignment::Center)
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -69,8 +69,8 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
|
||||
if key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Right => app.next(),
|
||||
KeyCode::Left => app.previous(),
|
||||
KeyCode::Right | KeyCode::Char('l') => app.next(),
|
||||
KeyCode::Left | KeyCode::Char('h') => app.previous(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -79,28 +79,25 @@ 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 chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Length(3), Constraint::Min(0)])
|
||||
.split(size);
|
||||
let area = f.size();
|
||||
let vertical = Layout::vertical([Constraint::Length(3), Constraint::Min(0)]);
|
||||
let [tabs_area, inner_area] = area.split(&vertical);
|
||||
|
||||
let block = Block::default().on_white().black();
|
||||
f.render_widget(block, size);
|
||||
let titles = app
|
||||
f.render_widget(block, area);
|
||||
let tabs = 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)
|
||||
.collect::<Tabs>()
|
||||
.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]);
|
||||
f.render_widget(tabs, tabs_area);
|
||||
let inner = match app.index {
|
||||
0 => Block::default().title("Inner 0").borders(Borders::ALL),
|
||||
1 => Block::default().title("Inner 1").borders(Borders::ALL),
|
||||
@@ -108,5 +105,5 @@ fn ui(f: &mut Frame, app: &App) {
|
||||
3 => Block::default().title("Inner 3").borders(Borders::ALL),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
f.render_widget(inner, chunks[1]);
|
||||
f.render_widget(inner, inner_area);
|
||||
}
|
||||
|
||||
@@ -172,14 +172,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] = f.size().split(&vertical);
|
||||
|
||||
let (msg, style) = match app.input_mode {
|
||||
InputMode::Normal => (
|
||||
@@ -203,10 +201,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 +211,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 +223,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 +241,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);
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
@@ -228,7 +229,7 @@ pub trait Backend {
|
||||
/// [`get_cursor`]: Backend::get_cursor
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()>;
|
||||
|
||||
/// Clears the whole terminal scree
|
||||
/// Clears the whole terminal screen
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
|
||||
@@ -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.
|
||||
@@ -161,7 +162,7 @@ where
|
||||
underline_color = cell.underline_color;
|
||||
}
|
||||
|
||||
queue!(self.writer, Print(&cell.symbol))?;
|
||||
queue!(self.writer, Print(cell.symbol()))?;
|
||||
}
|
||||
|
||||
#[cfg(feature = "underline-color")]
|
||||
@@ -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};
|
||||
///
|
||||
@@ -179,7 +182,7 @@ where
|
||||
write!(string, "{}", Bg(cell.bg)).unwrap();
|
||||
bg = cell.bg;
|
||||
}
|
||||
string.push_str(&cell.symbol);
|
||||
string.push_str(cell.symbol());
|
||||
}
|
||||
write!(
|
||||
self.writer,
|
||||
@@ -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>,
|
||||
}
|
||||
@@ -176,7 +176,7 @@ impl Backend for TermwizBackend {
|
||||
},
|
||||
)));
|
||||
|
||||
self.buffered_terminal.add_change(&cell.symbol);
|
||||
self.buffered_terminal.add_change(cell.symbol());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use std::{
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{
|
||||
backend::{Backend, WindowSize},
|
||||
backend::{Backend, ClearType, WindowSize},
|
||||
buffer::{Buffer, Cell},
|
||||
layout::{Rect, Size},
|
||||
};
|
||||
@@ -56,11 +56,11 @@ fn buffer_view(buffer: &Buffer) -> String {
|
||||
view.push('"');
|
||||
for (x, c) in cells.iter().enumerate() {
|
||||
if skip == 0 {
|
||||
view.push_str(&c.symbol);
|
||||
view.push_str(c.symbol());
|
||||
} else {
|
||||
overwritten.push((x, &c.symbol));
|
||||
overwritten.push((x, c.symbol()));
|
||||
}
|
||||
skip = std::cmp::max(skip, c.symbol.width()).saturating_sub(1);
|
||||
skip = std::cmp::max(skip, c.symbol().width()).saturating_sub(1);
|
||||
}
|
||||
view.push('"');
|
||||
if !overwritten.is_empty() {
|
||||
@@ -179,6 +179,71 @@ impl Backend for TestBackend {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear_region(&mut self, clear_type: super::ClearType) -> io::Result<()> {
|
||||
match clear_type {
|
||||
ClearType::All => self.clear()?,
|
||||
ClearType::AfterCursor => {
|
||||
let index = self.buffer.index_of(self.pos.0, self.pos.1) + 1;
|
||||
self.buffer.content[index..].fill(Cell::default());
|
||||
}
|
||||
ClearType::BeforeCursor => {
|
||||
let index = self.buffer.index_of(self.pos.0, self.pos.1);
|
||||
self.buffer.content[..index].fill(Cell::default());
|
||||
}
|
||||
ClearType::CurrentLine => {
|
||||
let line_start_index = self.buffer.index_of(0, self.pos.1);
|
||||
let line_end_index = self.buffer.index_of(self.width - 1, self.pos.1);
|
||||
self.buffer.content[line_start_index..=line_end_index].fill(Cell::default());
|
||||
}
|
||||
ClearType::UntilNewLine => {
|
||||
let index = self.buffer.index_of(self.pos.0, self.pos.1);
|
||||
let line_end_index = self.buffer.index_of(self.width - 1, self.pos.1);
|
||||
self.buffer.content[index..=line_end_index].fill(Cell::default());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Inserts n line breaks at the current cursor position.
|
||||
///
|
||||
/// After the insertion, the cursor x position will be incremented by 1 (unless it's already
|
||||
/// at the end of line). This is a common behaviour of terminals in raw mode.
|
||||
///
|
||||
/// If the number of lines to append is fewer than the number of lines in the buffer after the
|
||||
/// cursor y position then the cursor is moved down by n rows.
|
||||
///
|
||||
/// If the number of lines to append is greater than the number of lines in the buffer after
|
||||
/// the cursor y position then that number of empty lines (at most the buffer's height in this
|
||||
/// case but this limit is instead replaced with scrolling in most backend implementations) will
|
||||
/// be added after the current position and the cursor will be moved to the last row.
|
||||
fn append_lines(&mut self, n: u16) -> io::Result<()> {
|
||||
let (cur_x, cur_y) = self.get_cursor()?;
|
||||
|
||||
// the next column ensuring that we don't go past the last column
|
||||
let new_cursor_x = cur_x.saturating_add(1).min(self.width.saturating_sub(1));
|
||||
|
||||
let max_y = self.height.saturating_sub(1);
|
||||
let lines_after_cursor = max_y.saturating_sub(cur_y);
|
||||
if n > lines_after_cursor {
|
||||
let rotate_by = n.saturating_sub(lines_after_cursor).min(max_y);
|
||||
|
||||
if rotate_by == self.height - 1 {
|
||||
self.clear()?;
|
||||
}
|
||||
|
||||
self.set_cursor(0, rotate_by)?;
|
||||
self.clear_region(ClearType::BeforeCursor)?;
|
||||
self.buffer
|
||||
.content
|
||||
.rotate_left((self.width * rotate_by).into());
|
||||
}
|
||||
|
||||
let new_cursor_y = cur_y.saturating_add(n).min(max_y);
|
||||
self.set_cursor(new_cursor_x, new_cursor_y)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn size(&self) -> Result<Rect, io::Error> {
|
||||
Ok(Rect::new(0, 0, self.width, self.height))
|
||||
}
|
||||
@@ -310,13 +375,299 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn clear() {
|
||||
let mut backend = TestBackend::new(10, 2);
|
||||
let mut backend = TestBackend::new(10, 4);
|
||||
let mut cell = Cell::default();
|
||||
cell.set_symbol("a");
|
||||
backend.draw([(0, 0, &cell)].into_iter()).unwrap();
|
||||
backend.draw([(0, 1, &cell)].into_iter()).unwrap();
|
||||
backend.clear().unwrap();
|
||||
backend.assert_buffer(&Buffer::with_lines(vec![" "; 2]));
|
||||
backend.assert_buffer(&Buffer::with_lines(vec![
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_region_all() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines(vec![
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
]);
|
||||
|
||||
backend.clear_region(ClearType::All).unwrap();
|
||||
backend.assert_buffer(&Buffer::with_lines(vec![
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_region_after_cursor() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines(vec![
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
]);
|
||||
|
||||
backend.set_cursor(3, 2).unwrap();
|
||||
backend.clear_region(ClearType::AfterCursor).unwrap();
|
||||
backend.assert_buffer(&Buffer::with_lines(vec![
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaa ",
|
||||
" ",
|
||||
" ",
|
||||
]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_region_before_cursor() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines(vec![
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
]);
|
||||
|
||||
backend.set_cursor(5, 3).unwrap();
|
||||
backend.clear_region(ClearType::BeforeCursor).unwrap();
|
||||
backend.assert_buffer(&Buffer::with_lines(vec![
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" aaaaa",
|
||||
"aaaaaaaaaa",
|
||||
]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_region_current_line() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines(vec![
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
]);
|
||||
|
||||
backend.set_cursor(3, 1).unwrap();
|
||||
backend.clear_region(ClearType::CurrentLine).unwrap();
|
||||
backend.assert_buffer(&Buffer::with_lines(vec![
|
||||
"aaaaaaaaaa",
|
||||
" ",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_region_until_new_line() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines(vec![
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
]);
|
||||
|
||||
backend.set_cursor(3, 0).unwrap();
|
||||
backend.clear_region(ClearType::UntilNewLine).unwrap();
|
||||
backend.assert_buffer(&Buffer::with_lines(vec![
|
||||
"aaa ",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_lines_not_at_last_line() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines(vec![
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
"dddddddddd",
|
||||
"eeeeeeeeee",
|
||||
]);
|
||||
|
||||
backend.set_cursor(0, 0).unwrap();
|
||||
|
||||
// If the cursor is not at the last line in the terminal the addition of a
|
||||
// newline simply moves the cursor down and to the right
|
||||
|
||||
backend.append_lines(1).unwrap();
|
||||
assert_eq!(backend.get_cursor().unwrap(), (1, 1));
|
||||
|
||||
backend.append_lines(1).unwrap();
|
||||
assert_eq!(backend.get_cursor().unwrap(), (2, 2));
|
||||
|
||||
backend.append_lines(1).unwrap();
|
||||
assert_eq!(backend.get_cursor().unwrap(), (3, 3));
|
||||
|
||||
backend.append_lines(1).unwrap();
|
||||
assert_eq!(backend.get_cursor().unwrap(), (4, 4));
|
||||
|
||||
// As such the buffer should remain unchanged
|
||||
backend.assert_buffer(&Buffer::with_lines(vec![
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
"dddddddddd",
|
||||
"eeeeeeeeee",
|
||||
]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_lines_at_last_line() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines(vec![
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
"dddddddddd",
|
||||
"eeeeeeeeee",
|
||||
]);
|
||||
|
||||
// If the cursor is at the last line in the terminal the addition of a
|
||||
// newline will scroll the contents of the buffer
|
||||
backend.set_cursor(0, 4).unwrap();
|
||||
|
||||
backend.append_lines(1).unwrap();
|
||||
|
||||
backend.buffer = Buffer::with_lines(vec![
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
"dddddddddd",
|
||||
"eeeeeeeeee",
|
||||
" ",
|
||||
]);
|
||||
|
||||
// It also moves the cursor to the right, as is common of the behaviour of
|
||||
// terminals in raw-mode
|
||||
assert_eq!(backend.get_cursor().unwrap(), (1, 4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_multiple_lines_not_at_last_line() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines(vec![
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
"dddddddddd",
|
||||
"eeeeeeeeee",
|
||||
]);
|
||||
|
||||
backend.set_cursor(0, 0).unwrap();
|
||||
|
||||
// If the cursor is not at the last line in the terminal the addition of multiple
|
||||
// newlines simply moves the cursor n lines down and to the right by 1
|
||||
|
||||
backend.append_lines(4).unwrap();
|
||||
assert_eq!(backend.get_cursor().unwrap(), (1, 4));
|
||||
|
||||
// As such the buffer should remain unchanged
|
||||
backend.assert_buffer(&Buffer::with_lines(vec![
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
"dddddddddd",
|
||||
"eeeeeeeeee",
|
||||
]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_multiple_lines_past_last_line() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines(vec![
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
"dddddddddd",
|
||||
"eeeeeeeeee",
|
||||
]);
|
||||
|
||||
backend.set_cursor(0, 3).unwrap();
|
||||
|
||||
backend.append_lines(3).unwrap();
|
||||
assert_eq!(backend.get_cursor().unwrap(), (1, 4));
|
||||
|
||||
backend.assert_buffer(&Buffer::with_lines(vec![
|
||||
"cccccccccc",
|
||||
"dddddddddd",
|
||||
"eeeeeeeeee",
|
||||
" ",
|
||||
" ",
|
||||
]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_multiple_lines_where_cursor_at_end_appends_height_lines() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines(vec![
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
"dddddddddd",
|
||||
"eeeeeeeeee",
|
||||
]);
|
||||
|
||||
backend.set_cursor(0, 4).unwrap();
|
||||
|
||||
backend.append_lines(5).unwrap();
|
||||
assert_eq!(backend.get_cursor().unwrap(), (1, 4));
|
||||
|
||||
backend.assert_buffer(&Buffer::with_lines(vec![
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_multiple_lines_where_cursor_appends_height_lines() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines(vec![
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
"dddddddddd",
|
||||
"eeeeeeeeee",
|
||||
]);
|
||||
|
||||
backend.set_cursor(0, 0).unwrap();
|
||||
|
||||
backend.append_lines(5).unwrap();
|
||||
assert_eq!(backend.get_cursor().unwrap(), (1, 4));
|
||||
|
||||
backend.assert_buffer(&Buffer::with_lines(vec![
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
"dddddddddd",
|
||||
"eeeeeeeeee",
|
||||
" ",
|
||||
]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
1052
src/buffer.rs
1052
src/buffer.rs
File diff suppressed because it is too large
Load Diff
82
src/buffer/assert.rs
Normal file
82
src/buffer/assert.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
/// Assert that two buffers are equal by comparing their areas and content.
|
||||
///
|
||||
/// On panic, displays the areas or the content and a diff of the contents.
|
||||
#[macro_export]
|
||||
macro_rules! assert_buffer_eq {
|
||||
($actual_expr:expr, $expected_expr:expr) => {
|
||||
match (&$actual_expr, &$expected_expr) {
|
||||
(actual, expected) => {
|
||||
if actual.area != expected.area {
|
||||
panic!(
|
||||
indoc::indoc!(
|
||||
"
|
||||
buffer areas not equal
|
||||
expected: {:?}
|
||||
actual: {:?}"
|
||||
),
|
||||
expected, actual
|
||||
);
|
||||
}
|
||||
let diff = expected.diff(&actual);
|
||||
if !diff.is_empty() {
|
||||
let nice_diff = diff
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, (x, y, cell))| {
|
||||
let expected_cell = expected.get(*x, *y);
|
||||
indoc::formatdoc! {"
|
||||
{i}: at ({x}, {y})
|
||||
expected: {expected_cell:?}
|
||||
actual: {cell:?}
|
||||
"}
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n");
|
||||
panic!(
|
||||
indoc::indoc!(
|
||||
"
|
||||
buffer contents not equal
|
||||
expected: {:?}
|
||||
actual: {:?}
|
||||
diff:
|
||||
{}"
|
||||
),
|
||||
expected, actual, nice_diff
|
||||
);
|
||||
}
|
||||
// shouldn't get here, but this guards against future behavior
|
||||
// that changes equality but not area or content
|
||||
assert_eq!(actual, expected, "buffers not equal");
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::prelude::*;
|
||||
|
||||
#[test]
|
||||
fn assert_buffer_eq_does_not_panic_on_equal_buffers() {
|
||||
let buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
|
||||
let other_buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
|
||||
assert_buffer_eq!(buffer, other_buffer);
|
||||
}
|
||||
|
||||
#[should_panic]
|
||||
#[test]
|
||||
fn assert_buffer_eq_panics_on_unequal_area() {
|
||||
let buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
|
||||
let other_buffer = Buffer::empty(Rect::new(0, 0, 6, 1));
|
||||
assert_buffer_eq!(buffer, other_buffer);
|
||||
}
|
||||
|
||||
#[should_panic]
|
||||
#[test]
|
||||
fn assert_buffer_eq_panics_on_unequal_style() {
|
||||
let buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
|
||||
let mut other_buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
|
||||
other_buffer.set_string(0, 0, " ", Style::default().fg(Color::Red));
|
||||
assert_buffer_eq!(buffer, other_buffer);
|
||||
}
|
||||
}
|
||||
891
src/buffer/buffer.rs
Normal file
891
src/buffer/buffer.rs
Normal file
@@ -0,0 +1,891 @@
|
||||
use std::{
|
||||
cmp::min,
|
||||
fmt::{Debug, Formatter, Result},
|
||||
};
|
||||
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{buffer::Cell, prelude::*};
|
||||
|
||||
/// A buffer that maps to the desired content of the terminal after the draw call
|
||||
///
|
||||
/// No widget in the library interacts directly with the terminal. Instead each of them is required
|
||||
/// to draw their state to an intermediate buffer. It is basically a grid where each cell contains
|
||||
/// a grapheme, a foreground color and a background color. This grid will then be used to output
|
||||
/// the appropriate escape sequences and characters to draw the UI as the user has defined it.
|
||||
///
|
||||
/// # Examples:
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::{buffer::Cell, prelude::*};
|
||||
///
|
||||
/// let mut buf = Buffer::empty(Rect {
|
||||
/// x: 0,
|
||||
/// y: 0,
|
||||
/// width: 10,
|
||||
/// height: 5,
|
||||
/// });
|
||||
/// buf.get_mut(0, 2).set_symbol("x");
|
||||
/// assert_eq!(buf.get(0, 2).symbol(), "x");
|
||||
///
|
||||
/// buf.set_string(
|
||||
/// 3,
|
||||
/// 0,
|
||||
/// "string",
|
||||
/// Style::default().fg(Color::Red).bg(Color::White),
|
||||
/// );
|
||||
/// let cell = buf.get(5, 0);
|
||||
/// assert_eq!(cell.symbol(), "r");
|
||||
/// assert_eq!(cell.fg, Color::Red);
|
||||
/// assert_eq!(cell.bg, Color::White);
|
||||
///
|
||||
/// buf.get_mut(5, 0).set_char('x');
|
||||
/// assert_eq!(buf.get(5, 0).symbol(), "x");
|
||||
/// ```
|
||||
#[derive(Default, Clone, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Buffer {
|
||||
/// The area represented by this buffer
|
||||
pub area: Rect,
|
||||
/// The content of the buffer. The length of this Vec should always be equal to area.width *
|
||||
/// area.height
|
||||
pub content: Vec<Cell>,
|
||||
}
|
||||
|
||||
impl Buffer {
|
||||
/// Returns a Buffer with all cells set to the default one
|
||||
pub fn empty(area: Rect) -> Buffer {
|
||||
let cell = Cell::default();
|
||||
Buffer::filled(area, &cell)
|
||||
}
|
||||
|
||||
/// Returns a Buffer with all cells initialized with the attributes of the given Cell
|
||||
pub fn filled(area: Rect, cell: &Cell) -> Buffer {
|
||||
let size = area.area() as usize;
|
||||
let mut content = Vec::with_capacity(size);
|
||||
for _ in 0..size {
|
||||
content.push(cell.clone());
|
||||
}
|
||||
Buffer { area, content }
|
||||
}
|
||||
|
||||
/// Returns a Buffer containing the given lines
|
||||
pub fn with_lines<'a, S>(lines: Vec<S>) -> Buffer
|
||||
where
|
||||
S: Into<Line<'a>>,
|
||||
{
|
||||
let lines = lines.into_iter().map(Into::into).collect::<Vec<_>>();
|
||||
let height = lines.len() as u16;
|
||||
let width = lines.iter().map(Line::width).max().unwrap_or_default() as u16;
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, width, height));
|
||||
for (y, line) in lines.iter().enumerate() {
|
||||
buffer.set_line(0, y as u16, line, width);
|
||||
}
|
||||
buffer
|
||||
}
|
||||
|
||||
/// Returns the content of the buffer as a slice
|
||||
pub fn content(&self) -> &[Cell] {
|
||||
&self.content
|
||||
}
|
||||
|
||||
/// Returns the area covered by this buffer
|
||||
pub fn area(&self) -> &Rect {
|
||||
&self.area
|
||||
}
|
||||
|
||||
/// Returns a reference to Cell at the given coordinates
|
||||
pub fn get(&self, x: u16, y: u16) -> &Cell {
|
||||
let i = self.index_of(x, y);
|
||||
&self.content[i]
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to Cell at the given coordinates
|
||||
pub fn get_mut(&mut self, x: u16, y: u16) -> &mut Cell {
|
||||
let i = self.index_of(x, y);
|
||||
&mut self.content[i]
|
||||
}
|
||||
|
||||
/// Returns the index in the `Vec<Cell>` for the given global (x, y) coordinates.
|
||||
///
|
||||
/// Global coordinates are offset by the Buffer's area offset (`x`/`y`).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let rect = Rect::new(200, 100, 10, 10);
|
||||
/// let buffer = Buffer::empty(rect);
|
||||
/// // Global coordinates to the top corner of this buffer's area
|
||||
/// assert_eq!(buffer.index_of(200, 100), 0);
|
||||
/// ```
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics when given an coordinate that is outside of this Buffer's area.
|
||||
///
|
||||
/// ```should_panic
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let rect = Rect::new(200, 100, 10, 10);
|
||||
/// let buffer = Buffer::empty(rect);
|
||||
/// // Top coordinate is outside of the buffer in global coordinate space, as the Buffer's area
|
||||
/// // starts at (200, 100).
|
||||
/// buffer.index_of(0, 0); // Panics
|
||||
/// ```
|
||||
pub fn index_of(&self, x: u16, y: u16) -> usize {
|
||||
debug_assert!(
|
||||
x >= self.area.left()
|
||||
&& x < self.area.right()
|
||||
&& y >= self.area.top()
|
||||
&& y < self.area.bottom(),
|
||||
"Trying to access position outside the buffer: x={x}, y={y}, area={:?}",
|
||||
self.area
|
||||
);
|
||||
((y - self.area.y) * self.area.width + (x - self.area.x)) as usize
|
||||
}
|
||||
|
||||
/// Returns the (global) coordinates of a cell given its index
|
||||
///
|
||||
/// Global coordinates are offset by the Buffer's area offset (`x`/`y`).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let rect = Rect::new(200, 100, 10, 10);
|
||||
/// let buffer = Buffer::empty(rect);
|
||||
/// assert_eq!(buffer.pos_of(0), (200, 100));
|
||||
/// assert_eq!(buffer.pos_of(14), (204, 101));
|
||||
/// ```
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics when given an index that is outside the Buffer's content.
|
||||
///
|
||||
/// ```should_panic
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let rect = Rect::new(0, 0, 10, 10); // 100 cells in total
|
||||
/// let buffer = Buffer::empty(rect);
|
||||
/// // Index 100 is the 101th cell, which lies outside of the area of this Buffer.
|
||||
/// buffer.pos_of(100); // Panics
|
||||
/// ```
|
||||
pub fn pos_of(&self, i: usize) -> (u16, u16) {
|
||||
debug_assert!(
|
||||
i < self.content.len(),
|
||||
"Trying to get the coords of a cell outside the buffer: i={i} len={}",
|
||||
self.content.len()
|
||||
);
|
||||
(
|
||||
self.area.x + (i as u16) % self.area.width,
|
||||
self.area.y + (i as u16) / self.area.width,
|
||||
)
|
||||
}
|
||||
|
||||
/// Print a string, starting at the position (x, y)
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
pub fn set_string<T, S>(&mut self, x: u16, y: u16, string: T, style: S)
|
||||
where
|
||||
T: AsRef<str>,
|
||||
S: Into<Style>,
|
||||
{
|
||||
self.set_stringn(x, y, string, usize::MAX, style.into());
|
||||
}
|
||||
|
||||
/// Print at most the first n characters of a string if enough space is available
|
||||
/// until the end of the line
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
pub fn set_stringn<T, S>(
|
||||
&mut self,
|
||||
x: u16,
|
||||
y: u16,
|
||||
string: T,
|
||||
width: usize,
|
||||
style: S,
|
||||
) -> (u16, u16)
|
||||
where
|
||||
T: AsRef<str>,
|
||||
S: Into<Style>,
|
||||
{
|
||||
let style = style.into();
|
||||
let mut index = self.index_of(x, y);
|
||||
let mut x_offset = x as usize;
|
||||
let graphemes = UnicodeSegmentation::graphemes(string.as_ref(), true);
|
||||
let max_offset = min(self.area.right() as usize, width.saturating_add(x as usize));
|
||||
for s in graphemes {
|
||||
let width = s.width();
|
||||
if width == 0 {
|
||||
continue;
|
||||
}
|
||||
// `x_offset + width > max_offset` could be integer overflow on 32-bit machines if we
|
||||
// change dimensions to usize or u32 and someone resizes the terminal to 1x2^32.
|
||||
if width > max_offset.saturating_sub(x_offset) {
|
||||
break;
|
||||
}
|
||||
|
||||
self.content[index].set_symbol(s);
|
||||
self.content[index].set_style(style);
|
||||
// Reset following cells if multi-width (they would be hidden by the grapheme),
|
||||
for i in index + 1..index + width {
|
||||
self.content[i].reset();
|
||||
}
|
||||
index += width;
|
||||
x_offset += width;
|
||||
}
|
||||
(x_offset as u16, y)
|
||||
}
|
||||
|
||||
/// Print a line, starting at the position (x, y)
|
||||
pub fn set_line(&mut self, x: u16, y: u16, line: &Line<'_>, width: u16) -> (u16, u16) {
|
||||
let mut remaining_width = width;
|
||||
let mut x = x;
|
||||
for span in &line.spans {
|
||||
if remaining_width == 0 {
|
||||
break;
|
||||
}
|
||||
let pos = self.set_stringn(
|
||||
x,
|
||||
y,
|
||||
span.content.as_ref(),
|
||||
remaining_width as usize,
|
||||
span.style,
|
||||
);
|
||||
let w = pos.0.saturating_sub(x);
|
||||
x = pos.0;
|
||||
remaining_width = remaining_width.saturating_sub(w);
|
||||
}
|
||||
(x, y)
|
||||
}
|
||||
|
||||
/// Print a span, starting at the position (x, y)
|
||||
pub fn set_span(&mut self, x: u16, y: u16, span: &Span<'_>, width: u16) -> (u16, u16) {
|
||||
self.set_stringn(x, y, span.content.as_ref(), width as usize, span.style)
|
||||
}
|
||||
|
||||
/// Set the style of all cells in the given area.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
pub fn set_style<S: Into<Style>>(&mut self, area: Rect, style: S) {
|
||||
let style = style.into();
|
||||
let area = self.area.intersection(area);
|
||||
for y in area.top()..area.bottom() {
|
||||
for x in area.left()..area.right() {
|
||||
self.get_mut(x, y).set_style(style);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Resize the buffer so that the mapped area matches the given area and that the buffer
|
||||
/// length is equal to area.width * area.height
|
||||
pub fn resize(&mut self, area: Rect) {
|
||||
let length = area.area() as usize;
|
||||
if self.content.len() > length {
|
||||
self.content.truncate(length);
|
||||
} else {
|
||||
self.content.resize(length, Cell::default());
|
||||
}
|
||||
self.area = area;
|
||||
}
|
||||
|
||||
/// Reset all cells in the buffer
|
||||
pub fn reset(&mut self) {
|
||||
for c in &mut self.content {
|
||||
c.reset();
|
||||
}
|
||||
}
|
||||
|
||||
/// Merge an other buffer into this one
|
||||
pub fn merge(&mut self, other: &Buffer) {
|
||||
let area = self.area.union(other.area);
|
||||
let cell = Cell::default();
|
||||
self.content.resize(area.area() as usize, cell.clone());
|
||||
|
||||
// Move original content to the appropriate space
|
||||
let size = self.area.area() as usize;
|
||||
for i in (0..size).rev() {
|
||||
let (x, y) = self.pos_of(i);
|
||||
// New index in content
|
||||
let k = ((y - area.y) * area.width + x - area.x) as usize;
|
||||
if i != k {
|
||||
self.content[k] = self.content[i].clone();
|
||||
self.content[i] = cell.clone();
|
||||
}
|
||||
}
|
||||
|
||||
// Push content of the other buffer into this one (may erase previous
|
||||
// data)
|
||||
let size = other.area.area() as usize;
|
||||
for i in 0..size {
|
||||
let (x, y) = other.pos_of(i);
|
||||
// New index in content
|
||||
let k = ((y - area.y) * area.width + x - area.x) as usize;
|
||||
self.content[k] = other.content[i].clone();
|
||||
}
|
||||
self.area = area;
|
||||
}
|
||||
|
||||
/// Builds a minimal sequence of coordinates and Cells necessary to update the UI from
|
||||
/// self to other.
|
||||
///
|
||||
/// We're assuming that buffers are well-formed, that is no double-width cell is followed by
|
||||
/// a non-blank cell.
|
||||
///
|
||||
/// # Multi-width characters handling:
|
||||
///
|
||||
/// ```text
|
||||
/// (Index:) `01`
|
||||
/// Prev: `コ`
|
||||
/// Next: `aa`
|
||||
/// Updates: `0: a, 1: a'
|
||||
/// ```
|
||||
///
|
||||
/// ```text
|
||||
/// (Index:) `01`
|
||||
/// Prev: `a `
|
||||
/// Next: `コ`
|
||||
/// Updates: `0: コ` (double width symbol at index 0 - skip index 1)
|
||||
/// ```
|
||||
///
|
||||
/// ```text
|
||||
/// (Index:) `012`
|
||||
/// Prev: `aaa`
|
||||
/// Next: `aコ`
|
||||
/// Updates: `0: a, 1: コ` (double width symbol at index 1 - skip index 2)
|
||||
/// ```
|
||||
pub fn diff<'a>(&self, other: &'a Buffer) -> Vec<(u16, u16, &'a Cell)> {
|
||||
let previous_buffer = &self.content;
|
||||
let next_buffer = &other.content;
|
||||
|
||||
let mut updates: Vec<(u16, u16, &Cell)> = vec![];
|
||||
// Cells invalidated by drawing/replacing preceding multi-width characters:
|
||||
let mut invalidated: usize = 0;
|
||||
// Cells from the current buffer to skip due to preceding multi-width characters taking
|
||||
// their place (the skipped cells should be blank anyway), or due to per-cell-skipping:
|
||||
let mut to_skip: usize = 0;
|
||||
for (i, (current, previous)) in next_buffer.iter().zip(previous_buffer.iter()).enumerate() {
|
||||
if !current.skip && (current != previous || invalidated > 0) && to_skip == 0 {
|
||||
let (x, y) = self.pos_of(i);
|
||||
updates.push((x, y, &next_buffer[i]));
|
||||
}
|
||||
|
||||
to_skip = current.symbol().width().saturating_sub(1);
|
||||
|
||||
let affected_width = std::cmp::max(current.symbol().width(), previous.symbol().width());
|
||||
invalidated = std::cmp::max(affected_width, invalidated).saturating_sub(1);
|
||||
}
|
||||
updates
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Buffer {
|
||||
/// Writes a debug representation of the buffer to the given formatter.
|
||||
///
|
||||
/// The format is like a pretty printed struct, with the following fields:
|
||||
/// * `area`: displayed as `Rect { x: 1, y: 2, width: 3, height: 4 }`
|
||||
/// * `content`: displayed as a list of strings representing the content of the buffer
|
||||
/// * `styles`: displayed as a list of: `{ x: 1, y: 2, fg: Color::Red, bg: Color::Blue,
|
||||
/// modifier: Modifier::BOLD }` only showing a value when there is a change in style.
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
|
||||
f.write_fmt(format_args!(
|
||||
"Buffer {{\n area: {:?},\n content: [\n",
|
||||
&self.area
|
||||
))?;
|
||||
let mut last_style = None;
|
||||
let mut styles = vec![];
|
||||
for (y, line) in self.content.chunks(self.area.width as usize).enumerate() {
|
||||
let mut overwritten = vec![];
|
||||
let mut skip: usize = 0;
|
||||
f.write_str(" \"")?;
|
||||
for (x, c) in line.iter().enumerate() {
|
||||
if skip == 0 {
|
||||
f.write_str(c.symbol())?;
|
||||
} else {
|
||||
overwritten.push((x, c.symbol()));
|
||||
}
|
||||
skip = std::cmp::max(skip, c.symbol().width()).saturating_sub(1);
|
||||
#[cfg(feature = "underline-color")]
|
||||
{
|
||||
let style = (c.fg, c.bg, c.underline_color, c.modifier);
|
||||
if last_style != Some(style) {
|
||||
last_style = Some(style);
|
||||
styles.push((x, y, c.fg, c.bg, c.underline_color, c.modifier));
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "underline-color"))]
|
||||
{
|
||||
let style = (c.fg, c.bg, c.modifier);
|
||||
if last_style != Some(style) {
|
||||
last_style = Some(style);
|
||||
styles.push((x, y, c.fg, c.bg, c.modifier));
|
||||
}
|
||||
}
|
||||
}
|
||||
if !overwritten.is_empty() {
|
||||
f.write_fmt(format_args!(
|
||||
"// hidden by multi-width symbols: {overwritten:?}"
|
||||
))?;
|
||||
}
|
||||
f.write_str("\",\n")?;
|
||||
}
|
||||
f.write_str(" ],\n styles: [\n")?;
|
||||
for s in styles {
|
||||
#[cfg(feature = "underline-color")]
|
||||
f.write_fmt(format_args!(
|
||||
" x: {}, y: {}, fg: {:?}, bg: {:?}, underline: {:?}, modifier: {:?},\n",
|
||||
s.0, s.1, s.2, s.3, s.4, s.5
|
||||
))?;
|
||||
#[cfg(not(feature = "underline-color"))]
|
||||
f.write_fmt(format_args!(
|
||||
" x: {}, y: {}, fg: {:?}, bg: {:?}, modifier: {:?},\n",
|
||||
s.0, s.1, s.2, s.3, s.4
|
||||
))?;
|
||||
}
|
||||
f.write_str(" ]\n}")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::assert_buffer_eq;
|
||||
|
||||
fn cell(s: &str) -> Cell {
|
||||
let mut cell = Cell::default();
|
||||
cell.set_symbol(s);
|
||||
cell
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn debug() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 12, 2));
|
||||
buf.set_string(0, 0, "Hello World!", Style::default());
|
||||
buf.set_string(
|
||||
0,
|
||||
1,
|
||||
"G'day World!",
|
||||
Style::default()
|
||||
.fg(Color::Green)
|
||||
.bg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
);
|
||||
#[cfg(feature = "underline-color")]
|
||||
assert_eq!(
|
||||
format!("{buf:?}"),
|
||||
indoc::indoc!(
|
||||
"
|
||||
Buffer {
|
||||
area: Rect { x: 0, y: 0, width: 12, height: 2 },
|
||||
content: [
|
||||
\"Hello World!\",
|
||||
\"G'day World!\",
|
||||
],
|
||||
styles: [
|
||||
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 0, y: 1, fg: Green, bg: Yellow, underline: Reset, modifier: BOLD,
|
||||
]
|
||||
}"
|
||||
)
|
||||
);
|
||||
#[cfg(not(feature = "underline-color"))]
|
||||
assert_eq!(
|
||||
format!("{buf:?}"),
|
||||
indoc::indoc!(
|
||||
"
|
||||
Buffer {
|
||||
area: Rect { x: 0, y: 0, width: 12, height: 2 },
|
||||
content: [
|
||||
\"Hello World!\",
|
||||
\"G'day World!\",
|
||||
],
|
||||
styles: [
|
||||
x: 0, y: 0, fg: Reset, bg: Reset, modifier: NONE,
|
||||
x: 0, y: 1, fg: Green, bg: Yellow, modifier: BOLD,
|
||||
]
|
||||
}"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_translates_to_and_from_coordinates() {
|
||||
let rect = Rect::new(200, 100, 50, 80);
|
||||
let buf = Buffer::empty(rect);
|
||||
|
||||
// First cell is at the upper left corner.
|
||||
assert_eq!(buf.pos_of(0), (200, 100));
|
||||
assert_eq!(buf.index_of(200, 100), 0);
|
||||
|
||||
// Last cell is in the lower right.
|
||||
assert_eq!(buf.pos_of(buf.content.len() - 1), (249, 179));
|
||||
assert_eq!(buf.index_of(249, 179), buf.content.len() - 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "outside the buffer")]
|
||||
fn pos_of_panics_on_out_of_bounds() {
|
||||
let rect = Rect::new(0, 0, 10, 10);
|
||||
let buf = Buffer::empty(rect);
|
||||
|
||||
// There are a total of 100 cells; zero-indexed means that 100 would be the 101st cell.
|
||||
buf.pos_of(100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "outside the buffer")]
|
||||
fn index_of_panics_on_out_of_bounds() {
|
||||
let rect = Rect::new(0, 0, 10, 10);
|
||||
let buf = Buffer::empty(rect);
|
||||
|
||||
// width is 10; zero-indexed means that 10 would be the 11th cell.
|
||||
buf.index_of(10, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_string() {
|
||||
let area = Rect::new(0, 0, 5, 1);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
|
||||
// Zero-width
|
||||
buffer.set_stringn(0, 0, "aaa", 0, Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" "]));
|
||||
|
||||
buffer.set_string(0, 0, "aaa", Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["aaa "]));
|
||||
|
||||
// Width limit:
|
||||
buffer.set_stringn(0, 0, "bbbbbbbbbbbbbb", 4, Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["bbbb "]));
|
||||
|
||||
buffer.set_string(0, 0, "12345", Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["12345"]));
|
||||
|
||||
// Width truncation:
|
||||
buffer.set_string(0, 0, "123456", Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["12345"]));
|
||||
|
||||
// multi-line
|
||||
buffer = Buffer::empty(Rect::new(0, 0, 5, 2));
|
||||
buffer.set_string(0, 0, "12345", Style::default());
|
||||
buffer.set_string(0, 1, "67890", Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["12345", "67890"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_string_multi_width_overwrite() {
|
||||
let area = Rect::new(0, 0, 5, 1);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
|
||||
// multi-width overwrite
|
||||
buffer.set_string(0, 0, "aaaaa", Style::default());
|
||||
buffer.set_string(0, 0, "称号", Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["称号a"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_string_zero_width() {
|
||||
let area = Rect::new(0, 0, 1, 1);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
|
||||
// Leading grapheme with zero width
|
||||
let s = "\u{1}a";
|
||||
buffer.set_stringn(0, 0, s, 1, Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["a"]));
|
||||
|
||||
// Trailing grapheme with zero with
|
||||
let s = "a\u{1}";
|
||||
buffer.set_stringn(0, 0, s, 1, Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["a"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_string_double_width() {
|
||||
let area = Rect::new(0, 0, 5, 1);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
buffer.set_string(0, 0, "コン", Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["コン "]));
|
||||
|
||||
// Only 1 space left.
|
||||
buffer.set_string(0, 0, "コンピ", Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["コン "]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_style() {
|
||||
let mut buffer = Buffer::with_lines(vec!["aaaaa", "bbbbb", "ccccc"]);
|
||||
buffer.set_style(Rect::new(0, 1, 5, 1), Style::new().red());
|
||||
assert_buffer_eq!(
|
||||
buffer,
|
||||
Buffer::with_lines(vec!["aaaaa".into(), "bbbbb".red(), "ccccc".into(),])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_style_does_not_panic_when_out_of_area() {
|
||||
let mut buffer = Buffer::with_lines(vec!["aaaaa", "bbbbb", "ccccc"]);
|
||||
buffer.set_style(Rect::new(0, 1, 10, 3), Style::new().red());
|
||||
assert_buffer_eq!(
|
||||
buffer,
|
||||
Buffer::with_lines(vec!["aaaaa".into(), "bbbbb".red(), "ccccc".red(),])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_lines() {
|
||||
let buffer =
|
||||
Buffer::with_lines(vec!["┌────────┐", "│コンピュ│", "│ーa 上で│", "└────────┘"]);
|
||||
assert_eq!(buffer.area.x, 0);
|
||||
assert_eq!(buffer.area.y, 0);
|
||||
assert_eq!(buffer.area.width, 10);
|
||||
assert_eq!(buffer.area.height, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_empty_empty() {
|
||||
let area = Rect::new(0, 0, 40, 40);
|
||||
let prev = Buffer::empty(area);
|
||||
let next = Buffer::empty(area);
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(diff, vec![]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_empty_filled() {
|
||||
let area = Rect::new(0, 0, 40, 40);
|
||||
let prev = Buffer::empty(area);
|
||||
let next = Buffer::filled(area, Cell::default().set_symbol("a"));
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(diff.len(), 40 * 40);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_filled_filled() {
|
||||
let area = Rect::new(0, 0, 40, 40);
|
||||
let prev = Buffer::filled(area, Cell::default().set_symbol("a"));
|
||||
let next = Buffer::filled(area, Cell::default().set_symbol("a"));
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(diff, vec![]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_single_width() {
|
||||
let prev = Buffer::with_lines(vec![
|
||||
" ",
|
||||
"┌Title─┐ ",
|
||||
"│ │ ",
|
||||
"│ │ ",
|
||||
"└──────┘ ",
|
||||
]);
|
||||
let next = Buffer::with_lines(vec![
|
||||
" ",
|
||||
"┌TITLE─┐ ",
|
||||
"│ │ ",
|
||||
"│ │ ",
|
||||
"└──────┘ ",
|
||||
]);
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(
|
||||
diff,
|
||||
vec![
|
||||
(2, 1, &cell("I")),
|
||||
(3, 1, &cell("T")),
|
||||
(4, 1, &cell("L")),
|
||||
(5, 1, &cell("E")),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[rustfmt::skip]
|
||||
fn diff_multi_width() {
|
||||
let prev = Buffer::with_lines(vec![
|
||||
"┌Title─┐ ",
|
||||
"└──────┘ ",
|
||||
]);
|
||||
let next = Buffer::with_lines(vec![
|
||||
"┌称号──┐ ",
|
||||
"└──────┘ ",
|
||||
]);
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(
|
||||
diff,
|
||||
vec![
|
||||
(1, 0, &cell("称")),
|
||||
// Skipped "i"
|
||||
(3, 0, &cell("号")),
|
||||
// Skipped "l"
|
||||
(5, 0, &cell("─")),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_multi_width_offset() {
|
||||
let prev = Buffer::with_lines(vec!["┌称号──┐"]);
|
||||
let next = Buffer::with_lines(vec!["┌─称号─┐"]);
|
||||
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(
|
||||
diff,
|
||||
vec![(1, 0, &cell("─")), (2, 0, &cell("称")), (4, 0, &cell("号")),]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_skip() {
|
||||
let prev = Buffer::with_lines(vec!["123"]);
|
||||
let mut next = Buffer::with_lines(vec!["456"]);
|
||||
for i in 1..3 {
|
||||
next.content[i].set_skip(true);
|
||||
}
|
||||
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(diff, vec![(0, 0, &cell("4"))],);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge() {
|
||||
let mut one = Buffer::filled(
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("1"),
|
||||
);
|
||||
let two = Buffer::filled(
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 2,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("2"),
|
||||
);
|
||||
one.merge(&two);
|
||||
assert_buffer_eq!(one, Buffer::with_lines(vec!["11", "11", "22", "22"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge2() {
|
||||
let mut one = Buffer::filled(
|
||||
Rect {
|
||||
x: 2,
|
||||
y: 2,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("1"),
|
||||
);
|
||||
let two = Buffer::filled(
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("2"),
|
||||
);
|
||||
one.merge(&two);
|
||||
assert_buffer_eq!(
|
||||
one,
|
||||
Buffer::with_lines(vec!["22 ", "22 ", " 11", " 11"])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge3() {
|
||||
let mut one = Buffer::filled(
|
||||
Rect {
|
||||
x: 3,
|
||||
y: 3,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("1"),
|
||||
);
|
||||
let two = Buffer::filled(
|
||||
Rect {
|
||||
x: 1,
|
||||
y: 1,
|
||||
width: 3,
|
||||
height: 4,
|
||||
},
|
||||
Cell::default().set_symbol("2"),
|
||||
);
|
||||
one.merge(&two);
|
||||
let mut merged = Buffer::with_lines(vec!["222 ", "222 ", "2221", "2221"]);
|
||||
merged.area = Rect {
|
||||
x: 1,
|
||||
y: 1,
|
||||
width: 4,
|
||||
height: 4,
|
||||
};
|
||||
assert_buffer_eq!(one, merged);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_skip() {
|
||||
let mut one = Buffer::filled(
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("1"),
|
||||
);
|
||||
let two = Buffer::filled(
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 1,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("2").set_skip(true),
|
||||
);
|
||||
one.merge(&two);
|
||||
let skipped: Vec<bool> = one.content().iter().map(|c| c.skip).collect();
|
||||
assert_eq!(skipped, vec![false, false, true, true, true, true]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_skip2() {
|
||||
let mut one = Buffer::filled(
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("1").set_skip(true),
|
||||
);
|
||||
let two = Buffer::filled(
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 1,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("2"),
|
||||
);
|
||||
one.merge(&two);
|
||||
let skipped: Vec<bool> = one.content().iter().map(|c| c.skip).collect();
|
||||
assert_eq!(skipped, vec![true, true, false, false, false, false]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_lines_accepts_into_lines() {
|
||||
use crate::style::Stylize;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 3, 2));
|
||||
buf.set_string(0, 0, "foo", Style::new().red());
|
||||
buf.set_string(0, 1, "bar", Style::new().blue());
|
||||
assert_eq!(buf, Buffer::with_lines(vec!["foo".red(), "bar".blue()]));
|
||||
}
|
||||
}
|
||||
156
src/buffer/cell.rs
Normal file
156
src/buffer/cell.rs
Normal file
@@ -0,0 +1,156 @@
|
||||
use std::fmt::Debug;
|
||||
|
||||
use compact_str::CompactString;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
/// A buffer cell
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Cell {
|
||||
/// The string to be drawn in the cell.
|
||||
///
|
||||
/// This accepts unicode grapheme clusters which might take up more than one cell.
|
||||
///
|
||||
/// This is a [`CompactString`] which is a wrapper around [`String`] that uses a small inline
|
||||
/// buffer for short strings.
|
||||
///
|
||||
/// See <https://github.com/ratatui-org/ratatui/pull/601> for more information.
|
||||
symbol: CompactString,
|
||||
|
||||
/// The foreground color of the cell.
|
||||
pub fg: Color,
|
||||
|
||||
/// The background color of the cell.
|
||||
pub bg: Color,
|
||||
|
||||
/// The underline color of the cell.
|
||||
#[cfg(feature = "underline-color")]
|
||||
pub underline_color: Color,
|
||||
|
||||
/// The modifier of the cell.
|
||||
pub modifier: Modifier,
|
||||
|
||||
/// Whether the cell should be skipped when copying (diffing) the buffer to the screen.
|
||||
pub skip: bool,
|
||||
}
|
||||
|
||||
impl Cell {
|
||||
/// Gets the symbol of the cell.
|
||||
pub fn symbol(&self) -> &str {
|
||||
self.symbol.as_str()
|
||||
}
|
||||
|
||||
/// Sets the symbol of the cell.
|
||||
pub fn set_symbol(&mut self, symbol: &str) -> &mut Cell {
|
||||
self.symbol = CompactString::new(symbol);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the symbol of the cell to a single character.
|
||||
pub fn set_char(&mut self, ch: char) -> &mut Cell {
|
||||
let mut buf = [0; 4];
|
||||
self.symbol = CompactString::new(ch.encode_utf8(&mut buf));
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the foreground color of the cell.
|
||||
pub fn set_fg(&mut self, color: Color) -> &mut Cell {
|
||||
self.fg = color;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the background color of the cell.
|
||||
pub fn set_bg(&mut self, color: Color) -> &mut Cell {
|
||||
self.bg = color;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the cell.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
pub fn set_style<S: Into<Style>>(&mut self, style: S) -> &mut Cell {
|
||||
let style = style.into();
|
||||
if let Some(c) = style.fg {
|
||||
self.fg = c;
|
||||
}
|
||||
if let Some(c) = style.bg {
|
||||
self.bg = c;
|
||||
}
|
||||
#[cfg(feature = "underline-color")]
|
||||
if let Some(c) = style.underline_color {
|
||||
self.underline_color = c;
|
||||
}
|
||||
self.modifier.insert(style.add_modifier);
|
||||
self.modifier.remove(style.sub_modifier);
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the style of the cell.
|
||||
pub fn style(&self) -> Style {
|
||||
#[cfg(feature = "underline-color")]
|
||||
return Style::default()
|
||||
.fg(self.fg)
|
||||
.bg(self.bg)
|
||||
.underline_color(self.underline_color)
|
||||
.add_modifier(self.modifier);
|
||||
|
||||
#[cfg(not(feature = "underline-color"))]
|
||||
return Style::default()
|
||||
.fg(self.fg)
|
||||
.bg(self.bg)
|
||||
.add_modifier(self.modifier);
|
||||
}
|
||||
|
||||
/// Sets the cell to be skipped when copying (diffing) the buffer to the screen.
|
||||
///
|
||||
/// This is helpful when it is necessary to prevent the buffer from overwriting a cell that is
|
||||
/// covered by an image from some terminal graphics protocol (Sixel / iTerm / Kitty ...).
|
||||
pub fn set_skip(&mut self, skip: bool) -> &mut Cell {
|
||||
self.skip = skip;
|
||||
self
|
||||
}
|
||||
|
||||
/// Resets the cell to the default state.
|
||||
pub fn reset(&mut self) {
|
||||
self.symbol = CompactString::new(" ");
|
||||
self.fg = Color::Reset;
|
||||
self.bg = Color::Reset;
|
||||
#[cfg(feature = "underline-color")]
|
||||
{
|
||||
self.underline_color = Color::Reset;
|
||||
}
|
||||
self.modifier = Modifier::empty();
|
||||
self.skip = false;
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Cell {
|
||||
fn default() -> Cell {
|
||||
Cell {
|
||||
symbol: CompactString::new(" "),
|
||||
fg: Color::Reset,
|
||||
bg: Color::Reset,
|
||||
#[cfg(feature = "underline-color")]
|
||||
underline_color: Color::Reset,
|
||||
modifier: Modifier::empty(),
|
||||
skip: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn symbol_field() {
|
||||
let mut cell = Cell::default();
|
||||
assert_eq!(cell.symbol(), " ");
|
||||
cell.set_symbol("あ"); // Multi-byte character
|
||||
assert_eq!(cell.symbol(), "あ");
|
||||
cell.set_symbol("👨👩👧👦"); // Multiple code units combined with ZWJ
|
||||
assert_eq!(cell.symbol(), "👨👩👧👦");
|
||||
}
|
||||
}
|
||||
1323
src/layout.rs
1323
src/layout.rs
File diff suppressed because it is too large
Load Diff
31
src/layout/alignment.rs
Normal file
31
src/layout/alignment.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub enum Alignment {
|
||||
#[default]
|
||||
Left,
|
||||
Center,
|
||||
Right,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use strum::ParseError;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn alignment_to_string() {
|
||||
assert_eq!(Alignment::Left.to_string(), "Left");
|
||||
assert_eq!(Alignment::Center.to_string(), "Center");
|
||||
assert_eq!(Alignment::Right.to_string(), "Right");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alignment_from_str() {
|
||||
assert_eq!("Left".parse::<Alignment>(), Ok(Alignment::Left));
|
||||
assert_eq!("Center".parse::<Alignment>(), Ok(Alignment::Center));
|
||||
assert_eq!("Right".parse::<Alignment>(), Ok(Alignment::Right));
|
||||
assert_eq!("".parse::<Alignment>(), Err(ParseError::VariantNotFound));
|
||||
}
|
||||
}
|
||||
551
src/layout/constraint.rs
Normal file
551
src/layout/constraint.rs
Normal file
@@ -0,0 +1,551 @@
|
||||
use std::fmt::{self, Display};
|
||||
|
||||
use itertools::Itertools;
|
||||
|
||||
/// A constraint that defines the size of a layout element.
|
||||
///
|
||||
/// Constraints can be used to specify a fixed size, a percentage of the available space, a ratio of
|
||||
/// the available space, a minimum or maximum size or a proportional value for a layout element.
|
||||
///
|
||||
/// Relative constraints (percentage, ratio) are calculated relative to the entire space being
|
||||
/// divided, rather than the space available after applying more fixed constraints (min, max,
|
||||
/// length).
|
||||
///
|
||||
/// Constraints are prioritized in the following order:
|
||||
///
|
||||
/// 1. [`Constraint::Fixed`]
|
||||
/// 2. [`Constraint::Min`] / [`Constraint::Max`]
|
||||
/// 3. [`Constraint::Length`] / [`Constraint::Percentage`] / [`Constraint::Ratio`]
|
||||
/// 4. [`Constraint::Proportional`]
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// `Constraint` provides helper methods to create lists of constraints from various input formats.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// // Create a layout with specified lengths for each element
|
||||
/// let constraints = Constraint::from_lengths([10, 20, 10]);
|
||||
///
|
||||
/// // Create a layout with specified fixed lengths for each element
|
||||
/// let constraints = Constraint::from_fixed_lengths([10, 20, 10]);
|
||||
///
|
||||
/// // Create a centered layout using ratio or percentage constraints
|
||||
/// let constraints = Constraint::from_ratios([(1, 4), (1, 2), (1, 4)]);
|
||||
/// let constraints = Constraint::from_percentages([25, 50, 25]);
|
||||
///
|
||||
/// // Create a centered layout with a minimum size constraint for specific elements
|
||||
/// let constraints = Constraint::from_mins([0, 100, 0]);
|
||||
///
|
||||
/// // Create a sidebar layout specifying maximum sizes for the columns
|
||||
/// let constraints = Constraint::from_maxes([30, 170]);
|
||||
///
|
||||
/// // Create a layout with proportional sizes for each element
|
||||
/// let constraints = Constraint::from_proportional_lengths([1, 2, 1]);
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub enum Constraint {
|
||||
/// Applies a percentage of the available space to the element
|
||||
///
|
||||
/// Converts the given percentage to a floating-point value and multiplies that with area.
|
||||
/// This value is rounded back to a integer as part of the layout split calculation.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// `[Percentage(75), Proportional(1)]`
|
||||
///
|
||||
/// ```plain
|
||||
/// ┌────────────────────────────────────┐┌──────────┐
|
||||
/// │ 38 px ││ 12 px │
|
||||
/// └────────────────────────────────────┘└──────────┘
|
||||
/// ```
|
||||
///
|
||||
/// `[Percentage(50), Proportional(1)]`
|
||||
///
|
||||
/// ```plain
|
||||
/// ┌───────────────────────┐┌───────────────────────┐
|
||||
/// │ 25 px ││ 25 px │
|
||||
/// └───────────────────────┘└───────────────────────┘
|
||||
/// ```
|
||||
Percentage(u16),
|
||||
/// Applies a ratio of the available space to the element
|
||||
///
|
||||
/// Converts the given ratio to a floating-point value and multiplies that with area.
|
||||
/// This value is rounded back to a integer as part of the layout split calculation.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// `[Ratio(1, 2) ; 2]`
|
||||
///
|
||||
/// ```plain
|
||||
/// ┌───────────────────────┐┌───────────────────────┐
|
||||
/// │ 25 px ││ 25 px │
|
||||
/// └───────────────────────┘└───────────────────────┘
|
||||
/// ```
|
||||
///
|
||||
/// `[Ratio(1, 4) ; 4]`
|
||||
///
|
||||
/// ```plain
|
||||
/// ┌───────────┐┌──────────┐┌───────────┐┌──────────┐
|
||||
/// │ 13 px ││ 12 px ││ 13 px ││ 12 px │
|
||||
/// └───────────┘└──────────┘└───────────┘└──────────┘
|
||||
/// ```
|
||||
Ratio(u32, u32),
|
||||
/// Applies a fixed size to the element
|
||||
///
|
||||
/// The element size is set to the specified amount.
|
||||
/// [`Constraint::Fixed`] will take precedence over all other constraints.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// `[Fixed(40), Proportional(1)]`
|
||||
///
|
||||
/// ```plain
|
||||
/// ┌──────────────────────────────────────┐┌────────┐
|
||||
/// │ 40 px ││ 10 px │
|
||||
/// └──────────────────────────────────────┘└────────┘
|
||||
/// ```
|
||||
///
|
||||
/// `[Fixed(20), Fixed(20), Proportional(1)]`
|
||||
///
|
||||
/// ```plain
|
||||
/// ┌──────────────────┐┌──────────────────┐┌────────┐
|
||||
/// │ 20 px ││ 20 px ││ 10 px │
|
||||
/// └──────────────────┘└──────────────────┘└────────┘
|
||||
/// ```
|
||||
Fixed(u16),
|
||||
/// Applies a length constraint to the element
|
||||
///
|
||||
/// The element size is set to the specified amount.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// `[Length(20), Fixed(20)]`
|
||||
///
|
||||
/// ```plain
|
||||
/// ┌────────────────────────────┐┌──────────────────┐
|
||||
/// │ 30 px ││ 20 px │
|
||||
/// └────────────────────────────┘└──────────────────┘
|
||||
/// ```
|
||||
///
|
||||
/// `[Length(20), Length(20)]`
|
||||
///
|
||||
/// ```plain
|
||||
/// ┌──────────────────┐┌────────────────────────────┐
|
||||
/// │ 20 px ││ 30 px │
|
||||
/// └──────────────────┘└────────────────────────────┘
|
||||
/// ```
|
||||
Length(u16),
|
||||
/// Applies the scaling factor proportional to all other [`Constraint::Proportional`] elements
|
||||
/// to fill excess space
|
||||
///
|
||||
/// The element will only expand into excess available space, proportionally matching other
|
||||
/// [`Constraint::Proportional`] elements while satisfying all other constraints.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
///
|
||||
/// `[Proportional(1), Proportional(2), Proportional(3)]`
|
||||
///
|
||||
/// ```plain
|
||||
/// ┌──────┐┌───────────────┐┌───────────────────────┐
|
||||
/// │ 8 px ││ 17 px ││ 25 px │
|
||||
/// └──────┘└───────────────┘└───────────────────────┘
|
||||
/// ```
|
||||
///
|
||||
/// `[Proportional(1), Percentage(50), Proportional(1)]`
|
||||
///
|
||||
/// ```plain
|
||||
/// ┌───────────┐┌───────────────────────┐┌──────────┐
|
||||
/// │ 13 px ││ 25 px ││ 12 px │
|
||||
/// └───────────┘└───────────────────────┘└──────────┘
|
||||
/// ```
|
||||
Proportional(u16),
|
||||
/// Applies a maximum size constraint to the element
|
||||
///
|
||||
/// The element size is set to at most the specified amount.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// `[Percentage(100), Min(20)]`
|
||||
///
|
||||
/// ```plain
|
||||
/// ┌────────────────────────────┐┌──────────────────┐
|
||||
/// │ 30 px ││ 20 px │
|
||||
/// └────────────────────────────┘└──────────────────┘
|
||||
/// ```
|
||||
///
|
||||
/// `[Percentage(100), Min(10)]`
|
||||
///
|
||||
/// ```plain
|
||||
/// ┌──────────────────────────────────────┐┌────────┐
|
||||
/// │ 40 px ││ 10 px │
|
||||
/// └──────────────────────────────────────┘└────────┘
|
||||
/// ```
|
||||
Max(u16),
|
||||
/// Applies a minimum size constraint to the element
|
||||
///
|
||||
/// The element size is set to at least the specified amount.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// `[Percentage(100), Min(20)]`
|
||||
///
|
||||
/// ```plain
|
||||
/// ┌────────────────────────────┐┌──────────────────┐
|
||||
/// │ 30 px ││ 20 px │
|
||||
/// └────────────────────────────┘└──────────────────┘
|
||||
/// ```
|
||||
///
|
||||
/// `[Percentage(100), Min(10)]`
|
||||
///
|
||||
/// ```plain
|
||||
/// ┌──────────────────────────────────────┐┌────────┐
|
||||
/// │ 40 px ││ 10 px │
|
||||
/// └──────────────────────────────────────┘└────────┘
|
||||
/// ```
|
||||
Min(u16),
|
||||
}
|
||||
|
||||
impl Constraint {
|
||||
#[deprecated(
|
||||
since = "0.26.0",
|
||||
note = "This field will be hidden in the next minor version."
|
||||
)]
|
||||
pub fn apply(&self, length: u16) -> u16 {
|
||||
match *self {
|
||||
Constraint::Percentage(p) => {
|
||||
let p = p as f32 / 100.0;
|
||||
let length = length as f32;
|
||||
(p * length).min(length) as u16
|
||||
}
|
||||
Constraint::Ratio(numerator, denominator) => {
|
||||
// avoid division by zero by using 1 when denominator is 0
|
||||
// this results in 0/0 -> 0 and x/0 -> x for x != 0
|
||||
let percentage = numerator as f32 / denominator.max(1) as f32;
|
||||
let length = length as f32;
|
||||
(percentage * length).min(length) as u16
|
||||
}
|
||||
Constraint::Length(l) => length.min(l),
|
||||
Constraint::Fixed(l) => length.min(l),
|
||||
Constraint::Proportional(l) => length.min(l),
|
||||
Constraint::Max(m) => length.min(m),
|
||||
Constraint::Min(m) => length.max(m),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert an iterator of lengths into a vector of constraints
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// # let area = Rect::default();
|
||||
/// let constraints = Constraint::from_lengths([1, 2, 3]);
|
||||
/// let layout = Layout::default().constraints(constraints).split(area);
|
||||
/// ```
|
||||
pub fn from_lengths<T>(lengths: T) -> Vec<Constraint>
|
||||
where
|
||||
T: IntoIterator<Item = u16>,
|
||||
{
|
||||
lengths.into_iter().map(Constraint::Length).collect_vec()
|
||||
}
|
||||
|
||||
/// Convert an iterator of fixed lengths into a vector of constraints
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// # let area = Rect::default();
|
||||
/// let constraints = Constraint::from_fixed_lengths([1, 2, 3]);
|
||||
/// let layout = Layout::default().constraints(constraints).split(area);
|
||||
/// ```
|
||||
pub fn from_fixed_lengths<T>(fixed_lengths: T) -> Vec<Constraint>
|
||||
where
|
||||
T: IntoIterator<Item = u16>,
|
||||
{
|
||||
fixed_lengths
|
||||
.into_iter()
|
||||
.map(Constraint::Fixed)
|
||||
.collect_vec()
|
||||
}
|
||||
|
||||
/// Convert an iterator of ratios into a vector of constraints
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// # let area = Rect::default();
|
||||
/// let constraints = Constraint::from_ratios([(1, 4), (1, 2), (1, 4)]);
|
||||
/// let layout = Layout::default().constraints(constraints).split(area);
|
||||
/// ```
|
||||
pub fn from_ratios<T>(ratios: T) -> Vec<Constraint>
|
||||
where
|
||||
T: IntoIterator<Item = (u32, u32)>,
|
||||
{
|
||||
ratios
|
||||
.into_iter()
|
||||
.map(|(n, d)| Constraint::Ratio(n, d))
|
||||
.collect_vec()
|
||||
}
|
||||
|
||||
/// Convert an iterator of percentages into a vector of constraints
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// # let area = Rect::default();
|
||||
/// let constraints = Constraint::from_percentages([25, 50, 25]);
|
||||
/// let layout = Layout::default().constraints(constraints).split(area);
|
||||
/// ```
|
||||
pub fn from_percentages<T>(percentages: T) -> Vec<Constraint>
|
||||
where
|
||||
T: IntoIterator<Item = u16>,
|
||||
{
|
||||
percentages
|
||||
.into_iter()
|
||||
.map(Constraint::Percentage)
|
||||
.collect_vec()
|
||||
}
|
||||
|
||||
/// Convert an iterator of maxes into a vector of constraints
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// # let area = Rect::default();
|
||||
/// let constraints = Constraint::from_maxes([1, 2, 3]);
|
||||
/// let layout = Layout::default().constraints(constraints).split(area);
|
||||
/// ```
|
||||
pub fn from_maxes<T>(maxes: T) -> Vec<Constraint>
|
||||
where
|
||||
T: IntoIterator<Item = u16>,
|
||||
{
|
||||
maxes.into_iter().map(Constraint::Max).collect_vec()
|
||||
}
|
||||
|
||||
/// Convert an iterator of mins into a vector of constraints
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// # let area = Rect::default();
|
||||
/// let constraints = Constraint::from_mins([1, 2, 3]);
|
||||
/// let layout = Layout::default().constraints(constraints).split(area);
|
||||
/// ```
|
||||
pub fn from_mins<T>(mins: T) -> Vec<Constraint>
|
||||
where
|
||||
T: IntoIterator<Item = u16>,
|
||||
{
|
||||
mins.into_iter().map(Constraint::Min).collect_vec()
|
||||
}
|
||||
|
||||
/// Convert an iterator of proportional factors into a vector of constraints
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// # let area = Rect::default();
|
||||
/// let constraints = Constraint::from_mins([1, 2, 3]);
|
||||
/// let layout = Layout::default().constraints(constraints).split(area);
|
||||
/// ```
|
||||
pub fn from_proportional_lengths<T>(proportional_lengths: T) -> Vec<Constraint>
|
||||
where
|
||||
T: IntoIterator<Item = u16>,
|
||||
{
|
||||
proportional_lengths
|
||||
.into_iter()
|
||||
.map(Constraint::Proportional)
|
||||
.collect_vec()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u16> for Constraint {
|
||||
/// Convert a u16 into a [Constraint::Length]
|
||||
///
|
||||
/// This is useful when you want to specify a fixed size for a layout, but don't want to
|
||||
/// explicitly create a [Constraint::Length] yourself.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// # let area = Rect::default();
|
||||
/// let layout = Layout::new(Direction::Vertical, [1, 2, 3]).split(area);
|
||||
/// let layout = Layout::horizontal([1, 2, 3]).split(area);
|
||||
/// let layout = Layout::vertical([1, 2, 3]).split(area);
|
||||
/// ````
|
||||
fn from(length: u16) -> Constraint {
|
||||
Constraint::Length(length)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Constraint> for Constraint {
|
||||
fn from(constraint: &Constraint) -> Self {
|
||||
*constraint
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<Constraint> for Constraint {
|
||||
fn as_ref(&self) -> &Constraint {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Constraint {
|
||||
fn default() -> Self {
|
||||
Constraint::Percentage(100)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Constraint {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Constraint::Percentage(p) => write!(f, "Percentage({})", p),
|
||||
Constraint::Ratio(n, d) => write!(f, "Ratio({}, {})", n, d),
|
||||
Constraint::Length(l) => write!(f, "Length({})", l),
|
||||
Constraint::Fixed(l) => write!(f, "Fixed({})", l),
|
||||
Constraint::Proportional(l) => write!(f, "Proportional({})", l),
|
||||
Constraint::Max(m) => write!(f, "Max({})", m),
|
||||
Constraint::Min(m) => write!(f, "Min({})", m),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn default() {
|
||||
assert_eq!(Constraint::default(), Constraint::Percentage(100));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_string() {
|
||||
assert_eq!(Constraint::Percentage(50).to_string(), "Percentage(50)");
|
||||
assert_eq!(Constraint::Ratio(1, 2).to_string(), "Ratio(1, 2)");
|
||||
assert_eq!(Constraint::Length(10).to_string(), "Length(10)");
|
||||
assert_eq!(Constraint::Max(10).to_string(), "Max(10)");
|
||||
assert_eq!(Constraint::Min(10).to_string(), "Min(10)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_lengths() {
|
||||
let expected = [
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(2),
|
||||
Constraint::Length(3),
|
||||
];
|
||||
assert_eq!(Constraint::from_lengths([1, 2, 3]), expected);
|
||||
assert_eq!(Constraint::from_lengths(vec![1, 2, 3]), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_fixed_lengths() {
|
||||
let expected = [
|
||||
Constraint::Fixed(1),
|
||||
Constraint::Fixed(2),
|
||||
Constraint::Fixed(3),
|
||||
];
|
||||
assert_eq!(Constraint::from_fixed_lengths([1, 2, 3]), expected);
|
||||
assert_eq!(Constraint::from_fixed_lengths(vec![1, 2, 3]), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_ratios() {
|
||||
let expected = [
|
||||
Constraint::Ratio(1, 4),
|
||||
Constraint::Ratio(1, 2),
|
||||
Constraint::Ratio(1, 4),
|
||||
];
|
||||
assert_eq!(Constraint::from_ratios([(1, 4), (1, 2), (1, 4)]), expected);
|
||||
assert_eq!(
|
||||
Constraint::from_ratios(vec![(1, 4), (1, 2), (1, 4)]),
|
||||
expected
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_percentages() {
|
||||
let expected = [
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(50),
|
||||
Constraint::Percentage(25),
|
||||
];
|
||||
assert_eq!(Constraint::from_percentages([25, 50, 25]), expected);
|
||||
assert_eq!(Constraint::from_percentages(vec![25, 50, 25]), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_maxes() {
|
||||
let expected = [Constraint::Max(1), Constraint::Max(2), Constraint::Max(3)];
|
||||
assert_eq!(Constraint::from_maxes([1, 2, 3]), expected);
|
||||
assert_eq!(Constraint::from_maxes(vec![1, 2, 3]), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_mins() {
|
||||
let expected = [Constraint::Min(1), Constraint::Min(2), Constraint::Min(3)];
|
||||
assert_eq!(Constraint::from_mins([1, 2, 3]), expected);
|
||||
assert_eq!(Constraint::from_mins(vec![1, 2, 3]), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_proportional_lengths() {
|
||||
let expected = [
|
||||
Constraint::Proportional(1),
|
||||
Constraint::Proportional(2),
|
||||
Constraint::Proportional(3),
|
||||
];
|
||||
assert_eq!(Constraint::from_proportional_lengths([1, 2, 3]), expected);
|
||||
assert_eq!(
|
||||
Constraint::from_proportional_lengths(vec![1, 2, 3]),
|
||||
expected
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(deprecated)]
|
||||
fn apply() {
|
||||
assert_eq!(Constraint::Percentage(0).apply(100), 0);
|
||||
assert_eq!(Constraint::Percentage(50).apply(100), 50);
|
||||
assert_eq!(Constraint::Percentage(100).apply(100), 100);
|
||||
assert_eq!(Constraint::Percentage(200).apply(100), 100);
|
||||
assert_eq!(Constraint::Percentage(u16::MAX).apply(100), 100);
|
||||
|
||||
// 0/0 intentionally avoids a panic by returning 0.
|
||||
assert_eq!(Constraint::Ratio(0, 0).apply(100), 0);
|
||||
// 1/0 intentionally avoids a panic by returning 100% of the length.
|
||||
assert_eq!(Constraint::Ratio(1, 0).apply(100), 100);
|
||||
assert_eq!(Constraint::Ratio(0, 1).apply(100), 0);
|
||||
assert_eq!(Constraint::Ratio(1, 2).apply(100), 50);
|
||||
assert_eq!(Constraint::Ratio(2, 2).apply(100), 100);
|
||||
assert_eq!(Constraint::Ratio(3, 2).apply(100), 100);
|
||||
assert_eq!(Constraint::Ratio(u32::MAX, 2).apply(100), 100);
|
||||
|
||||
assert_eq!(Constraint::Length(0).apply(100), 0);
|
||||
assert_eq!(Constraint::Length(50).apply(100), 50);
|
||||
assert_eq!(Constraint::Length(100).apply(100), 100);
|
||||
assert_eq!(Constraint::Length(200).apply(100), 100);
|
||||
assert_eq!(Constraint::Length(u16::MAX).apply(100), 100);
|
||||
|
||||
assert_eq!(Constraint::Max(0).apply(100), 0);
|
||||
assert_eq!(Constraint::Max(50).apply(100), 50);
|
||||
assert_eq!(Constraint::Max(100).apply(100), 100);
|
||||
assert_eq!(Constraint::Max(200).apply(100), 100);
|
||||
assert_eq!(Constraint::Max(u16::MAX).apply(100), 100);
|
||||
|
||||
assert_eq!(Constraint::Min(0).apply(100), 100);
|
||||
assert_eq!(Constraint::Min(50).apply(100), 100);
|
||||
assert_eq!(Constraint::Min(100).apply(100), 100);
|
||||
assert_eq!(Constraint::Min(200).apply(100), 200);
|
||||
assert_eq!(Constraint::Min(u16::MAX).apply(100), u16::MAX);
|
||||
}
|
||||
}
|
||||
33
src/layout/corner.rs
Normal file
33
src/layout/corner.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub enum Corner {
|
||||
#[default]
|
||||
TopLeft,
|
||||
TopRight,
|
||||
BottomRight,
|
||||
BottomLeft,
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use strum::ParseError;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn corner_to_string() {
|
||||
assert_eq!(Corner::BottomLeft.to_string(), "BottomLeft");
|
||||
assert_eq!(Corner::BottomRight.to_string(), "BottomRight");
|
||||
assert_eq!(Corner::TopLeft.to_string(), "TopLeft");
|
||||
assert_eq!(Corner::TopRight.to_string(), "TopRight");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corner_from_str() {
|
||||
assert_eq!("BottomLeft".parse::<Corner>(), Ok(Corner::BottomLeft));
|
||||
assert_eq!("BottomRight".parse::<Corner>(), Ok(Corner::BottomRight));
|
||||
assert_eq!("TopLeft".parse::<Corner>(), Ok(Corner::TopLeft));
|
||||
assert_eq!("TopRight".parse::<Corner>(), Ok(Corner::TopRight));
|
||||
assert_eq!("".parse::<Corner>(), Err(ParseError::VariantNotFound));
|
||||
}
|
||||
}
|
||||
28
src/layout/direction.rs
Normal file
28
src/layout/direction.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub enum Direction {
|
||||
Horizontal,
|
||||
#[default]
|
||||
Vertical,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use strum::ParseError;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn direction_to_string() {
|
||||
assert_eq!(Direction::Horizontal.to_string(), "Horizontal");
|
||||
assert_eq!(Direction::Vertical.to_string(), "Vertical");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direction_from_str() {
|
||||
assert_eq!("Horizontal".parse::<Direction>(), Ok(Direction::Horizontal));
|
||||
assert_eq!("Vertical".parse::<Direction>(), Ok(Direction::Vertical));
|
||||
assert_eq!("".parse::<Direction>(), Err(ParseError::VariantNotFound));
|
||||
}
|
||||
}
|
||||
250
src/layout/flex.rs
Normal file
250
src/layout/flex.rs
Normal file
@@ -0,0 +1,250 @@
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
#[allow(unused_imports)]
|
||||
use super::constraint::Constraint;
|
||||
|
||||
/// Defines the options for layout flex justify content in a container.
|
||||
///
|
||||
/// This enumeration controls the distribution of space when layout constraints are met.
|
||||
///
|
||||
/// - `StretchLast`: Fills the available space within the container, putting excess space into the
|
||||
/// last element.
|
||||
/// - `Stretch`: Always fills the available space within the container.
|
||||
/// - `Start`: Aligns items to the start of the container.
|
||||
/// - `End`: Aligns items to the end of the container.
|
||||
/// - `Center`: Centers items within the container.
|
||||
/// - `SpaceBetween`: Adds excess space between each element.
|
||||
/// - `SpaceAround`: Adds excess space around each element.
|
||||
#[derive(Copy, Debug, Default, Display, EnumString, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum Flex {
|
||||
/// Fills the available space within the container, putting excess space into the last
|
||||
/// constraint of the lowest priority. This matches the default behavior of ratatui and tui
|
||||
/// applications without [`Flex`]
|
||||
///
|
||||
/// The following examples illustrate the allocation of excess in various combinations of
|
||||
/// constraints. As a refresher, the priorities of constraints are as follows:
|
||||
///
|
||||
/// 1. [`Constraint::Fixed`]
|
||||
/// 2. [`Constraint::Min`] / [`Constraint::Max`]
|
||||
/// 3. [`Constraint::Length`] / [`Constraint::Percentage`] / [`Constraint::Ratio`]
|
||||
/// 4. [`Constraint::Proportional`]
|
||||
///
|
||||
/// When every constraint is `Length`, the last element gets the excess.
|
||||
///
|
||||
/// ```plain
|
||||
/// <----------------------------------- 80 px ------------------------------------>
|
||||
/// ┌──────20 px───────┐┌──────20 px───────┐┌────────────────40 px─────────────────┐
|
||||
/// │ Length(20) ││ Length(20) ││ Length(20) │
|
||||
/// └──────────────────┘└──────────────────┘└──────────────────────────────────────┘
|
||||
/// ^^^^^^^^^^^^^^^^ EXCESS ^^^^^^^^^^^^^^^^
|
||||
/// ```
|
||||
///
|
||||
/// If we replace the constraint at the end with a `Fixed`, because it has a
|
||||
/// higher priority, the last constraint with the lowest priority, i.e. the last
|
||||
/// `Length` gets the excess.
|
||||
///
|
||||
/// ```plain
|
||||
/// <----------------------------------- 80 px ------------------------------------>
|
||||
/// ┌──────20 px───────┐┌────────────────40 px─────────────────┐┌──────20 px───────┐
|
||||
/// │ Length(20) ││ Length(20) ││ Fixed(20) │
|
||||
/// └──────────────────┘└──────────────────────────────────────┘└──────────────────┘
|
||||
/// ^^^^^^^^^^^^^^^^ EXCESS ^^^^^^^^^^^^^^^^
|
||||
/// ```
|
||||
///
|
||||
/// Violating a `Max` is lower priority than `Fixed` but higher
|
||||
/// than `Length`.
|
||||
///
|
||||
/// ```plain
|
||||
/// <----------------------------------- 80 px ------------------------------------>
|
||||
/// ┌────────────────40 px─────────────────┐┌──────20 px───────┐┌──────20 px───────┐
|
||||
/// │ Length(20) ││ Max(20) ││ Fixed(20) │
|
||||
/// └──────────────────────────────────────┘└──────────────────┘└──────────────────┘
|
||||
/// ^^^^^^^^^^^^^^^^ EXCESS ^^^^^^^^^^^^^^^^
|
||||
/// ```
|
||||
///
|
||||
/// It's important to note that while not violating a `Min` or `Max` constraint is
|
||||
/// prioritized higher than a `Length`, `Min` and `Max` constraints allow for a range
|
||||
/// of values and excess can (and will) be dumped into these ranges first, if possible,
|
||||
/// even if it not the last constraint.
|
||||
///
|
||||
/// ```plain
|
||||
/// <----------------------------------- 80 px ------------------------------------>
|
||||
/// ┌──────20 px───────┐┌────────────────40 px─────────────────┐┌──────20 px───────┐
|
||||
/// │ Length(20) ││ Min(20) ││ Fixed(20) │
|
||||
/// └──────────────────┘└──────────────────────────────────────┘└──────────────────┘
|
||||
/// ^^^^^^^^^^^^^^^^ EXCESS ^^^^^^^^^^^^^^^^
|
||||
///
|
||||
/// <----------------------------------- 80 px ------------------------------------>
|
||||
/// ┌────────────────40 px─────────────────┐┌──────20 px───────┐┌──────20 px───────┐
|
||||
/// │ Min(20) ││ Length(20) ││ Fixed(20) │
|
||||
/// └──────────────────────────────────────┘└──────────────────┘└──────────────────┘
|
||||
/// ^^^^^^^^^^^^^^^^ EXCESS ^^^^^^^^^^^^^^^^
|
||||
/// ```
|
||||
///
|
||||
/// Proportional constraints have the lowest priority amongst all the constraints and hence
|
||||
/// will always take up any excess space available.
|
||||
///
|
||||
/// ```plain
|
||||
/// <----------------------------------- 80 px ------------------------------------>
|
||||
/// ┌──────20 px───────┐┌──────20 px───────┐┌──────20 px───────┐┌──────20 px───────┐
|
||||
/// │ Proportional(0) ││ Min(20) ││ Length(20) ││ Fixed(20) │
|
||||
/// └──────────────────┘└──────────────────┘└──────────────────┘└──────────────────┘
|
||||
/// ^^^^^^ EXCESS ^^^^^^
|
||||
/// ```
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```plain
|
||||
/// <------------------------------------80 px------------------------------------->
|
||||
/// ┌───────────30 px────────────┐┌───────────30 px────────────┐┌──────20 px───────┐
|
||||
/// │ Percentage(20) ││ Length(20) ││ Fixed(20) │
|
||||
/// └────────────────────────────┘└────────────────────────────┘└──────────────────┘
|
||||
///
|
||||
/// <------------------------------------80 px------------------------------------->
|
||||
/// ┌──────────────────────────60 px───────────────────────────┐┌──────20 px───────┐
|
||||
/// │ Min(20) ││ Max(20) │
|
||||
/// └──────────────────────────────────────────────────────────┘└──────────────────┘
|
||||
///
|
||||
/// <------------------------------------80 px------------------------------------->
|
||||
/// ┌────────────────────────────────────80 px─────────────────────────────────────┐
|
||||
/// │ Max(20) │
|
||||
/// └──────────────────────────────────────────────────────────────────────────────┘
|
||||
/// ```
|
||||
#[default]
|
||||
StretchLast,
|
||||
|
||||
/// Always fills the available space within the container.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```plain
|
||||
/// <------------------------------------80 px------------------------------------->
|
||||
/// ┌────16 px─────┐┌──────────────────44 px───────────────────┐┌──────20 px───────┐
|
||||
/// │Percentage(20)││ Length(20) ││ Fixed(20) │
|
||||
/// └──────────────┘└──────────────────────────────────────────┘└──────────────────┘
|
||||
///
|
||||
/// <------------------------------------80 px------------------------------------->
|
||||
/// ┌──────────────────────────60 px───────────────────────────┐┌──────20 px───────┐
|
||||
/// │ Min(20) ││ Max(20) │
|
||||
/// └──────────────────────────────────────────────────────────┘└──────────────────┘
|
||||
///
|
||||
/// <------------------------------------80 px------------------------------------->
|
||||
/// ┌────────────────────────────────────80 px─────────────────────────────────────┐
|
||||
/// │ Max(20) │
|
||||
/// └──────────────────────────────────────────────────────────────────────────────┘
|
||||
/// ```
|
||||
Stretch,
|
||||
|
||||
/// Aligns items to the start of the container.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```plain
|
||||
/// <------------------------------------80 px------------------------------------->
|
||||
/// ┌────16 px─────┐┌──────20 px───────┐┌──────20 px───────┐
|
||||
/// │Percentage(20)││ Length(20) ││ Fixed(20) │
|
||||
/// └──────────────┘└──────────────────┘└──────────────────┘
|
||||
///
|
||||
/// <------------------------------------80 px------------------------------------->
|
||||
/// ┌──────20 px───────┐┌──────20 px───────┐
|
||||
/// │ Min(20) ││ Max(20) │
|
||||
/// └──────────────────┘└──────────────────┘
|
||||
///
|
||||
/// <------------------------------------80 px------------------------------------->
|
||||
/// ┌──────20 px───────┐
|
||||
/// │ Max(20) │
|
||||
/// └──────────────────┘
|
||||
/// ```
|
||||
Start,
|
||||
|
||||
/// Aligns items to the end of the container.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```plain
|
||||
/// <------------------------------------80 px------------------------------------->
|
||||
/// ┌────16 px─────┐┌──────20 px───────┐┌──────20 px───────┐
|
||||
/// │Percentage(20)││ Length(20) ││ Fixed(20) │
|
||||
/// └──────────────┘└──────────────────┘└──────────────────┘
|
||||
///
|
||||
/// <------------------------------------80 px------------------------------------->
|
||||
/// ┌──────20 px───────┐┌──────20 px───────┐
|
||||
/// │ Min(20) ││ Max(20) │
|
||||
/// └──────────────────┘└──────────────────┘
|
||||
///
|
||||
/// <------------------------------------80 px------------------------------------->
|
||||
/// ┌──────20 px───────┐
|
||||
/// │ Max(20) │
|
||||
/// └──────────────────┘
|
||||
/// ```
|
||||
End,
|
||||
|
||||
/// Centers items within the container.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```plain
|
||||
/// <------------------------------------80 px------------------------------------->
|
||||
/// ┌────16 px─────┐┌──────20 px───────┐┌──────20 px───────┐
|
||||
/// │Percentage(20)││ Length(20) ││ Fixed(20) │
|
||||
/// └──────────────┘└──────────────────┘└──────────────────┘
|
||||
///
|
||||
/// <------------------------------------80 px------------------------------------->
|
||||
/// ┌──────20 px───────┐┌──────20 px───────┐
|
||||
/// │ Min(20) ││ Max(20) │
|
||||
/// └──────────────────┘└──────────────────┘
|
||||
///
|
||||
/// <------------------------------------80 px------------------------------------->
|
||||
/// ┌──────20 px───────┐
|
||||
/// │ Max(20) │
|
||||
/// └──────────────────┘
|
||||
/// ```
|
||||
Center,
|
||||
|
||||
/// Adds excess space between each element.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```plain
|
||||
///
|
||||
/// <------------------------------------80 px------------------------------------->
|
||||
/// ┌────16 px─────┐ ┌──────20 px───────┐ ┌──────20 px───────┐
|
||||
/// │Percentage(20)│ │ Length(20) │ │ Fixed(20) │
|
||||
/// └──────────────┘ └──────────────────┘ └──────────────────┘
|
||||
///
|
||||
/// <------------------------------------80 px------------------------------------->
|
||||
/// ┌──────20 px───────┐ ┌──────20 px───────┐
|
||||
/// │ Min(20) │ │ Max(20) │
|
||||
/// └──────────────────┘ └──────────────────┘
|
||||
///
|
||||
/// <------------------------------------80 px------------------------------------->
|
||||
/// ┌────────────────────────────────────80 px─────────────────────────────────────┐
|
||||
/// │ Max(20) │
|
||||
/// └──────────────────────────────────────────────────────────────────────────────┘
|
||||
/// ```
|
||||
SpaceBetween,
|
||||
|
||||
/// Adds excess space around each element.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```plain
|
||||
/// <------------------------------------80 px------------------------------------->
|
||||
/// ┌────16 px─────┐ ┌──────20 px───────┐ ┌──────20 px───────┐
|
||||
/// │Percentage(20)│ │ Length(20) │ │ Fixed(20) │
|
||||
/// └──────────────┘ └──────────────────┘ └──────────────────┘
|
||||
///
|
||||
/// <------------------------------------80 px------------------------------------->
|
||||
/// ┌──────20 px───────┐ ┌──────20 px───────┐
|
||||
/// │ Min(20) │ │ Max(20) │
|
||||
/// └──────────────────┘ └──────────────────┘
|
||||
///
|
||||
/// <------------------------------------80 px------------------------------------->
|
||||
/// ┌──────20 px───────┐
|
||||
/// │ Max(20) │
|
||||
/// └──────────────────┘
|
||||
/// ```
|
||||
SpaceAround,
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {}
|
||||
2240
src/layout/layout.rs
Normal file
2240
src/layout/layout.rs
Normal file
File diff suppressed because it is too large
Load Diff
43
src/layout/margin.rs
Normal file
43
src/layout/margin.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
use std::fmt::{self, Display};
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub struct Margin {
|
||||
pub horizontal: u16,
|
||||
pub vertical: u16,
|
||||
}
|
||||
|
||||
impl Margin {
|
||||
pub const fn new(horizontal: u16, vertical: u16) -> Margin {
|
||||
Margin {
|
||||
horizontal,
|
||||
vertical,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Margin {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}x{}", self.horizontal, self.vertical)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn margin_to_string() {
|
||||
assert_eq!(Margin::new(1, 2).to_string(), "1x2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn margin_new() {
|
||||
assert_eq!(
|
||||
Margin::new(1, 2),
|
||||
Margin {
|
||||
horizontal: 1,
|
||||
vertical: 2
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
97
src/layout/position.rs
Normal file
97
src/layout/position.rs
Normal file
@@ -0,0 +1,97 @@
|
||||
#![warn(missing_docs)]
|
||||
use crate::layout::Rect;
|
||||
|
||||
/// Position in the terminal
|
||||
///
|
||||
/// The position is relative to the top left corner of the terminal window, with the top left corner
|
||||
/// being (0, 0). The x axis is horizontal increasing to the right, and the y axis is vertical
|
||||
/// increasing downwards.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::layout::{Position, Rect};
|
||||
///
|
||||
/// // the following are all equivalent
|
||||
/// let position = Position { x: 1, y: 2 };
|
||||
/// let position = Position::new(1, 2);
|
||||
/// let position = Position::from((1, 2));
|
||||
/// let position = Position::from(Rect::new(1, 2, 3, 4));
|
||||
///
|
||||
/// // position can be converted back into the components when needed
|
||||
/// let (x, y) = position.into();
|
||||
/// ```
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct Position {
|
||||
/// The x coordinate of the position
|
||||
///
|
||||
/// The x coordinate is relative to the left edge of the terminal window, with the left edge
|
||||
/// being 0.
|
||||
pub x: u16,
|
||||
|
||||
/// The y coordinate of the position
|
||||
///
|
||||
/// The y coordinate is relative to the top edge of the terminal window, with the top edge
|
||||
/// being 0.
|
||||
pub y: u16,
|
||||
}
|
||||
|
||||
impl Position {
|
||||
/// Create a new position
|
||||
pub fn new(x: u16, y: u16) -> Self {
|
||||
Position { x, y }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(u16, u16)> for Position {
|
||||
fn from((x, y): (u16, u16)) -> Self {
|
||||
Position { x, y }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Position> for (u16, u16) {
|
||||
fn from(position: Position) -> Self {
|
||||
(position.x, position.y)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Rect> for Position {
|
||||
fn from(rect: Rect) -> Self {
|
||||
rect.as_position()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn new() {
|
||||
let position = Position::new(1, 2);
|
||||
assert_eq!(position.x, 1);
|
||||
assert_eq!(position.y, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_tuple() {
|
||||
let position = Position::from((1, 2));
|
||||
assert_eq!(position.x, 1);
|
||||
assert_eq!(position.y, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn into_tuple() {
|
||||
let position = Position::new(1, 2);
|
||||
let (x, y) = position.into();
|
||||
assert_eq!(x, 1);
|
||||
assert_eq!(y, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_rect() {
|
||||
let rect = Rect::new(1, 2, 3, 4);
|
||||
let position = Position::from(rect);
|
||||
assert_eq!(position.x, 1);
|
||||
assert_eq!(position.y, 2);
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,13 @@ use std::{
|
||||
fmt,
|
||||
};
|
||||
|
||||
use layout::{Position, Size};
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
mod offset;
|
||||
pub use offset::*;
|
||||
|
||||
/// A simple rectangle used in the computation of the layout and to give widgets a hint about the
|
||||
/// area they are supposed to render to.
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
@@ -21,6 +26,58 @@ pub struct Rect {
|
||||
pub height: u16,
|
||||
}
|
||||
|
||||
/// Manages row divisions within a `Rect`.
|
||||
///
|
||||
/// The `Rows` struct is an iterator that allows iterating through rows of a given `Rect`.
|
||||
pub struct Rows {
|
||||
/// The `Rect` associated with the rows.
|
||||
pub rect: Rect,
|
||||
/// The y coordinate of the row within the `Rect`.
|
||||
pub current_row: u16,
|
||||
}
|
||||
|
||||
impl Iterator for Rows {
|
||||
type Item = Rect;
|
||||
|
||||
/// Retrieves the next row within the `Rect`.
|
||||
///
|
||||
/// Returns `None` when there are no more rows to iterate through.
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.current_row >= self.rect.bottom() {
|
||||
return None;
|
||||
}
|
||||
let row = Rect::new(self.rect.x, self.current_row, self.rect.width, 1);
|
||||
self.current_row += 1;
|
||||
Some(row)
|
||||
}
|
||||
}
|
||||
|
||||
/// Manages column divisions within a `Rect`.
|
||||
///
|
||||
/// The `Columns` struct is an iterator that allows iterating through columns of a given `Rect`.
|
||||
pub struct Columns {
|
||||
/// The `Rect` associated with the columns.
|
||||
pub rect: Rect,
|
||||
/// The x coordinate of the column within the `Rect`.
|
||||
pub current_column: u16,
|
||||
}
|
||||
|
||||
impl Iterator for Columns {
|
||||
type Item = Rect;
|
||||
|
||||
/// Retrieves the next column within the `Rect`.
|
||||
///
|
||||
/// Returns `None` when there are no more columns to iterate through.
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.current_column >= self.rect.right() {
|
||||
return None;
|
||||
}
|
||||
let column = Rect::new(self.current_column, self.rect.y, 1, self.rect.height);
|
||||
self.current_column += 1;
|
||||
Some(column)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Rect {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}x{}+{}+{}", self.width, self.height, self.x, self.y)
|
||||
@@ -106,6 +163,26 @@ impl Rect {
|
||||
}
|
||||
}
|
||||
|
||||
/// Moves the `Rect` without modifying its size.
|
||||
///
|
||||
/// Moves the `Rect` according to the given offset without modifying its [`width`](Rect::width)
|
||||
/// or [`height`](Rect::height).
|
||||
/// - Positive `x` moves the whole `Rect` to the right, negative to the left.
|
||||
/// - Positive `y` moves the whole `Rect` to the bottom, negative to the top.
|
||||
///
|
||||
/// See [`Offset`] for details.
|
||||
pub fn offset(self, offset: Offset) -> Rect {
|
||||
Rect {
|
||||
x: i32::from(self.x)
|
||||
.saturating_add(offset.x)
|
||||
.clamp(0, (u16::MAX - self.width) as i32) as u16,
|
||||
y: i32::from(self.y)
|
||||
.saturating_add(offset.y)
|
||||
.clamp(0, (u16::MAX - self.height) as i32) as u16,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a new rect that contains both the current one and the given one.
|
||||
pub fn union(self, other: Rect) -> Rect {
|
||||
let x1 = min(self.x, other.x);
|
||||
@@ -131,8 +208,8 @@ impl Rect {
|
||||
Rect {
|
||||
x: x1,
|
||||
y: y1,
|
||||
width: x2 - x1,
|
||||
height: y2 - y1,
|
||||
width: x2.saturating_sub(x1),
|
||||
height: y2.saturating_sub(y1),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,10 +220,153 @@ impl Rect {
|
||||
&& self.y < other.bottom()
|
||||
&& self.bottom() > other.y
|
||||
}
|
||||
|
||||
/// Split the rect into a number of sub-rects according to the given [`Layout`]`.
|
||||
///
|
||||
/// An ergonomic wrapper around [`Layout::split`] that returns an array of `Rect`s instead of
|
||||
/// `Rc<[Rect]>`.
|
||||
///
|
||||
/// This method requires the number of constraints to be known at compile time. If you don't
|
||||
/// know the number of constraints at compile time, use [`Layout::split`] instead.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the number of constraints is not equal to the length of the returned array.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// # fn render(frame: &mut Frame) {
|
||||
/// let area = frame.size();
|
||||
/// let layout = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
|
||||
/// let [top, main] = area.split(&layout);
|
||||
/// // or explicitly specify the number of constraints:
|
||||
/// let rects = area.split::<2>(&layout);
|
||||
/// # }
|
||||
pub fn split<const N: usize>(self, layout: &Layout) -> [Rect; N] {
|
||||
layout
|
||||
.split(self)
|
||||
.to_vec()
|
||||
.try_into()
|
||||
.expect("invalid number of rects")
|
||||
}
|
||||
|
||||
/// Clamp this rect to fit inside the other rect.
|
||||
///
|
||||
/// If the width or height of this rect is larger than the other rect, it will be clamped to the
|
||||
/// other rect's width or height.
|
||||
///
|
||||
/// If the left or top coordinate of this rect is smaller than the other rect, it will be
|
||||
/// clamped to the other rect's left or top coordinate.
|
||||
///
|
||||
/// If the right or bottom coordinate of this rect is larger than the other rect, it will be
|
||||
/// clamped to the other rect's right or bottom coordinate.
|
||||
///
|
||||
/// This is different from [`Rect::intersection`] because it will move this rect to fit inside
|
||||
/// the other rect, while [`Rect::intersection`] instead would keep this rect's position and
|
||||
/// truncate its size to only that which is inside the other rect.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// # fn render(frame: &mut Frame) {
|
||||
/// let area = frame.size();
|
||||
/// let rect = Rect::new(0, 0, 100, 100).clamp(area);
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn clamp(self, other: Rect) -> Rect {
|
||||
let width = self.width.min(other.width);
|
||||
let height = self.height.min(other.height);
|
||||
let x = self.x.clamp(other.x, other.right().saturating_sub(width));
|
||||
let y = self.y.clamp(other.y, other.bottom().saturating_sub(height));
|
||||
Rect::new(x, y, width, height)
|
||||
}
|
||||
|
||||
/// Creates an iterator over rows within the `Rect`.
|
||||
///
|
||||
/// This method returns a `Rows` iterator that allows iterating through rows of the `Rect`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::prelude::*;
|
||||
/// let area = Rect::new(0, 0, 10, 5);
|
||||
/// for row in area.rows() {
|
||||
/// // Perform operations on each row of the area
|
||||
/// println!("Row: {:?}", row);
|
||||
/// }
|
||||
/// ```
|
||||
pub fn rows(&self) -> Rows {
|
||||
Rows {
|
||||
rect: *self,
|
||||
current_row: self.y,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an iterator over columns within the `Rect`.
|
||||
///
|
||||
/// This method returns a `Columns` iterator that allows iterating through columns of the
|
||||
/// `Rect`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::prelude::*;
|
||||
/// let area = Rect::new(0, 0, 10, 5);
|
||||
/// for column in area.columns() {
|
||||
/// // Perform operations on each column of the area
|
||||
/// println!("Column: {:?}", column);
|
||||
/// }
|
||||
/// ```
|
||||
pub fn columns(&self) -> Columns {
|
||||
Columns {
|
||||
rect: *self,
|
||||
current_column: self.x,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a [`Position`] with the same coordinates as this rect.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let rect = Rect::new(1, 2, 3, 4);
|
||||
/// let position = rect.as_position();
|
||||
/// ````
|
||||
pub fn as_position(self) -> Position {
|
||||
Position {
|
||||
x: self.x,
|
||||
y: self.y,
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts the rect into a size struct.
|
||||
pub fn as_size(self) -> Size {
|
||||
Size {
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(Position, Size)> for Rect {
|
||||
fn from((position, size): (Position, Size)) -> Self {
|
||||
Rect {
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
@@ -207,6 +427,39 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offset() {
|
||||
assert_eq!(
|
||||
Rect::new(1, 2, 3, 4).offset(Offset { x: 5, y: 6 }),
|
||||
Rect::new(6, 8, 3, 4),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn negative_offset() {
|
||||
assert_eq!(
|
||||
Rect::new(4, 3, 3, 4).offset(Offset { x: -2, y: -1 }),
|
||||
Rect::new(2, 2, 3, 4),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn negative_offset_saturate() {
|
||||
assert_eq!(
|
||||
Rect::new(1, 2, 3, 4).offset(Offset { x: -5, y: -6 }),
|
||||
Rect::new(0, 0, 3, 4),
|
||||
);
|
||||
}
|
||||
|
||||
/// Offsets a [`Rect`] making it go outside [`u16::MAX`], it should keep its size.
|
||||
#[test]
|
||||
fn offset_saturate_max() {
|
||||
assert_eq!(
|
||||
Rect::new(u16::MAX - 500, u16::MAX - 500, 100, 100).offset(Offset { x: 1000, y: 1000 }),
|
||||
Rect::new(u16::MAX - 100, u16::MAX - 100, 100, 100),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn union() {
|
||||
assert_eq!(
|
||||
@@ -223,6 +476,14 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersection_underflow() {
|
||||
assert_eq!(
|
||||
Rect::new(1, 1, 2, 2).intersection(Rect::new(4, 4, 2, 2)),
|
||||
Rect::new(4, 4, 0, 0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersects() {
|
||||
assert!(Rect::new(1, 2, 3, 4).intersects(Rect::new(2, 3, 4, 5)));
|
||||
@@ -288,4 +549,98 @@ mod tests {
|
||||
const _BOTTOM: u16 = RECT.bottom();
|
||||
assert!(RECT.intersects(RECT));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split() {
|
||||
let layout = Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]);
|
||||
let [a, b] = Rect::new(0, 0, 2, 1).split(&layout);
|
||||
assert_eq!(a, Rect::new(0, 0, 1, 1));
|
||||
assert_eq!(b, Rect::new(1, 0, 1, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "invalid number of rects")]
|
||||
fn split_invalid_number_of_recs() {
|
||||
let layout = Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]);
|
||||
let [_a, _b, _c] = Rect::new(0, 0, 2, 1).split(&layout);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::inside(Rect::new(20, 20, 10, 10), Rect::new(20, 20, 10, 10))]
|
||||
#[case::up_left(Rect::new(5, 5, 10, 10), Rect::new(10, 10, 10, 10))]
|
||||
#[case::up(Rect::new(20, 5, 10, 10), Rect::new(20, 10, 10, 10))]
|
||||
#[case::up_right(Rect::new(105, 5, 10, 10), Rect::new(100, 10, 10, 10))]
|
||||
#[case::left(Rect::new(5, 20, 10, 10), Rect::new(10, 20, 10, 10))]
|
||||
#[case::right(Rect::new(105, 20, 10, 10), Rect::new(100, 20, 10, 10))]
|
||||
#[case::down_left(Rect::new(5, 105, 10, 10), Rect::new(10, 100, 10, 10))]
|
||||
#[case::down(Rect::new(20, 105, 10, 10), Rect::new(20, 100, 10, 10))]
|
||||
#[case::down_right(Rect::new(105, 105, 10, 10), Rect::new(100, 100, 10, 10))]
|
||||
#[case::too_wide(Rect::new(5, 20, 200, 10), Rect::new(10, 20, 100, 10))]
|
||||
#[case::too_tall(Rect::new(20, 5, 10, 200), Rect::new(20, 10, 10, 100))]
|
||||
#[case::too_large(Rect::new(0, 0, 200, 200), Rect::new(10, 10, 100, 100))]
|
||||
fn clamp(#[case] rect: Rect, #[case] expected: Rect) {
|
||||
let other = Rect::new(10, 10, 100, 100);
|
||||
assert_eq!(rect.clamp(other), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rows() {
|
||||
let area = Rect::new(0, 0, 3, 2);
|
||||
let rows: Vec<Rect> = area.rows().collect();
|
||||
|
||||
let expected_rows: Vec<Rect> = vec![Rect::new(0, 0, 3, 1), Rect::new(0, 1, 3, 1)];
|
||||
|
||||
assert_eq!(rows, expected_rows);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn columns() {
|
||||
let area = Rect::new(0, 0, 3, 2);
|
||||
let columns: Vec<Rect> = area.columns().collect();
|
||||
|
||||
let expected_columns: Vec<Rect> = vec![
|
||||
Rect::new(0, 0, 1, 2),
|
||||
Rect::new(1, 0, 1, 2),
|
||||
Rect::new(2, 0, 1, 2),
|
||||
];
|
||||
|
||||
assert_eq!(columns, expected_columns);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn as_position() {
|
||||
let rect = Rect::new(1, 2, 3, 4);
|
||||
let position = rect.as_position();
|
||||
assert_eq!(position.x, 1);
|
||||
assert_eq!(position.y, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn as_size() {
|
||||
assert_eq!(
|
||||
Rect::new(1, 2, 3, 4).as_size(),
|
||||
Size {
|
||||
width: 3,
|
||||
height: 4
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_position_and_size() {
|
||||
let position = Position { x: 1, y: 2 };
|
||||
let size = Size {
|
||||
width: 3,
|
||||
height: 4,
|
||||
};
|
||||
assert_eq!(
|
||||
Rect::from((position, size)),
|
||||
Rect {
|
||||
x: 1,
|
||||
y: 2,
|
||||
width: 3,
|
||||
height: 4
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
12
src/layout/rect/offset.rs
Normal file
12
src/layout/rect/offset.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
/// Amounts by which to move a [`Rect`](super::Rect).
|
||||
///
|
||||
/// Positive numbers move to the right/bottom and negative to the left/top.
|
||||
///
|
||||
/// See [`Rect::offset`](super::Rect::offset)
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub struct Offset {
|
||||
/// How much to move on the X axis
|
||||
pub x: i32,
|
||||
/// How much to move on the Y axis
|
||||
pub y: i32,
|
||||
}
|
||||
147
src/layout/segment_size.rs
Normal file
147
src/layout/segment_size.rs
Normal file
@@ -0,0 +1,147 @@
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
/// Option for segment size preferences
|
||||
///
|
||||
/// This controls how the space is distributed when the constraints are satisfied. By default, the
|
||||
/// last chunk is expanded to fill the remaining space, but this can be changed to prefer equal
|
||||
/// chunks or to not distribute extra space at all (which is the default used for laying out the
|
||||
/// columns for [`Table`] widgets).
|
||||
///
|
||||
/// Note: If you're using this feature please help us come up with a good name. See [Issue
|
||||
/// #536](https://github.com/ratatui-org/ratatui/issues/536) for more information.
|
||||
///
|
||||
/// [`Table`]: crate::widgets::Table
|
||||
#[stability::unstable(
|
||||
feature = "segment-size",
|
||||
reason = "The name for this feature is not final and may change in the future",
|
||||
issue = "https://github.com/ratatui-org/ratatui/issues/536"
|
||||
)]
|
||||
#[derive(Copy, Debug, Default, Display, EnumString, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum SegmentSize {
|
||||
/// prefer equal chunks if other constraints are all satisfied
|
||||
EvenDistribution,
|
||||
|
||||
/// the last chunk is expanded to fill the remaining space
|
||||
#[default]
|
||||
LastTakesRemainder,
|
||||
|
||||
/// extra space is not distributed
|
||||
None,
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use strum::ParseError;
|
||||
|
||||
use super::{SegmentSize::*, *};
|
||||
use crate::prelude::{Constraint::*, *};
|
||||
#[test]
|
||||
fn segment_size_to_string() {
|
||||
assert_eq!(EvenDistribution.to_string(), "EvenDistribution");
|
||||
assert_eq!(LastTakesRemainder.to_string(), "LastTakesRemainder");
|
||||
assert_eq!(None.to_string(), "None");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn segment_size_from_string() {
|
||||
assert_eq!(
|
||||
"EvenDistribution".parse::<SegmentSize>(),
|
||||
Ok(EvenDistribution)
|
||||
);
|
||||
assert_eq!(
|
||||
"LastTakesRemainder".parse::<SegmentSize>(),
|
||||
Ok(LastTakesRemainder)
|
||||
);
|
||||
assert_eq!("None".parse::<SegmentSize>(), Ok(None));
|
||||
assert_eq!("".parse::<SegmentSize>(), Err(ParseError::VariantNotFound));
|
||||
}
|
||||
|
||||
fn get_x_width_with_segment_size(
|
||||
segment_size: SegmentSize,
|
||||
constraints: Vec<Constraint>,
|
||||
target: Rect,
|
||||
) -> Vec<(u16, u16)> {
|
||||
#[allow(deprecated)]
|
||||
let layout = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(constraints)
|
||||
.segment_size(segment_size);
|
||||
let chunks = layout.split(target);
|
||||
chunks.iter().map(|r| (r.x, r.width)).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_split_equally_in_underspecified_case() {
|
||||
let target = Rect::new(100, 200, 10, 10);
|
||||
assert_eq!(
|
||||
get_x_width_with_segment_size(LastTakesRemainder, vec![Min(2), Min(2), Min(0)], target),
|
||||
[(100, 2), (102, 2), (104, 6)]
|
||||
);
|
||||
assert_eq!(
|
||||
get_x_width_with_segment_size(EvenDistribution, vec![Min(2), Min(2), Min(0)], target),
|
||||
[(100, 3), (103, 4), (107, 3)]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_split_equally_in_overconstrained_case_for_min() {
|
||||
let target = Rect::new(100, 200, 100, 10);
|
||||
assert_eq!(
|
||||
get_x_width_with_segment_size(
|
||||
LastTakesRemainder,
|
||||
vec![Percentage(50), Min(10), Percentage(50)],
|
||||
target
|
||||
),
|
||||
[(100, 50), (150, 10), (160, 40)]
|
||||
);
|
||||
assert_eq!(
|
||||
get_x_width_with_segment_size(
|
||||
EvenDistribution,
|
||||
vec![Percentage(50), Min(10), Percentage(50)],
|
||||
target
|
||||
),
|
||||
[(100, 45), (145, 10), (155, 45)]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_split_equally_in_overconstrained_case_for_max() {
|
||||
let target = Rect::new(100, 200, 100, 10);
|
||||
assert_eq!(
|
||||
get_x_width_with_segment_size(
|
||||
LastTakesRemainder,
|
||||
vec![Percentage(30), Max(10), Percentage(30)],
|
||||
target
|
||||
),
|
||||
[(100, 30), (130, 10), (140, 60)]
|
||||
);
|
||||
assert_eq!(
|
||||
get_x_width_with_segment_size(
|
||||
EvenDistribution,
|
||||
vec![Percentage(30), Max(10), Percentage(30)],
|
||||
target
|
||||
),
|
||||
[(100, 45), (145, 10), (155, 45)]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_split_equally_in_overconstrained_case_for_length() {
|
||||
let target = Rect::new(100, 200, 100, 10);
|
||||
assert_eq!(
|
||||
get_x_width_with_segment_size(
|
||||
LastTakesRemainder,
|
||||
vec![Percentage(50), Length(10), Percentage(50)],
|
||||
target
|
||||
),
|
||||
[(100, 50), (150, 10), (160, 40)]
|
||||
);
|
||||
assert_eq!(
|
||||
get_x_width_with_segment_size(
|
||||
EvenDistribution,
|
||||
vec![Percentage(50), Length(10), Percentage(50)],
|
||||
target
|
||||
),
|
||||
[(100, 45), (145, 10), (155, 45)]
|
||||
);
|
||||
}
|
||||
}
|
||||
59
src/layout/size.rs
Normal file
59
src/layout/size.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
#![warn(missing_docs)]
|
||||
use crate::prelude::*;
|
||||
|
||||
/// A simple size struct
|
||||
///
|
||||
/// The width and height are stored as `u16` values and represent the number of columns and rows
|
||||
/// respectively.
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub struct Size {
|
||||
/// The width in columns
|
||||
pub width: u16,
|
||||
/// The height in rows
|
||||
pub height: u16,
|
||||
}
|
||||
|
||||
impl Size {
|
||||
/// Create a new `Size` struct
|
||||
pub fn new(width: u16, height: u16) -> Self {
|
||||
Size { width, height }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(u16, u16)> for Size {
|
||||
fn from((width, height): (u16, u16)) -> Self {
|
||||
Size { width, height }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Rect> for Size {
|
||||
fn from(rect: Rect) -> Self {
|
||||
rect.as_size()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn new() {
|
||||
let size = Size::new(10, 20);
|
||||
assert_eq!(size.width, 10);
|
||||
assert_eq!(size.height, 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_tuple() {
|
||||
let size = Size::from((10, 20));
|
||||
assert_eq!(size.width, 10);
|
||||
assert_eq!(size.height, 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_rect() {
|
||||
let size = Size::from(Rect::new(0, 0, 10, 20));
|
||||
assert_eq!(size.width, 10);
|
||||
assert_eq!(size.height, 20);
|
||||
}
|
||||
}
|
||||
146
src/lib.rs
146
src/lib.rs
@@ -1,30 +1,24 @@
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
//! 
|
||||
//! 
|
||||
//!
|
||||
//! <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)<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
|
||||
//!
|
||||
@@ -35,7 +29,7 @@
|
||||
//! ```
|
||||
//!
|
||||
//! 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
|
||||
@@ -43,29 +37,26 @@
|
||||
//! 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.
|
||||
//!
|
||||
//! ## 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:
|
||||
//!
|
||||
@@ -89,30 +80,31 @@
|
||||
//!
|
||||
//! 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,no_run
|
||||
//! 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::*};
|
||||
//!
|
||||
@@ -138,7 +130,7 @@
|
||||
//! if key.kind == event::KeyEventKind::Press && key.code == KeyCode::Char('q') {
|
||||
//! return Ok(true);
|
||||
//! }
|
||||
//! }
|
||||
//! }
|
||||
//! }
|
||||
//! Ok(false)
|
||||
//! }
|
||||
@@ -161,20 +153,21 @@
|
||||
//! 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,no_run
|
||||
//! 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],
|
||||
@@ -184,10 +177,11 @@
|
||||
//! 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],
|
||||
@@ -213,22 +207,23 @@
|
||||
//! 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,no_run
|
||||
//! 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(
|
||||
@@ -282,23 +277,24 @@
|
||||
doc = "[`calendar`]: widgets::calendar::Monthly"
|
||||
)]
|
||||
//!
|
||||
//! [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
|
||||
@@ -318,24 +314,28 @@
|
||||
//! [`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
|
||||
//! [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
|
||||
|
||||
// show the feature flags in the generated documentation
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
|
||||
307
src/style.rs
307
src/style.rs
@@ -16,9 +16,9 @@
|
||||
//! use ratatui::prelude::*;
|
||||
//!
|
||||
//! let heading_style = Style::new()
|
||||
//! .fg(Color::Black)
|
||||
//! .bg(Color::Green)
|
||||
//! .add_modifier(Modifier::ITALIC | Modifier::BOLD);
|
||||
//! .fg(Color::Black)
|
||||
//! .bg(Color::Green)
|
||||
//! .add_modifier(Modifier::ITALIC | Modifier::BOLD);
|
||||
//! let span = Span::styled("hello", heading_style);
|
||||
//! ```
|
||||
//!
|
||||
@@ -44,16 +44,24 @@
|
||||
//! use ratatui::{prelude::*, widgets::*};
|
||||
//!
|
||||
//! assert_eq!(
|
||||
//! "hello".red().on_blue().bold(),
|
||||
//! "hello".red().on_blue().bold(),
|
||||
//! Span::styled(
|
||||
//! "hello",
|
||||
//! Style::default().fg(Color::Red).bg(Color::Blue).add_modifier(Modifier::BOLD))
|
||||
//! Style::default()
|
||||
//! .fg(Color::Red)
|
||||
//! .bg(Color::Blue)
|
||||
//! .add_modifier(Modifier::BOLD)
|
||||
//! )
|
||||
//! );
|
||||
//!
|
||||
//! assert_eq!(
|
||||
//! Paragraph::new("hello").red().on_blue().bold(),
|
||||
//! Paragraph::new("hello")
|
||||
//! .style(Style::default().fg(Color::Red).bg(Color::Blue).add_modifier(Modifier::BOLD))
|
||||
//! Paragraph::new("hello").style(
|
||||
//! Style::default()
|
||||
//! .fg(Color::Red)
|
||||
//! .bg(Color::Blue)
|
||||
//! .add_modifier(Modifier::BOLD)
|
||||
//! )
|
||||
//! );
|
||||
//! ```
|
||||
//!
|
||||
@@ -64,16 +72,21 @@ use std::fmt::{self, Debug};
|
||||
|
||||
use bitflags::bitflags;
|
||||
|
||||
mod stylize;
|
||||
pub use stylize::{Styled, Stylize};
|
||||
mod color;
|
||||
mod stylize;
|
||||
|
||||
pub use color::Color;
|
||||
pub use stylize::{Styled, Stylize};
|
||||
pub mod palette;
|
||||
|
||||
bitflags! {
|
||||
/// Modifier changes the way a piece of text is displayed.
|
||||
///
|
||||
/// They are bitflags so they can easily be composed.
|
||||
///
|
||||
/// `From<Modifier> for Style` is implemented so you can use `Modifier` anywhere that accepts
|
||||
/// `Into<Style>`.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
@@ -113,7 +126,7 @@ impl fmt::Debug for Modifier {
|
||||
/// Style lets you control the main characteristics of the displayed elements.
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::{prelude::*};
|
||||
/// use ratatui::prelude::*;
|
||||
///
|
||||
/// Style::default()
|
||||
/// .fg(Color::Black)
|
||||
@@ -130,23 +143,43 @@ impl fmt::Debug for Modifier {
|
||||
///
|
||||
/// For more information about the style shorthands, see the [`Stylize`] trait.
|
||||
///
|
||||
/// We implement conversions from [`Color`] and [`Modifier`] to [`Style`] so you can use them
|
||||
/// anywhere that accepts `Into<Style>`.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// Line::styled("hello", Style::new().fg(Color::Red));
|
||||
/// // simplifies to
|
||||
/// Line::styled("hello", Color::Red);
|
||||
///
|
||||
/// Line::styled("hello", Style::new().add_modifier(Modifier::BOLD));
|
||||
/// // simplifies to
|
||||
/// Line::styled("hello", Modifier::BOLD);
|
||||
/// ```
|
||||
///
|
||||
/// Styles represents an incremental change. If you apply the styles S1, S2, S3 to a cell of the
|
||||
/// terminal buffer, the style of this cell will be the result of the merge of S1, S2 and S3, not
|
||||
/// just S3.
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::{prelude::*};
|
||||
/// use ratatui::prelude::*;
|
||||
///
|
||||
/// let styles = [
|
||||
/// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC),
|
||||
/// Style::default().bg(Color::Red).add_modifier(Modifier::UNDERLINED),
|
||||
/// Style::default()
|
||||
/// .fg(Color::Blue)
|
||||
/// .add_modifier(Modifier::BOLD | Modifier::ITALIC),
|
||||
/// Style::default()
|
||||
/// .bg(Color::Red)
|
||||
/// .add_modifier(Modifier::UNDERLINED),
|
||||
/// #[cfg(feature = "underline-color")]
|
||||
/// Style::default().underline_color(Color::Green),
|
||||
/// Style::default().fg(Color::Yellow).remove_modifier(Modifier::ITALIC),
|
||||
/// Style::default()
|
||||
/// .fg(Color::Yellow)
|
||||
/// .remove_modifier(Modifier::ITALIC),
|
||||
/// ];
|
||||
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
|
||||
/// for style in &styles {
|
||||
/// buffer.get_mut(0, 0).set_style(*style);
|
||||
/// buffer.get_mut(0, 0).set_style(*style);
|
||||
/// }
|
||||
/// assert_eq!(
|
||||
/// Style {
|
||||
@@ -165,15 +198,17 @@ impl fmt::Debug for Modifier {
|
||||
/// reset all properties until that point use [`Style::reset`].
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::{prelude::*};
|
||||
/// use ratatui::prelude::*;
|
||||
///
|
||||
/// let styles = [
|
||||
/// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC),
|
||||
/// Style::default()
|
||||
/// .fg(Color::Blue)
|
||||
/// .add_modifier(Modifier::BOLD | Modifier::ITALIC),
|
||||
/// Style::reset().fg(Color::Yellow),
|
||||
/// ];
|
||||
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
|
||||
/// for style in &styles {
|
||||
/// buffer.get_mut(0, 0).set_style(*style);
|
||||
/// buffer.get_mut(0, 0).set_style(*style);
|
||||
/// }
|
||||
/// assert_eq!(
|
||||
/// Style {
|
||||
@@ -211,10 +246,11 @@ impl Styled for Style {
|
||||
*self
|
||||
}
|
||||
|
||||
fn set_style(self, style: Style) -> Self::Item {
|
||||
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
|
||||
self.patch(style)
|
||||
}
|
||||
}
|
||||
|
||||
impl Style {
|
||||
pub const fn new() -> Style {
|
||||
Style {
|
||||
@@ -249,6 +285,7 @@ impl Style {
|
||||
/// let diff = Style::default().fg(Color::Red);
|
||||
/// assert_eq!(style.patch(diff), Style::default().fg(Color::Red));
|
||||
/// ```
|
||||
#[must_use = "`fg` returns the modified style without modifying the original"]
|
||||
pub const fn fg(mut self, color: Color) -> Style {
|
||||
self.fg = Some(color);
|
||||
self
|
||||
@@ -264,6 +301,7 @@ impl Style {
|
||||
/// let diff = Style::default().bg(Color::Red);
|
||||
/// assert_eq!(style.patch(diff), Style::default().bg(Color::Red));
|
||||
/// ```
|
||||
#[must_use = "`bg` returns the modified style without modifying the original"]
|
||||
pub const fn bg(mut self, color: Color) -> Style {
|
||||
self.bg = Some(color);
|
||||
self
|
||||
@@ -283,11 +321,21 @@ impl Style {
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let style = Style::default().underline_color(Color::Blue).add_modifier(Modifier::UNDERLINED);
|
||||
/// let diff = Style::default().underline_color(Color::Red).add_modifier(Modifier::UNDERLINED);
|
||||
/// assert_eq!(style.patch(diff), Style::default().underline_color(Color::Red).add_modifier(Modifier::UNDERLINED));
|
||||
/// let style = Style::default()
|
||||
/// .underline_color(Color::Blue)
|
||||
/// .add_modifier(Modifier::UNDERLINED);
|
||||
/// let diff = Style::default()
|
||||
/// .underline_color(Color::Red)
|
||||
/// .add_modifier(Modifier::UNDERLINED);
|
||||
/// assert_eq!(
|
||||
/// style.patch(diff),
|
||||
/// Style::default()
|
||||
/// .underline_color(Color::Red)
|
||||
/// .add_modifier(Modifier::UNDERLINED)
|
||||
/// );
|
||||
/// ```
|
||||
#[cfg(feature = "underline-color")]
|
||||
#[must_use = "`underline_color` returns the modified style without modifying the original"]
|
||||
pub const fn underline_color(mut self, color: Color) -> Style {
|
||||
self.underline_color = Some(color);
|
||||
self
|
||||
@@ -307,6 +355,7 @@ impl Style {
|
||||
/// assert_eq!(patched.add_modifier, Modifier::BOLD | Modifier::ITALIC);
|
||||
/// assert_eq!(patched.sub_modifier, Modifier::empty());
|
||||
/// ```
|
||||
#[must_use = "`add_modifier` returns the modified style without modifying the original"]
|
||||
pub const fn add_modifier(mut self, modifier: Modifier) -> Style {
|
||||
self.sub_modifier = self.sub_modifier.difference(modifier);
|
||||
self.add_modifier = self.add_modifier.union(modifier);
|
||||
@@ -327,6 +376,7 @@ impl Style {
|
||||
/// assert_eq!(patched.add_modifier, Modifier::BOLD);
|
||||
/// assert_eq!(patched.sub_modifier, Modifier::ITALIC);
|
||||
/// ```
|
||||
#[must_use = "`remove_modifier` returns the modified style without modifying the original"]
|
||||
pub const fn remove_modifier(mut self, modifier: Modifier) -> Style {
|
||||
self.add_modifier = self.add_modifier.difference(modifier);
|
||||
self.sub_modifier = self.sub_modifier.union(modifier);
|
||||
@@ -336,6 +386,9 @@ impl Style {
|
||||
/// Results in a combined style that is equivalent to applying the two individual styles to
|
||||
/// a style one after the other.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```
|
||||
/// # use ratatui::prelude::*;
|
||||
@@ -344,9 +397,12 @@ impl Style {
|
||||
/// let combined = style_1.patch(style_2);
|
||||
/// assert_eq!(
|
||||
/// Style::default().patch(style_1).patch(style_2),
|
||||
/// Style::default().patch(combined));
|
||||
/// Style::default().patch(combined)
|
||||
/// );
|
||||
/// ```
|
||||
pub fn patch(mut self, other: Style) -> Style {
|
||||
#[must_use = "`patch` returns the modified style without modifying the original"]
|
||||
pub fn patch<S: Into<Style>>(mut self, other: S) -> Style {
|
||||
let other = other.into();
|
||||
self.fg = other.fg.or(self.fg);
|
||||
self.bg = other.bg.or(self.bg);
|
||||
|
||||
@@ -364,6 +420,134 @@ impl Style {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color> for Style {
|
||||
/// Creates a new `Style` with the given foreground color.
|
||||
///
|
||||
/// To specify a foreground and background color, use the `from((fg, bg))` constructor.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let style = Style::from(Color::Red);
|
||||
/// ```
|
||||
fn from(color: Color) -> Self {
|
||||
Self::new().fg(color)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(Color, Color)> for Style {
|
||||
/// Creates a new `Style` with the given foreground and background colors.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// // red foreground, blue background
|
||||
/// let style = Style::from((Color::Red, Color::Blue));
|
||||
/// // default foreground, blue background
|
||||
/// let style = Style::from((Color::Reset, Color::Blue));
|
||||
/// ```
|
||||
fn from((fg, bg): (Color, Color)) -> Self {
|
||||
Self::new().fg(fg).bg(bg)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Modifier> for Style {
|
||||
/// Creates a new `Style` with the given modifier added.
|
||||
///
|
||||
/// To specify multiple modifiers, use the `|` operator.
|
||||
///
|
||||
/// To specify modifiers to add and remove, use the `from((add_modifier, sub_modifier))`
|
||||
/// constructor.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// // add bold and italic
|
||||
/// let style = Style::from(Modifier::BOLD|Modifier::ITALIC);
|
||||
fn from(modifier: Modifier) -> Self {
|
||||
Self::new().add_modifier(modifier)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(Modifier, Modifier)> for Style {
|
||||
/// Creates a new `Style` with the given modifiers added and removed.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// // add bold and italic, remove dim
|
||||
/// let style = Style::from((Modifier::BOLD | Modifier::ITALIC, Modifier::DIM));
|
||||
/// ```
|
||||
fn from((add_modifier, sub_modifier): (Modifier, Modifier)) -> Self {
|
||||
Self::new()
|
||||
.add_modifier(add_modifier)
|
||||
.remove_modifier(sub_modifier)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(Color, Modifier)> for Style {
|
||||
/// Creates a new `Style` with the given foreground color and modifier added.
|
||||
///
|
||||
/// To specify multiple modifiers, use the `|` operator.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// // red foreground, add bold and italic
|
||||
/// let style = Style::from((Color::Red, Modifier::BOLD | Modifier::ITALIC));
|
||||
/// ```
|
||||
fn from((fg, modifier): (Color, Modifier)) -> Self {
|
||||
Self::new().fg(fg).add_modifier(modifier)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(Color, Color, Modifier)> for Style {
|
||||
/// Creates a new `Style` with the given foreground and background colors and modifier added.
|
||||
///
|
||||
/// To specify multiple modifiers, use the `|` operator.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// // red foreground, blue background, add bold and italic
|
||||
/// let style = Style::from((Color::Red, Color::Blue, Modifier::BOLD | Modifier::ITALIC));
|
||||
/// ```
|
||||
fn from((fg, bg, modifier): (Color, Color, Modifier)) -> Self {
|
||||
Self::new().fg(fg).bg(bg).add_modifier(modifier)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(Color, Color, Modifier, Modifier)> for Style {
|
||||
/// Creates a new `Style` with the given foreground and background colors and modifiers added
|
||||
/// and removed.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// // red foreground, blue background, add bold and italic, remove dim
|
||||
/// let style = Style::from((
|
||||
/// Color::Red,
|
||||
/// Color::Blue,
|
||||
/// Modifier::BOLD | Modifier::ITALIC,
|
||||
/// Modifier::DIM,
|
||||
/// ));
|
||||
/// ```
|
||||
fn from((fg, bg, add_modifier, sub_modifier): (Color, Color, Modifier, Modifier)) -> Self {
|
||||
Self::new()
|
||||
.fg(fg)
|
||||
.bg(bg)
|
||||
.add_modifier(add_modifier)
|
||||
.remove_modifier(sub_modifier)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -625,4 +809,79 @@ mod tests {
|
||||
// reset
|
||||
assert_eq!(Style::new().reset(), Style::reset());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_color() {
|
||||
assert_eq!(Style::from(Color::Red), Style::new().fg(Color::Red));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_color_color() {
|
||||
assert_eq!(
|
||||
Style::from((Color::Red, Color::Blue)),
|
||||
Style::new().fg(Color::Red).bg(Color::Blue)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_modifier() {
|
||||
assert_eq!(
|
||||
Style::from(Modifier::BOLD | Modifier::ITALIC),
|
||||
Style::new()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.add_modifier(Modifier::ITALIC)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_modifier_modifier() {
|
||||
assert_eq!(
|
||||
Style::from((Modifier::BOLD | Modifier::ITALIC, Modifier::DIM)),
|
||||
Style::new()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.add_modifier(Modifier::ITALIC)
|
||||
.remove_modifier(Modifier::DIM)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_color_modifier() {
|
||||
assert_eq!(
|
||||
Style::from((Color::Red, Modifier::BOLD | Modifier::ITALIC)),
|
||||
Style::new()
|
||||
.fg(Color::Red)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.add_modifier(Modifier::ITALIC)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_color_color_modifier() {
|
||||
assert_eq!(
|
||||
Style::from((Color::Red, Color::Blue, Modifier::BOLD | Modifier::ITALIC)),
|
||||
Style::new()
|
||||
.fg(Color::Red)
|
||||
.bg(Color::Blue)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.add_modifier(Modifier::ITALIC)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_color_color_modifier_modifier() {
|
||||
assert_eq!(
|
||||
Style::from((
|
||||
Color::Red,
|
||||
Color::Blue,
|
||||
Modifier::BOLD | Modifier::ITALIC,
|
||||
Modifier::DIM
|
||||
)),
|
||||
Style::new()
|
||||
.fg(Color::Red)
|
||||
.bg(Color::Blue)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.add_modifier(Modifier::ITALIC)
|
||||
.remove_modifier(Modifier::DIM)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,10 +35,14 @@ use std::{
|
||||
/// - we support `-` and `_` and ` ` as separators for all colors
|
||||
/// - we support both `gray` and `grey` spellings
|
||||
///
|
||||
/// `From<Color> for Style` is implemented by creating a style with the foreground color set to the
|
||||
/// given color. This allows you to use colors anywhere that accepts `Into<Style>`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use std::str::FromStr;
|
||||
///
|
||||
/// use ratatui::prelude::*;
|
||||
///
|
||||
/// assert_eq!(Color::from_str("red"), Ok(Color::Red));
|
||||
@@ -60,7 +64,7 @@ use std::{
|
||||
///
|
||||
/// [ANSI color table]: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
|
||||
pub enum Color {
|
||||
/// Resets the foreground or background color
|
||||
#[default]
|
||||
@@ -123,6 +127,29 @@ pub enum Color {
|
||||
Indexed(u8),
|
||||
}
|
||||
|
||||
impl Color {
|
||||
/// Convert a u32 to a Color
|
||||
///
|
||||
/// The u32 should be in the format 0x00RRGGBB.
|
||||
pub const fn from_u32(u: u32) -> Color {
|
||||
let r = (u >> 16) as u8;
|
||||
let g = (u >> 8) as u8;
|
||||
let b = u as u8;
|
||||
Color::Rgb(r, g, b)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl<'de> serde::Deserialize<'de> for Color {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
FromStr::from_str(&s).map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
/// Error type indicating a failure to parse a color string.
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub struct ParseColorError;
|
||||
@@ -147,6 +174,7 @@ impl std::error::Error for ParseColorError {}
|
||||
///
|
||||
/// ```
|
||||
/// use std::str::FromStr;
|
||||
///
|
||||
/// use ratatui::prelude::*;
|
||||
///
|
||||
/// let color: Color = Color::from_str("blue").unwrap();
|
||||
@@ -249,8 +277,20 @@ impl Display for Color {
|
||||
mod tests {
|
||||
use std::error::Error;
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::de::{Deserialize, IntoDeserializer};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn from_u32() {
|
||||
assert_eq!(Color::from_u32(0x000000), Color::Rgb(0, 0, 0));
|
||||
assert_eq!(Color::from_u32(0xFF0000), Color::Rgb(255, 0, 0));
|
||||
assert_eq!(Color::from_u32(0x00FF00), Color::Rgb(0, 255, 0));
|
||||
assert_eq!(Color::from_u32(0x0000FF), Color::Rgb(0, 0, 255));
|
||||
assert_eq!(Color::from_u32(0xFFFFFF), Color::Rgb(255, 255, 255));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_rgb_color() {
|
||||
let color: Color = Color::from_str("#FF0000").unwrap();
|
||||
@@ -359,4 +399,46 @@ mod tests {
|
||||
assert_eq!(format!("{}", Color::Rgb(255, 0, 0)), "#FF0000");
|
||||
assert_eq!(format!("{}", Color::Reset), "Reset");
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
#[test]
|
||||
fn deserialize() -> Result<(), serde::de::value::Error> {
|
||||
assert_eq!(
|
||||
Color::Black,
|
||||
Color::deserialize("Black".into_deserializer())?
|
||||
);
|
||||
assert_eq!(
|
||||
Color::Magenta,
|
||||
Color::deserialize("magenta".into_deserializer())?
|
||||
);
|
||||
assert_eq!(
|
||||
Color::LightGreen,
|
||||
Color::deserialize("LightGreen".into_deserializer())?
|
||||
);
|
||||
assert_eq!(
|
||||
Color::White,
|
||||
Color::deserialize("bright-white".into_deserializer())?
|
||||
);
|
||||
assert_eq!(
|
||||
Color::Indexed(42),
|
||||
Color::deserialize("42".into_deserializer())?
|
||||
);
|
||||
assert_eq!(
|
||||
Color::Rgb(0, 255, 0),
|
||||
Color::deserialize("#00ff00".into_deserializer())?
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
#[test]
|
||||
fn deserialize_error() {
|
||||
let color: Result<_, serde::de::value::Error> =
|
||||
Color::deserialize("invalid".into_deserializer());
|
||||
assert!(color.is_err());
|
||||
|
||||
let color: Result<_, serde::de::value::Error> =
|
||||
Color::deserialize("#00000000".into_deserializer());
|
||||
assert!(color.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
4
src/style/palette.rs
Normal file
4
src/style/palette.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
//! A module for defining color palettes.
|
||||
|
||||
pub mod material;
|
||||
pub mod tailwind;
|
||||
606
src/style/palette/material.rs
Normal file
606
src/style/palette/material.rs
Normal file
@@ -0,0 +1,606 @@
|
||||
//! Material design color palettes.
|
||||
//!
|
||||
//! Represents the colors from the 2014 [Material design color palettes][palettes] by Google.
|
||||
//!
|
||||
//! [palettes]: https://m2.material.io/design/color/the-color-system.html#tools-for-picking-colors
|
||||
//!
|
||||
//! There are 16 palettes with accent colors, and 3 palettes without accent colors. Each palette
|
||||
//! has 10 colors, with variants from 50 to 900. The accent palettes also have 4 accent colors
|
||||
//! with variants from 100 to 700. Black and White are also included for completeness and to avoid
|
||||
//! being affected by any terminal theme that might be in use.
|
||||
//!
|
||||
//! This module exists to provide a convenient way to use the colors from the
|
||||
//! [`matdesign-color` crate] in your application.
|
||||
//!
|
||||
//! <style>
|
||||
//! .color { display: flex; align-items: center; }
|
||||
//! .color > div { width: 2rem; height: 2rem; }
|
||||
//! .color > div.name { width: 150px; !important; }
|
||||
//! </style>
|
||||
//! <div style="overflow-x: auto">
|
||||
//! <div style="display: flex; flex-direction:column; text-align: left">
|
||||
//! <div class="color" style="font-size:0.8em">
|
||||
//! <div class="name"></div>
|
||||
//! <div>C50</div>
|
||||
//! <div>C100</div>
|
||||
//! <div>C200</div>
|
||||
//! <div>C300</div>
|
||||
//! <div>C400</div>
|
||||
//! <div>C500</div>
|
||||
//! <div>C600</div>
|
||||
//! <div>C700</div>
|
||||
//! <div>C800</div>
|
||||
//! <div>C900</div>
|
||||
//! <div>A100</div>
|
||||
//! <div>A200</div>
|
||||
//! <div>A400</div>
|
||||
//! <div>A700</div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`RED`]</div>
|
||||
//! <div style="background-color: #FFEBEE"></div>
|
||||
//! <div style="background-color: #FFCDD2"></div>
|
||||
//! <div style="background-color: #EF9A9A"></div>
|
||||
//! <div style="background-color: #E57373"></div>
|
||||
//! <div style="background-color: #EF5350"></div>
|
||||
//! <div style="background-color: #F44336"></div>
|
||||
//! <div style="background-color: #E53935"></div>
|
||||
//! <div style="background-color: #D32F2F"></div>
|
||||
//! <div style="background-color: #C62828"></div>
|
||||
//! <div style="background-color: #B71C1C"></div>
|
||||
//! <div style="background-color: #FF8A80"></div>
|
||||
//! <div style="background-color: #FF5252"></div>
|
||||
//! <div style="background-color: #FF1744"></div>
|
||||
//! <div style="background-color: #D50000"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`PINK`]</div>
|
||||
//! <div style="background-color: #FCE4EC"></div>
|
||||
//! <div style="background-color: #F8BBD0"></div>
|
||||
//! <div style="background-color: #F48FB1"></div>
|
||||
//! <div style="background-color: #F06292"></div>
|
||||
//! <div style="background-color: #EC407A"></div>
|
||||
//! <div style="background-color: #E91E63"></div>
|
||||
//! <div style="background-color: #D81B60"></div>
|
||||
//! <div style="background-color: #C2185B"></div>
|
||||
//! <div style="background-color: #AD1457"></div>
|
||||
//! <div style="background-color: #880E4F"></div>
|
||||
//! <div style="background-color: #FF80AB"></div>
|
||||
//! <div style="background-color: #FF4081"></div>
|
||||
//! <div style="background-color: #F50057"></div>
|
||||
//! <div style="background-color: #C51162"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`PURPLE`]</div>
|
||||
//! <div style="background-color: #F3E5F5"></div>
|
||||
//! <div style="background-color: #E1BEE7"></div>
|
||||
//! <div style="background-color: #CE93D8"></div>
|
||||
//! <div style="background-color: #BA68C8"></div>
|
||||
//! <div style="background-color: #AB47BC"></div>
|
||||
//! <div style="background-color: #9C27B0"></div>
|
||||
//! <div style="background-color: #8E24AA"></div>
|
||||
//! <div style="background-color: #7B1FA2"></div>
|
||||
//! <div style="background-color: #6A1B9A"></div>
|
||||
//! <div style="background-color: #4A148C"></div>
|
||||
//! <div style="background-color: #EA80FC"></div>
|
||||
//! <div style="background-color: #E040FB"></div>
|
||||
//! <div style="background-color: #D500F9"></div>
|
||||
//! <div style="background-color: #AA00FF"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`DEEP_PURPLE`]</div>
|
||||
//! <div style="background-color: #EDE7F6"></div>
|
||||
//! <div style="background-color: #D1C4E9"></div>
|
||||
//! <div style="background-color: #B39DDB"></div>
|
||||
//! <div style="background-color: #9575CD"></div>
|
||||
//! <div style="background-color: #7E57C2"></div>
|
||||
//! <div style="background-color: #673AB7"></div>
|
||||
//! <div style="background-color: #5E35B1"></div>
|
||||
//! <div style="background-color: #512DA8"></div>
|
||||
//! <div style="background-color: #4527A0"></div>
|
||||
//! <div style="background-color: #311B92"></div>
|
||||
//! <div style="background-color: #B388FF"></div>
|
||||
//! <div style="background-color: #7C4DFF"></div>
|
||||
//! <div style="background-color: #651FFF"></div>
|
||||
//! <div style="background-color: #6200EA"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`INDIGO`]</div>
|
||||
//! <div style="background-color: #E8EAF6"></div>
|
||||
//! <div style="background-color: #C5CAE9"></div>
|
||||
//! <div style="background-color: #9FA8DA"></div>
|
||||
//! <div style="background-color: #7986CB"></div>
|
||||
//! <div style="background-color: #5C6BC0"></div>
|
||||
//! <div style="background-color: #3F51B5"></div>
|
||||
//! <div style="background-color: #3949AB"></div>
|
||||
//! <div style="background-color: #303F9F"></div>
|
||||
//! <div style="background-color: #283593"></div>
|
||||
//! <div style="background-color: #1A237E"></div>
|
||||
//! <div style="background-color: #8C9EFF"></div>
|
||||
//! <div style="background-color: #536DFE"></div>
|
||||
//! <div style="background-color: #3D5AFE"></div>
|
||||
//! <div style="background-color: #304FFE"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`BLUE`]</div>
|
||||
//! <div style="background-color: #E3F2FD"></div>
|
||||
//! <div style="background-color: #BBDEFB"></div>
|
||||
//! <div style="background-color: #90CAF9"></div>
|
||||
//! <div style="background-color: #64B5F6"></div>
|
||||
//! <div style="background-color: #42A5F5"></div>
|
||||
//! <div style="background-color: #2196F3"></div>
|
||||
//! <div style="background-color: #1E88E5"></div>
|
||||
//! <div style="background-color: #1976D2"></div>
|
||||
//! <div style="background-color: #1565C0"></div>
|
||||
//! <div style="background-color: #0D47A1"></div>
|
||||
//! <div style="background-color: #82B1FF"></div>
|
||||
//! <div style="background-color: #448AFF"></div>
|
||||
//! <div style="background-color: #2979FF"></div>
|
||||
//! <div style="background-color: #2962FF"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`LIGHT_BLUE`]</div>
|
||||
//! <div style="background-color: #E1F5FE"></div>
|
||||
//! <div style="background-color: #B3E5FC"></div>
|
||||
//! <div style="background-color: #81D4FA"></div>
|
||||
//! <div style="background-color: #4FC3F7"></div>
|
||||
//! <div style="background-color: #29B6F6"></div>
|
||||
//! <div style="background-color: #03A9F4"></div>
|
||||
//! <div style="background-color: #039BE5"></div>
|
||||
//! <div style="background-color: #0288D1"></div>
|
||||
//! <div style="background-color: #0277BD"></div>
|
||||
//! <div style="background-color: #01579B"></div>
|
||||
//! <div style="background-color: #80D8FF"></div>
|
||||
//! <div style="background-color: #40C4FF"></div>
|
||||
//! <div style="background-color: #00B0FF"></div>
|
||||
//! <div style="background-color: #0091EA"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`CYAN`]</div>
|
||||
//! <div style="background-color: #E0F7FA"></div>
|
||||
//! <div style="background-color: #B2EBF2"></div>
|
||||
//! <div style="background-color: #80DEEA"></div>
|
||||
//! <div style="background-color: #4DD0E1"></div>
|
||||
//! <div style="background-color: #26C6DA"></div>
|
||||
//! <div style="background-color: #00BCD4"></div>
|
||||
//! <div style="background-color: #00ACC1"></div>
|
||||
//! <div style="background-color: #0097A7"></div>
|
||||
//! <div style="background-color: #00838F"></div>
|
||||
//! <div style="background-color: #006064"></div>
|
||||
//! <div style="background-color: #84FFFF"></div>
|
||||
//! <div style="background-color: #18FFFF"></div>
|
||||
//! <div style="background-color: #00E5FF"></div>
|
||||
//! <div style="background-color: #00B8D4"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`TEAL`]</div>
|
||||
//! <div style="background-color: #E0F2F1"></div>
|
||||
//! <div style="background-color: #B2DFDB"></div>
|
||||
//! <div style="background-color: #80CBC4"></div>
|
||||
//! <div style="background-color: #4DB6AC"></div>
|
||||
//! <div style="background-color: #26A69A"></div>
|
||||
//! <div style="background-color: #009688"></div>
|
||||
//! <div style="background-color: #00897B"></div>
|
||||
//! <div style="background-color: #00796B"></div>
|
||||
//! <div style="background-color: #00695C"></div>
|
||||
//! <div style="background-color: #004D40"></div>
|
||||
//! <div style="background-color: #A7FFEB"></div>
|
||||
//! <div style="background-color: #64FFDA"></div>
|
||||
//! <div style="background-color: #1DE9B6"></div>
|
||||
//! <div style="background-color: #00BFA5"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`GREEN`]</div>
|
||||
//! <div style="background-color: #E8F5E9"></div>
|
||||
//! <div style="background-color: #C8E6C9"></div>
|
||||
//! <div style="background-color: #A5D6A7"></div>
|
||||
//! <div style="background-color: #81C784"></div>
|
||||
//! <div style="background-color: #66BB6A"></div>
|
||||
//! <div style="background-color: #4CAF50"></div>
|
||||
//! <div style="background-color: #43A047"></div>
|
||||
//! <div style="background-color: #388E3C"></div>
|
||||
//! <div style="background-color: #2E7D32"></div>
|
||||
//! <div style="background-color: #1B5E20"></div>
|
||||
//! <div style="background-color: #B9F6CA"></div>
|
||||
//! <div style="background-color: #69F0AE"></div>
|
||||
//! <div style="background-color: #00E676"></div>
|
||||
//! <div style="background-color: #00C853"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`LIGHT_GREEN`]</div>
|
||||
//! <div style="background-color: #F1F8E9"></div>
|
||||
//! <div style="background-color: #DCEDC8"></div>
|
||||
//! <div style="background-color: #C5E1A5"></div>
|
||||
//! <div style="background-color: #AED581"></div>
|
||||
//! <div style="background-color: #9CCC65"></div>
|
||||
//! <div style="background-color: #8BC34A"></div>
|
||||
//! <div style="background-color: #7CB342"></div>
|
||||
//! <div style="background-color: #689F38"></div>
|
||||
//! <div style="background-color: #558B2F"></div>
|
||||
//! <div style="background-color: #33691E"></div>
|
||||
//! <div style="background-color: #CCFF90"></div>
|
||||
//! <div style="background-color: #B2FF59"></div>
|
||||
//! <div style="background-color: #76FF03"></div>
|
||||
//! <div style="background-color: #64DD17"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`LIME`]</div>
|
||||
//! <div style="background-color: #F9FBE7"></div>
|
||||
//! <div style="background-color: #F0F4C3"></div>
|
||||
//! <div style="background-color: #E6EE9C"></div>
|
||||
//! <div style="background-color: #DCE775"></div>
|
||||
//! <div style="background-color: #D4E157"></div>
|
||||
//! <div style="background-color: #CDDC39"></div>
|
||||
//! <div style="background-color: #C0CA33"></div>
|
||||
//! <div style="background-color: #AFB42B"></div>
|
||||
//! <div style="background-color: #9E9D24"></div>
|
||||
//! <div style="background-color: #827717"></div>
|
||||
//! <div style="background-color: #F4FF81"></div>
|
||||
//! <div style="background-color: #EEFF41"></div>
|
||||
//! <div style="background-color: #C6FF00"></div>
|
||||
//! <div style="background-color: #AEEA00"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`YELLOW`]</div>
|
||||
//! <div style="background-color: #FFFDE7"></div>
|
||||
//! <div style="background-color: #FFF9C4"></div>
|
||||
//! <div style="background-color: #FFF59D"></div>
|
||||
//! <div style="background-color: #FFF176"></div>
|
||||
//! <div style="background-color: #FFEE58"></div>
|
||||
//! <div style="background-color: #FFEB3B"></div>
|
||||
//! <div style="background-color: #FDD835"></div>
|
||||
//! <div style="background-color: #FBC02D"></div>
|
||||
//! <div style="background-color: #F9A825"></div>
|
||||
//! <div style="background-color: #F57F17"></div>
|
||||
//! <div style="background-color: #FFFF8D"></div>
|
||||
//! <div style="background-color: #FFFF00"></div>
|
||||
//! <div style="background-color: #FFEA00"></div>
|
||||
//! <div style="background-color: #FFD600"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`AMBER`]</div>
|
||||
//! <div style="background-color: #FFF8E1"></div>
|
||||
//! <div style="background-color: #FFECB3"></div>
|
||||
//! <div style="background-color: #FFE082"></div>
|
||||
//! <div style="background-color: #FFD54F"></div>
|
||||
//! <div style="background-color: #FFCA28"></div>
|
||||
//! <div style="background-color: #FFC107"></div>
|
||||
//! <div style="background-color: #FFB300"></div>
|
||||
//! <div style="background-color: #FFA000"></div>
|
||||
//! <div style="background-color: #FF8F00"></div>
|
||||
//! <div style="background-color: #FF6F00"></div>
|
||||
//! <div style="background-color: #FFE57F"></div>
|
||||
//! <div style="background-color: #FFD740"></div>
|
||||
//! <div style="background-color: #FFC400"></div>
|
||||
//! <div style="background-color: #FFAB00"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`ORANGE`]</div>
|
||||
//! <div style="background-color: #FFF3E0"></div>
|
||||
//! <div style="background-color: #FFE0B2"></div>
|
||||
//! <div style="background-color: #FFCC80"></div>
|
||||
//! <div style="background-color: #FFB74D"></div>
|
||||
//! <div style="background-color: #FFA726"></div>
|
||||
//! <div style="background-color: #FF9800"></div>
|
||||
//! <div style="background-color: #FB8C00"></div>
|
||||
//! <div style="background-color: #F57C00"></div>
|
||||
//! <div style="background-color: #EF6C00"></div>
|
||||
//! <div style="background-color: #E65100"></div>
|
||||
//! <div style="background-color: #FFD180"></div>
|
||||
//! <div style="background-color: #FFAB40"></div>
|
||||
//! <div style="background-color: #FF9100"></div>
|
||||
//! <div style="background-color: #FF6D00"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`DEEP_ORANGE`]</div>
|
||||
//! <div style="background-color: #FBE9E7"></div>
|
||||
//! <div style="background-color: #FFCCBC"></div>
|
||||
//! <div style="background-color: #FFAB91"></div>
|
||||
//! <div style="background-color: #FF8A65"></div>
|
||||
//! <div style="background-color: #FF7043"></div>
|
||||
//! <div style="background-color: #FF5722"></div>
|
||||
//! <div style="background-color: #F4511E"></div>
|
||||
//! <div style="background-color: #E64A19"></div>
|
||||
//! <div style="background-color: #D84315"></div>
|
||||
//! <div style="background-color: #BF360C"></div>
|
||||
//! <div style="background-color: #FF9E80"></div>
|
||||
//! <div style="background-color: #FF6E40"></div>
|
||||
//! <div style="background-color: #FF3D00"></div>
|
||||
//! <div style="background-color: #DD2C00"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`BROWN`]</div>
|
||||
//! <div style="background-color: #EFEBE9"></div>
|
||||
//! <div style="background-color: #D7CCC8"></div>
|
||||
//! <div style="background-color: #BCAAA4"></div>
|
||||
//! <div style="background-color: #A1887F"></div>
|
||||
//! <div style="background-color: #8D6E63"></div>
|
||||
//! <div style="background-color: #795548"></div>
|
||||
//! <div style="background-color: #6D4C41"></div>
|
||||
//! <div style="background-color: #5D4037"></div>
|
||||
//! <div style="background-color: #4E342E"></div>
|
||||
//! <div style="background-color: #3E2723"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`GRAY`]</div>
|
||||
//! <div style="background-color: #FAFAFA"></div>
|
||||
//! <div style="background-color: #F5F5F5"></div>
|
||||
//! <div style="background-color: #EEEEEE"></div>
|
||||
//! <div style="background-color: #E0E0E0"></div>
|
||||
//! <div style="background-color: #BDBDBD"></div>
|
||||
//! <div style="background-color: #9E9E9E"></div>
|
||||
//! <div style="background-color: #757575"></div>
|
||||
//! <div style="background-color: #616161"></div>
|
||||
//! <div style="background-color: #424242"></div>
|
||||
//! <div style="background-color: #212121"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`BLUE_GRAY`]</div>
|
||||
//! <div style="background-color: #ECEFF1"></div>
|
||||
//! <div style="background-color: #CFD8DC"></div>
|
||||
//! <div style="background-color: #B0BEC5"></div>
|
||||
//! <div style="background-color: #90A4AE"></div>
|
||||
//! <div style="background-color: #78909C"></div>
|
||||
//! <div style="background-color: #607D8B"></div>
|
||||
//! <div style="background-color: #546E7A"></div>
|
||||
//! <div style="background-color: #455A64"></div>
|
||||
//! <div style="background-color: #37474F"></div>
|
||||
//! <div style="background-color: #263238"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`BLACK`]</div>
|
||||
//! <div class="bw" style="width: 350px; background-color: #000000"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`WHITE`]</div>
|
||||
//! <div style="width: 350px; background-color: #FFFFFF"></div>
|
||||
//! </div>
|
||||
//! </div>
|
||||
//! </div>
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use ratatui::prelude::*;
|
||||
//! use ratatui::style::palette::material::{BLUE, RED};
|
||||
//!
|
||||
//! assert_eq!(RED.c500, Color::Rgb(244, 67, 54));
|
||||
//! assert_eq!(BLUE.c500, Color::Rgb(33, 150, 243));
|
||||
//! ```
|
||||
//!
|
||||
//! [`matdesign-color` crate]: https://crates.io/crates/matdesign-color
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
/// A palette of colors for use in Material design with accent colors
|
||||
///
|
||||
/// This is a collection of colors that are used in Material design. They consist of a set of
|
||||
/// colors from 50 to 900, and a set of accent colors from 100 to 700.
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub struct AccentedPalette {
|
||||
pub c50: Color,
|
||||
pub c100: Color,
|
||||
pub c200: Color,
|
||||
pub c300: Color,
|
||||
pub c400: Color,
|
||||
pub c500: Color,
|
||||
pub c600: Color,
|
||||
pub c700: Color,
|
||||
pub c800: Color,
|
||||
pub c900: Color,
|
||||
pub a100: Color,
|
||||
pub a200: Color,
|
||||
pub a400: Color,
|
||||
pub a700: Color,
|
||||
}
|
||||
|
||||
/// A palette of colors for use in Material design without accent colors
|
||||
///
|
||||
/// This is a collection of colors that are used in Material design. They consist of a set of
|
||||
/// colors from 50 to 900.
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub struct NonAccentedPalette {
|
||||
pub c50: Color,
|
||||
pub c100: Color,
|
||||
pub c200: Color,
|
||||
pub c300: Color,
|
||||
pub c400: Color,
|
||||
pub c500: Color,
|
||||
pub c600: Color,
|
||||
pub c700: Color,
|
||||
pub c800: Color,
|
||||
pub c900: Color,
|
||||
}
|
||||
|
||||
impl AccentedPalette {
|
||||
/// Create a new AccentedPalette from the given variants
|
||||
///
|
||||
/// The variants should be in the format [0x00RRGGBB, ...]
|
||||
pub const fn from_variants(variants: [u32; 14]) -> AccentedPalette {
|
||||
AccentedPalette {
|
||||
c50: Color::from_u32(variants[0]),
|
||||
c100: Color::from_u32(variants[1]),
|
||||
c200: Color::from_u32(variants[2]),
|
||||
c300: Color::from_u32(variants[3]),
|
||||
c400: Color::from_u32(variants[4]),
|
||||
c500: Color::from_u32(variants[5]),
|
||||
c600: Color::from_u32(variants[6]),
|
||||
c700: Color::from_u32(variants[7]),
|
||||
c800: Color::from_u32(variants[8]),
|
||||
c900: Color::from_u32(variants[9]),
|
||||
a100: Color::from_u32(variants[10]),
|
||||
a200: Color::from_u32(variants[11]),
|
||||
a400: Color::from_u32(variants[12]),
|
||||
a700: Color::from_u32(variants[13]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NonAccentedPalette {
|
||||
/// Create a new NonAccented from the given variants
|
||||
///
|
||||
/// The variants should be in the format [0x00RRGGBB, ...]
|
||||
pub const fn from_variants(variants: [u32; 10]) -> NonAccentedPalette {
|
||||
NonAccentedPalette {
|
||||
c50: Color::from_u32(variants[0]),
|
||||
c100: Color::from_u32(variants[1]),
|
||||
c200: Color::from_u32(variants[2]),
|
||||
c300: Color::from_u32(variants[3]),
|
||||
c400: Color::from_u32(variants[4]),
|
||||
c500: Color::from_u32(variants[5]),
|
||||
c600: Color::from_u32(variants[6]),
|
||||
c700: Color::from_u32(variants[7]),
|
||||
c800: Color::from_u32(variants[8]),
|
||||
c900: Color::from_u32(variants[9]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Accented palettes
|
||||
|
||||
pub const RED: AccentedPalette = AccentedPalette::from_variants(variants::RED);
|
||||
pub const PINK: AccentedPalette = AccentedPalette::from_variants(variants::PINK);
|
||||
pub const PURPLE: AccentedPalette = AccentedPalette::from_variants(variants::PURPLE);
|
||||
pub const DEEP_PURPLE: AccentedPalette = AccentedPalette::from_variants(variants::DEEP_PURPLE);
|
||||
pub const INDIGO: AccentedPalette = AccentedPalette::from_variants(variants::INDIGO);
|
||||
pub const BLUE: AccentedPalette = AccentedPalette::from_variants(variants::BLUE);
|
||||
pub const LIGHT_BLUE: AccentedPalette = AccentedPalette::from_variants(variants::LIGHT_BLUE);
|
||||
pub const CYAN: AccentedPalette = AccentedPalette::from_variants(variants::CYAN);
|
||||
pub const TEAL: AccentedPalette = AccentedPalette::from_variants(variants::TEAL);
|
||||
pub const GREEN: AccentedPalette = AccentedPalette::from_variants(variants::GREEN);
|
||||
pub const LIGHT_GREEN: AccentedPalette = AccentedPalette::from_variants(variants::LIGHT_GREEN);
|
||||
pub const LIME: AccentedPalette = AccentedPalette::from_variants(variants::LIME);
|
||||
pub const YELLOW: AccentedPalette = AccentedPalette::from_variants(variants::YELLOW);
|
||||
pub const AMBER: AccentedPalette = AccentedPalette::from_variants(variants::AMBER);
|
||||
pub const ORANGE: AccentedPalette = AccentedPalette::from_variants(variants::ORANGE);
|
||||
pub const DEEP_ORANGE: AccentedPalette = AccentedPalette::from_variants(variants::DEEP_ORANGE);
|
||||
|
||||
// Unaccented palettes
|
||||
pub const BROWN: NonAccentedPalette = NonAccentedPalette::from_variants(variants::BROWN);
|
||||
pub const GRAY: NonAccentedPalette = NonAccentedPalette::from_variants(variants::GRAY);
|
||||
pub const BLUE_GRAY: NonAccentedPalette = NonAccentedPalette::from_variants(variants::BLUE_GRAY);
|
||||
|
||||
// Black and white included for completeness
|
||||
pub const BLACK: Color = Color::from_u32(0x000000);
|
||||
pub const WHITE: Color = Color::from_u32(0xFFFFFF);
|
||||
|
||||
mod variants {
|
||||
pub const RED: [u32; 14] = [
|
||||
0xFFEBEE, 0xFFCDD2, 0xEF9A9A, 0xE57373, 0xEF5350, 0xF44336, 0xE53935, 0xD32F2F, 0xC62828,
|
||||
0xB71C1C, 0xFF8A80, 0xFF5252, 0xFF1744, 0xD50000,
|
||||
];
|
||||
pub const PINK: [u32; 14] = [
|
||||
0xFCE4EC, 0xF8BBD0, 0xF48FB1, 0xF06292, 0xEC407A, 0xE91E63, 0xD81B60, 0xC2185B, 0xAD1457,
|
||||
0x880E4F, 0xFF80AB, 0xFF4081, 0xF50057, 0xC51162,
|
||||
];
|
||||
pub const PURPLE: [u32; 14] = [
|
||||
0xF3E5F5, 0xE1BEE7, 0xCE93D8, 0xBA68C8, 0xAB47BC, 0x9C27B0, 0x8E24AA, 0x7B1FA2, 0x6A1B9A,
|
||||
0x4A148C, 0xEA80FC, 0xE040FB, 0xD500F9, 0xAA00FF,
|
||||
];
|
||||
pub const DEEP_PURPLE: [u32; 14] = [
|
||||
0xEDE7F6, 0xD1C4E9, 0xB39DDB, 0x9575CD, 0x7E57C2, 0x673AB7, 0x5E35B1, 0x512DA8, 0x4527A0,
|
||||
0x311B92, 0xB388FF, 0x7C4DFF, 0x651FFF, 0x6200EA,
|
||||
];
|
||||
pub const INDIGO: [u32; 14] = [
|
||||
0xE8EAF6, 0xC5CAE9, 0x9FA8DA, 0x7986CB, 0x5C6BC0, 0x3F51B5, 0x3949AB, 0x303F9F, 0x283593,
|
||||
0x1A237E, 0x8C9EFF, 0x536DFE, 0x3D5AFE, 0x304FFE,
|
||||
];
|
||||
pub const BLUE: [u32; 14] = [
|
||||
0xE3F2FD, 0xBBDEFB, 0x90CAF9, 0x64B5F6, 0x42A5F5, 0x2196F3, 0x1E88E5, 0x1976D2, 0x1565C0,
|
||||
0x0D47A1, 0x82B1FF, 0x448AFF, 0x2979FF, 0x2962FF,
|
||||
];
|
||||
pub const LIGHT_BLUE: [u32; 14] = [
|
||||
0xE1F5FE, 0xB3E5FC, 0x81D4FA, 0x4FC3F7, 0x29B6F6, 0x03A9F4, 0x039BE5, 0x0288D1, 0x0277BD,
|
||||
0x01579B, 0x80D8FF, 0x40C4FF, 0x00B0FF, 0x0091EA,
|
||||
];
|
||||
pub const CYAN: [u32; 14] = [
|
||||
0xE0F7FA, 0xB2EBF2, 0x80DEEA, 0x4DD0E1, 0x26C6DA, 0x00BCD4, 0x00ACC1, 0x0097A7, 0x00838F,
|
||||
0x006064, 0x84FFFF, 0x18FFFF, 0x00E5FF, 0x00B8D4,
|
||||
];
|
||||
pub const TEAL: [u32; 14] = [
|
||||
0xE0F2F1, 0xB2DFDB, 0x80CBC4, 0x4DB6AC, 0x26A69A, 0x009688, 0x00897B, 0x00796B, 0x00695C,
|
||||
0x004D40, 0xA7FFEB, 0x64FFDA, 0x1DE9B6, 0x00BFA5,
|
||||
];
|
||||
pub const GREEN: [u32; 14] = [
|
||||
0xE8F5E9, 0xC8E6C9, 0xA5D6A7, 0x81C784, 0x66BB6A, 0x4CAF50, 0x43A047, 0x388E3C, 0x2E7D32,
|
||||
0x1B5E20, 0xB9F6CA, 0x69F0AE, 0x00E676, 0x00C853,
|
||||
];
|
||||
pub const LIGHT_GREEN: [u32; 14] = [
|
||||
0xF1F8E9, 0xDCEDC8, 0xC5E1A5, 0xAED581, 0x9CCC65, 0x8BC34A, 0x7CB342, 0x689F38, 0x558B2F,
|
||||
0x33691E, 0xCCFF90, 0xB2FF59, 0x76FF03, 0x64DD17,
|
||||
];
|
||||
pub const LIME: [u32; 14] = [
|
||||
0xF9FBE7, 0xF0F4C3, 0xE6EE9C, 0xDCE775, 0xD4E157, 0xCDDC39, 0xC0CA33, 0xAFB42B, 0x9E9D24,
|
||||
0x827717, 0xF4FF81, 0xEEFF41, 0xC6FF00, 0xAEEA00,
|
||||
];
|
||||
pub const YELLOW: [u32; 14] = [
|
||||
0xFFFDE7, 0xFFF9C4, 0xFFF59D, 0xFFF176, 0xFFEE58, 0xFFEB3B, 0xFDD835, 0xFBC02D, 0xF9A825,
|
||||
0xF57F17, 0xFFFF8D, 0xFFFF00, 0xFFEA00, 0xFFD600,
|
||||
];
|
||||
pub const AMBER: [u32; 14] = [
|
||||
0xFFF8E1, 0xFFECB3, 0xFFE082, 0xFFD54F, 0xFFCA28, 0xFFC107, 0xFFB300, 0xFFA000, 0xFF8F00,
|
||||
0xFF6F00, 0xFFE57F, 0xFFD740, 0xFFC400, 0xFFAB00,
|
||||
];
|
||||
pub const ORANGE: [u32; 14] = [
|
||||
0xFFF3E0, 0xFFE0B2, 0xFFCC80, 0xFFB74D, 0xFFA726, 0xFF9800, 0xFB8C00, 0xF57C00, 0xEF6C00,
|
||||
0xE65100, 0xFFD180, 0xFFAB40, 0xFF9100, 0xFF6D00,
|
||||
];
|
||||
pub const DEEP_ORANGE: [u32; 14] = [
|
||||
0xFBE9E7, 0xFFCCBC, 0xFFAB91, 0xFF8A65, 0xFF7043, 0xFF5722, 0xF4511E, 0xE64A19, 0xD84315,
|
||||
0xBF360C, 0xFF9E80, 0xFF6E40, 0xFF3D00, 0xDD2C00,
|
||||
];
|
||||
pub const BROWN: [u32; 10] = [
|
||||
0xEFEBE9, 0xD7CCC8, 0xBCAAA4, 0xA1887F, 0x8D6E63, 0x795548, 0x6D4C41, 0x5D4037, 0x4E342E,
|
||||
0x3E2723,
|
||||
];
|
||||
pub const GRAY: [u32; 10] = [
|
||||
0xFAFAFA, 0xF5F5F5, 0xEEEEEE, 0xE0E0E0, 0xBDBDBD, 0x9E9E9E, 0x757575, 0x616161, 0x424242,
|
||||
0x212121,
|
||||
];
|
||||
pub const BLUE_GRAY: [u32; 10] = [
|
||||
0xECEFF1, 0xCFD8DC, 0xB0BEC5, 0x90A4AE, 0x78909C, 0x607D8B, 0x546E7A, 0x455A64, 0x37474F,
|
||||
0x263238,
|
||||
];
|
||||
}
|
||||
652
src/style/palette/tailwind.rs
Normal file
652
src/style/palette/tailwind.rs
Normal file
@@ -0,0 +1,652 @@
|
||||
//! Represents the Tailwind CSS [default color palette][palette].
|
||||
//!
|
||||
//! [palette]: https://tailwindcss.com/docs/customizing-colors#default-color-palette
|
||||
//!
|
||||
//! There are 22 palettes. Each palette has 11 colors, with variants from 50 to 950. Black and White
|
||||
//! are also included for completeness and to avoid being affected by any terminal theme that might
|
||||
//! be in use.
|
||||
//!
|
||||
//! <style>
|
||||
//! .color { display: flex; align-items: center; }
|
||||
//! .color > div { width: 2rem; height: 2rem; }
|
||||
//! .color > div.name { width: 150px; !important; }
|
||||
//! </style>
|
||||
//! <div style="overflow-x: auto">
|
||||
//! <div style="display: flex; flex-direction:column; text-align: left">
|
||||
//! <div class="color" style="font-size:0.8em">
|
||||
//! <div class="name"></div>
|
||||
//! <div>C50</div> <div>C100</div> <div>C200</div> <div>C300</div> <div>C400</div>
|
||||
//! <div>C500</div> <div>C600</div> <div>C700</div> <div>C800</div> <div>C900</div>
|
||||
//! <div>C950</div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`SLATE`]</div>
|
||||
//! <div style="background-color: #f8fafc"></div> <div style="background-color: #f1f5f9"></div>
|
||||
//! <div style="background-color: #e2e8f0"></div> <div style="background-color: #cbd5e1"></div>
|
||||
//! <div style="background-color: #94a3b8"></div> <div style="background-color: #64748b"></div>
|
||||
//! <div style="background-color: #475569"></div> <div style="background-color: #334155"></div>
|
||||
//! <div style="background-color: #1e293b"></div> <div style="background-color: #0f172a"></div>
|
||||
//! <div style="background-color: #020617"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`GRAY`]</div>
|
||||
//! <div style="background-color: #f9fafb"></div> <div style="background-color: #f3f4f6"></div>
|
||||
//! <div style="background-color: #e5e7eb"></div> <div style="background-color: #d1d5db"></div>
|
||||
//! <div style="background-color: #9ca3af"></div> <div style="background-color: #6b7280"></div>
|
||||
//! <div style="background-color: #4b5563"></div> <div style="background-color: #374151"></div>
|
||||
//! <div style="background-color: #1f2937"></div> <div style="background-color: #111827"></div>
|
||||
//! <div style="background-color: #0a0a0a"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`ZINC`]</div>
|
||||
//! <div style="background-color: #fafafa"></div> <div style="background-color: #f5f5f5"></div>
|
||||
//! <div style="background-color: #e5e5e5"></div> <div style="background-color: #d4d4d4"></div>
|
||||
//! <div style="background-color: #a1a1aa"></div> <div style="background-color: #71717a"></div>
|
||||
//! <div style="background-color: #52525b"></div> <div style="background-color: #404040"></div>
|
||||
//! <div style="background-color: #262626"></div> <div style="background-color: #171717"></div>
|
||||
//! <div style="background-color: #0a0a0a"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`NEUTRAL`]</div>
|
||||
//! <div style="background-color: #fafafa"></div> <div style="background-color: #f5f5f5"></div>
|
||||
//! <div style="background-color: #e5e5e5"></div> <div style="background-color: #d4d4d4"></div>
|
||||
//! <div style="background-color: #a3a3a3"></div> <div style="background-color: #737373"></div>
|
||||
//! <div style="background-color: #525252"></div> <div style="background-color: #404040"></div>
|
||||
//! <div style="background-color: #262626"></div> <div style="background-color: #171717"></div>
|
||||
//! <div style="background-color: #0a0a0a"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`STONE`]</div>
|
||||
//! <div style="background-color: #fafaf9"></div> <div style="background-color: #f5f5f4"></div>
|
||||
//! <div style="background-color: #e7e5e4"></div> <div style="background-color: #d6d3d1"></div>
|
||||
//! <div style="background-color: #a8a29e"></div> <div style="background-color: #78716c"></div>
|
||||
//! <div style="background-color: #57534e"></div> <div style="background-color: #44403c"></div>
|
||||
//! <div style="background-color: #292524"></div> <div style="background-color: #1c1917"></div>
|
||||
//! <div style="background-color: #0c0a09"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`RED`]</div>
|
||||
//! <div style="background-color: #fef2f2"></div> <div style="background-color: #fee2e2"></div>
|
||||
//! <div style="background-color: #fecaca"></div> <div style="background-color: #fca5a5"></div>
|
||||
//! <div style="background-color: #f87171"></div> <div style="background-color: #ef4444"></div>
|
||||
//! <div style="background-color: #dc2626"></div> <div style="background-color: #b91c1c"></div>
|
||||
//! <div style="background-color: #991b1b"></div> <div style="background-color: #7f1d1d"></div>
|
||||
//! <div style="background-color: #450a0a"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`ORANGE`]</div>
|
||||
//! <div style="background-color: #fff7ed"></div> <div style="background-color: #ffedd5"></div>
|
||||
//! <div style="background-color: #fed7aa"></div> <div style="background-color: #fdba74"></div>
|
||||
//! <div style="background-color: #fb923c"></div> <div style="background-color: #f97316"></div>
|
||||
//! <div style="background-color: #ea580c"></div> <div style="background-color: #c2410c"></div>
|
||||
//! <div style="background-color: #9a3412"></div> <div style="background-color: #7c2d12"></div>
|
||||
//! <div style="background-color: #431407"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`AMBER`]</div>
|
||||
//! <div style="background-color: #fffbeb"></div> <div style="background-color: #fef3c7"></div>
|
||||
//! <div style="background-color: #fde68a"></div> <div style="background-color: #fcd34d"></div>
|
||||
//! <div style="background-color: #fbbf24"></div> <div style="background-color: #f59e0b"></div>
|
||||
//! <div style="background-color: #d97706"></div> <div style="background-color: #b45309"></div>
|
||||
//! <div style="background-color: #92400e"></div> <div style="background-color: #78350f"></div>
|
||||
//! <div style="background-color: #451a03"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`YELLOW`]</div>
|
||||
//! <div style="background-color: #fefce8"></div> <div style="background-color: #fef9c3"></div>
|
||||
//! <div style="background-color: #fef08a"></div> <div style="background-color: #fde047"></div>
|
||||
//! <div style="background-color: #facc15"></div> <div style="background-color: #eab308"></div>
|
||||
//! <div style="background-color: #ca8a04"></div> <div style="background-color: #a16207"></div>
|
||||
//! <div style="background-color: #854d0e"></div> <div style="background-color: #713f12"></div>
|
||||
//! <div style="background-color: #422006"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`LIME`]</div>
|
||||
//! <div style="background-color: #f7fee7"></div> <div style="background-color: #ecfccb"></div>
|
||||
//! <div style="background-color: #d9f99d"></div> <div style="background-color: #bef264"></div>
|
||||
//! <div style="background-color: #a3e635"></div> <div style="background-color: #84cc16"></div>
|
||||
//! <div style="background-color: #65a30d"></div> <div style="background-color: #4d7c0f"></div>
|
||||
//! <div style="background-color: #3f6212"></div> <div style="background-color: #365314"></div>
|
||||
//! <div style="background-color: #1a2e05"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`GREEN`]</div>
|
||||
//! <div style="background-color: #f0fdf4"></div> <div style="background-color: #dcfce7"></div>
|
||||
//! <div style="background-color: #bbf7d0"></div> <div style="background-color: #86efac"></div>
|
||||
//! <div style="background-color: #4ade80"></div> <div style="background-color: #22c55e"></div>
|
||||
//! <div style="background-color: #16a34a"></div> <div style="background-color: #15803d"></div>
|
||||
//! <div style="background-color: #166534"></div> <div style="background-color: #14532d"></div>
|
||||
//! <div style="background-color: #052e16"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`EMERALD`]</div>
|
||||
//! <div style="background-color: #ecfdf5"></div> <div style="background-color: #d1fae5"></div>
|
||||
//! <div style="background-color: #a7f3d0"></div> <div style="background-color: #6ee7b7"></div>
|
||||
//! <div style="background-color: #34d399"></div> <div style="background-color: #10b981"></div>
|
||||
//! <div style="background-color: #059669"></div> <div style="background-color: #047857"></div>
|
||||
//! <div style="background-color: #065f46"></div> <div style="background-color: #064e3b"></div>
|
||||
//! <div style="background-color: #022c22"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`TEAL`]</div>
|
||||
//! <div style="background-color: #f0fdfa"></div> <div style="background-color: #ccfbf1"></div>
|
||||
//! <div style="background-color: #99f6e4"></div> <div style="background-color: #5eead4"></div>
|
||||
//! <div style="background-color: #2dd4bf"></div> <div style="background-color: #14b8a6"></div>
|
||||
//! <div style="background-color: #0d9488"></div> <div style="background-color: #0f766e"></div>
|
||||
//! <div style="background-color: #115e59"></div> <div style="background-color: #134e4a"></div>
|
||||
//! <div style="background-color: #042f2e"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`CYAN`]</div>
|
||||
//! <div style="background-color: #ecfeff"></div> <div style="background-color: #cffafe"></div>
|
||||
//! <div style="background-color: #a5f3fc"></div> <div style="background-color: #67e8f9"></div>
|
||||
//! <div style="background-color: #22d3ee"></div> <div style="background-color: #06b6d4"></div>
|
||||
//! <div style="background-color: #0891b2"></div> <div style="background-color: #0e7490"></div>
|
||||
//! <div style="background-color: #155e75"></div> <div style="background-color: #164e63"></div>
|
||||
//! <div style="background-color: #083344"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`SKY`]</div>
|
||||
//! <div style="background-color: #f0f9ff"></div> <div style="background-color: #e0f2fe"></div>
|
||||
//! <div style="background-color: #bae6fd"></div> <div style="background-color: #7dd3fc"></div>
|
||||
//! <div style="background-color: #38bdf8"></div> <div style="background-color: #0ea5e9"></div>
|
||||
//! <div style="background-color: #0284c7"></div> <div style="background-color: #0369a1"></div>
|
||||
//! <div style="background-color: #075985"></div> <div style="background-color: #0c4a6e"></div>
|
||||
//! <div style="background-color: #082f49"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`BLUE`]</div>
|
||||
//! <div style="background-color: #eff6ff"></div> <div style="background-color: #dbeafe"></div>
|
||||
//! <div style="background-color: #bfdbfe"></div> <div style="background-color: #93c5fd"></div>
|
||||
//! <div style="background-color: #60a5fa"></div> <div style="background-color: #3b82f6"></div>
|
||||
//! <div style="background-color: #2563eb"></div> <div style="background-color: #1d4ed8"></div>
|
||||
//! <div style="background-color: #1e40af"></div> <div style="background-color: #1e3a8a"></div>
|
||||
//! <div style="background-color: #172554"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`INDIGO`]</div>
|
||||
//! <div style="background-color: #eef2ff"></div> <div style="background-color: #e0e7ff"></div>
|
||||
//! <div style="background-color: #c7d2fe"></div> <div style="background-color: #a5b4fc"></div>
|
||||
//! <div style="background-color: #818cf8"></div> <div style="background-color: #6366f1"></div>
|
||||
//! <div style="background-color: #4f46e5"></div> <div style="background-color: #4338ca"></div>
|
||||
//! <div style="background-color: #3730a3"></div> <div style="background-color: #312e81"></div>
|
||||
//! <div style="background-color: #1e1b4b"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`VIOLET`]</div>
|
||||
//! <div style="background-color: #f5f3ff"></div> <div style="background-color: #ede9fe"></div>
|
||||
//! <div style="background-color: #ddd6fe"></div> <div style="background-color: #c4b5fd"></div>
|
||||
//! <div style="background-color: #a78bfa"></div> <div style="background-color: #8b5cf6"></div>
|
||||
//! <div style="background-color: #7c3aed"></div> <div style="background-color: #6d28d9"></div>
|
||||
//! <div style="background-color: #5b21b6"></div> <div style="background-color: #4c1d95"></div>
|
||||
//! <div style="background-color: #2e1065"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`PURPLE`]</div>
|
||||
//! <div style="background-color: #faf5ff"></div> <div style="background-color: #f3e8ff"></div>
|
||||
//! <div style="background-color: #e9d5ff"></div> <div style="background-color: #d8b4fe"></div>
|
||||
//! <div style="background-color: #c084fc"></div> <div style="background-color: #a855f7"></div>
|
||||
//! <div style="background-color: #9333ea"></div> <div style="background-color: #7e22ce"></div>
|
||||
//! <div style="background-color: #6b21a8"></div> <div style="background-color: #581c87"></div>
|
||||
//! <div style="background-color: #4c136e"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`FUCHSIA`]</div>
|
||||
//! <div style="background-color: #fdf4ff"></div> <div style="background-color: #fae8ff"></div>
|
||||
//! <div style="background-color: #f5d0fe"></div> <div style="background-color: #f0abfc"></div>
|
||||
//! <div style="background-color: #e879f9"></div> <div style="background-color: #d946ef"></div>
|
||||
//! <div style="background-color: #c026d3"></div> <div style="background-color: #a21caf"></div>
|
||||
//! <div style="background-color: #86198f"></div> <div style="background-color: #701a75"></div>
|
||||
//! <div style="background-color: #4e145b"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`PINK`]</div>
|
||||
//! <div style="background-color: #fdf2f8"></div> <div style="background-color: #fce7f3"></div>
|
||||
//! <div style="background-color: #fbcfe8"></div> <div style="background-color: #f9a8d4"></div>
|
||||
//! <div style="background-color: #f472b6"></div> <div style="background-color: #ec4899"></div>
|
||||
//! <div style="background-color: #db2777"></div> <div style="background-color: #be185d"></div>
|
||||
//! <div style="background-color: #9d174d"></div> <div style="background-color: #831843"></div>
|
||||
//! <div style="background-color: #5f0b37"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`BLACK`]</div>
|
||||
//! <div style="background-color: #000000; width:22rem"></div>
|
||||
//! </div>
|
||||
//! <div class="color">
|
||||
//! <div class="name">
|
||||
//!
|
||||
//! [`WHITE`]</div>
|
||||
//! <div style="background-color: #ffffff; width:22rem"></div>
|
||||
//! </div>
|
||||
//! </div>
|
||||
//! </div>
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use ratatui::prelude::*;
|
||||
//! use ratatui::style::palette::tailwind::{BLUE, RED};
|
||||
//!
|
||||
//! assert_eq!(RED.c500, Color::Rgb(239, 68, 68));
|
||||
//! assert_eq!(BLUE.c500, Color::Rgb(59, 130, 246));
|
||||
//! ```
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
pub struct Palette {
|
||||
pub c50: Color,
|
||||
pub c100: Color,
|
||||
pub c200: Color,
|
||||
pub c300: Color,
|
||||
pub c400: Color,
|
||||
pub c500: Color,
|
||||
pub c600: Color,
|
||||
pub c700: Color,
|
||||
pub c800: Color,
|
||||
pub c900: Color,
|
||||
pub c950: Color,
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
/// <style>.palette div{width:22rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #000000"></div></div>
|
||||
pub const BLACK: Color = Color::from_u32(0x000000);
|
||||
|
||||
#[rustfmt::skip]
|
||||
/// <style>.palette div{width:22rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #ffffff"></div></div>
|
||||
pub const WHITE: Color = Color::from_u32(0xffffff);
|
||||
|
||||
#[rustfmt::skip]
|
||||
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #f8fafc"></div><div style="background-color: #f1f5f9"></div><div style="background-color: #e2e8f0"></div><div style="background-color: #cbd5e1"></div><div style="background-color: #94a3b8"></div><div style="background-color: #64748b"></div><div style="background-color: #475569"></div><div style="background-color: #334155"></div><div style="background-color: #1e293b"></div><div style="background-color: #0f172a"></div><div style="background-color: #020617"></div></div>
|
||||
pub const SLATE: Palette = Palette {
|
||||
c50: Color::from_u32(0xf8fafc),
|
||||
c100: Color::from_u32(0xf1f5f9),
|
||||
c200: Color::from_u32(0xe2e8f0),
|
||||
c300: Color::from_u32(0xcbd5e1),
|
||||
c400: Color::from_u32(0x94a3b8),
|
||||
c500: Color::from_u32(0x64748b),
|
||||
c600: Color::from_u32(0x475569),
|
||||
c700: Color::from_u32(0x334155),
|
||||
c800: Color::from_u32(0x1e293b),
|
||||
c900: Color::from_u32(0x0f172a),
|
||||
c950: Color::from_u32(0x020617),
|
||||
};
|
||||
|
||||
#[rustfmt::skip]
|
||||
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #f9fafb"></div><div style="background-color: #f3f4f6"></div><div style="background-color: #e5e7eb"></div><div style="background-color: #d1d5db"></div><div style="background-color: #9ca3af"></div><div style="background-color: #6b7280"></div><div style="background-color: #4b5563"></div><div style="background-color: #374151"></div><div style="background-color: #1f2937"></div><div style="background-color: #111827"></div><div style="background-color: #030712"></div></div>
|
||||
pub const GRAY: Palette = Palette {
|
||||
c50: Color::from_u32(0xf9fafb),
|
||||
c100: Color::from_u32(0xf3f4f6),
|
||||
c200: Color::from_u32(0xe5e7eb),
|
||||
c300: Color::from_u32(0xd1d5db),
|
||||
c400: Color::from_u32(0x9ca3af),
|
||||
c500: Color::from_u32(0x6b7280),
|
||||
c600: Color::from_u32(0x4b5563),
|
||||
c700: Color::from_u32(0x374151),
|
||||
c800: Color::from_u32(0x1f2937),
|
||||
c900: Color::from_u32(0x111827),
|
||||
c950: Color::from_u32(0x030712),
|
||||
};
|
||||
|
||||
#[rustfmt::skip]
|
||||
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #fafafa"></div><div style="background-color: #f5f5f5"></div><div style="background-color: #e5e5e5"></div><div style="background-color: #d4d4d4"></div><div style="background-color: #a1a1aa"></div><div style="background-color: #71717a"></div><div style="background-color: #52525b"></div><div style="background-color: #404040"></div><div style="background-color: #262626"></div><div style="background-color: #171717"></div><div style="background-color: #09090b"></div></div>
|
||||
pub const ZINC: Palette = Palette {
|
||||
c50: Color::from_u32(0xfafafa),
|
||||
c100: Color::from_u32(0xf4f4f5),
|
||||
c200: Color::from_u32(0xe4e4e7),
|
||||
c300: Color::from_u32(0xd4d4d8),
|
||||
c400: Color::from_u32(0xa1a1aa),
|
||||
c500: Color::from_u32(0x71717a),
|
||||
c600: Color::from_u32(0x52525b),
|
||||
c700: Color::from_u32(0x3f3f46),
|
||||
c800: Color::from_u32(0x27272a),
|
||||
c900: Color::from_u32(0x18181b),
|
||||
c950: Color::from_u32(0x09090b),
|
||||
};
|
||||
|
||||
#[rustfmt::skip]
|
||||
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #fafafa"></div><div style="background-color: #f5f5f5"></div><div style="background-color: #e5e5e5"></div><div style="background-color: #d4d4d4"></div><div style="background-color: #a3a3a3"></div><div style="background-color: #737373"></div><div style="background-color: #525252"></div><div style="background-color: #404040"></div><div style="background-color: #262626"></div><div style="background-color: #171717"></div><div style="background-color: #0a0a0a"></div></div>
|
||||
pub const NEUTRAL: Palette = Palette {
|
||||
c50: Color::from_u32(0xfafafa),
|
||||
c100: Color::from_u32(0xf5f5f5),
|
||||
c200: Color::from_u32(0xe5e5e5),
|
||||
c300: Color::from_u32(0xd4d4d4),
|
||||
c400: Color::from_u32(0xa3a3a3),
|
||||
c500: Color::from_u32(0x737373),
|
||||
c600: Color::from_u32(0x525252),
|
||||
c700: Color::from_u32(0x404040),
|
||||
c800: Color::from_u32(0x262626),
|
||||
c900: Color::from_u32(0x171717),
|
||||
c950: Color::from_u32(0x0a0a0a),
|
||||
};
|
||||
|
||||
#[rustfmt::skip]
|
||||
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #fafaf9"></div><div style="background-color: #f5f5f4"></div><div style="background-color: #e7e5e4"></div><div style="background-color: #d6d3d1"></div><div style="background-color: #a8a29e"></div><div style="background-color: #78716c"></div><div style="background-color: #57534e"></div><div style="background-color: #44403c"></div><div style="background-color: #292524"></div><div style="background-color: #1c1917"></div><div style="background-color: #0c0a09"></div></div>
|
||||
pub const STONE: Palette = Palette {
|
||||
c50: Color::from_u32(0xfafaf9),
|
||||
c100: Color::from_u32(0xf5f5f4),
|
||||
c200: Color::from_u32(0xe7e5e4),
|
||||
c300: Color::from_u32(0xd6d3d1),
|
||||
c400: Color::from_u32(0xa8a29e),
|
||||
c500: Color::from_u32(0x78716c),
|
||||
c600: Color::from_u32(0x57534e),
|
||||
c700: Color::from_u32(0x44403c),
|
||||
c800: Color::from_u32(0x292524),
|
||||
c900: Color::from_u32(0x1c1917),
|
||||
c950: Color::from_u32(0x0c0a09),
|
||||
};
|
||||
|
||||
#[rustfmt::skip]
|
||||
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #fef2f2"></div><div style="background-color: #fee2e2"></div><div style="background-color: #fecaca"></div><div style="background-color: #fca5a5"></div><div style="background-color: #f87171"></div><div style="background-color: #ef4444"></div><div style="background-color: #dc2626"></div><div style="background-color: #b91c1c"></div><div style="background-color: #991b1b"></div><div style="background-color: #7f1d1d"></div><div style="background-color: #450a0a"></div></div>
|
||||
pub const RED: Palette = Palette {
|
||||
c50: Color::from_u32(0xfef2f2),
|
||||
c100: Color::from_u32(0xfee2e2),
|
||||
c200: Color::from_u32(0xfecaca),
|
||||
c300: Color::from_u32(0xfca5a5),
|
||||
c400: Color::from_u32(0xf87171),
|
||||
c500: Color::from_u32(0xef4444),
|
||||
c600: Color::from_u32(0xdc2626),
|
||||
c700: Color::from_u32(0xb91c1c),
|
||||
c800: Color::from_u32(0x991b1b),
|
||||
c900: Color::from_u32(0x7f1d1d),
|
||||
c950: Color::from_u32(0x450a0a),
|
||||
};
|
||||
|
||||
#[rustfmt::skip]
|
||||
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #fff7ed"></div><div style="background-color: #ffedd5"></div><div style="background-color: #fed7aa"></div><div style="background-color: #fdba74"></div><div style="background-color: #fb923c"></div><div style="background-color: #f97316"></div><div style="background-color: #ea580c"></div><div style="background-color: #c2410c"></div><div style="background-color: #9a3412"></div><div style="background-color: #7c2d12"></div><div style="background-color: #431407"></div></div>
|
||||
pub const ORANGE: Palette = Palette {
|
||||
c50: Color::from_u32(0xfff7ed),
|
||||
c100: Color::from_u32(0xffedd5),
|
||||
c200: Color::from_u32(0xfed7aa),
|
||||
c300: Color::from_u32(0xfdba74),
|
||||
c400: Color::from_u32(0xfb923c),
|
||||
c500: Color::from_u32(0xf97316),
|
||||
c600: Color::from_u32(0xea580c),
|
||||
c700: Color::from_u32(0xc2410c),
|
||||
c800: Color::from_u32(0x9a3412),
|
||||
c900: Color::from_u32(0x7c2d12),
|
||||
c950: Color::from_u32(0x431407),
|
||||
};
|
||||
|
||||
#[rustfmt::skip]
|
||||
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #fffbeb"></div><div style="background-color: #fef3c7"></div><div style="background-color: #fde68a"></div><div style="background-color: #fcd34d"></div><div style="background-color: #fbbf24"></div><div style="background-color: #f59e0b"></div><div style="background-color: #d97706"></div><div style="background-color: #b45309"></div><div style="background-color: #92400e"></div><div style="background-color: #78350f"></div><div style="background-color: #451a03"></div></div>
|
||||
pub const AMBER: Palette = Palette {
|
||||
c50: Color::from_u32(0xfffbeb),
|
||||
c100: Color::from_u32(0xfef3c7),
|
||||
c200: Color::from_u32(0xfde68a),
|
||||
c300: Color::from_u32(0xfcd34d),
|
||||
c400: Color::from_u32(0xfbbf24),
|
||||
c500: Color::from_u32(0xf59e0b),
|
||||
c600: Color::from_u32(0xd97706),
|
||||
c700: Color::from_u32(0xb45309),
|
||||
c800: Color::from_u32(0x92400e),
|
||||
c900: Color::from_u32(0x78350f),
|
||||
c950: Color::from_u32(0x451a03),
|
||||
};
|
||||
|
||||
#[rustfmt::skip]
|
||||
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #fefce8"></div><div style="background-color: #fef9c3"></div><div style="background-color: #fef08a"></div><div style="background-color: #fde047"></div><div style="background-color: #facc15"></div><div style="background-color: #eab308"></div><div style="background-color: #ca8a04"></div><div style="background-color: #a16207"></div><div style="background-color: #854d0e"></div><div style="background-color: #713f12"></div><div style="background-color: #422006"></div></div>
|
||||
pub const YELLOW: Palette = Palette {
|
||||
c50: Color::from_u32(0xfefce8),
|
||||
c100: Color::from_u32(0xfef9c3),
|
||||
c200: Color::from_u32(0xfef08a),
|
||||
c300: Color::from_u32(0xfde047),
|
||||
c400: Color::from_u32(0xfacc15),
|
||||
c500: Color::from_u32(0xeab308),
|
||||
c600: Color::from_u32(0xca8a04),
|
||||
c700: Color::from_u32(0xa16207),
|
||||
c800: Color::from_u32(0x854d0e),
|
||||
c900: Color::from_u32(0x713f12),
|
||||
c950: Color::from_u32(0x422006),
|
||||
};
|
||||
|
||||
#[rustfmt::skip]
|
||||
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #f7fee7"></div><div style="background-color: #ecfccb"></div><div style="background-color: #d9f99d"></div><div style="background-color: #bef264"></div><div style="background-color: #a3e635"></div><div style="background-color: #84cc16"></div><div style="background-color: #65a30d"></div><div style="background-color: #4d7c0f"></div><div style="background-color: #3f6212"></div><div style="background-color: #365314"></div><div style="background-color: #1a2e05"></div></div>
|
||||
pub const LIME: Palette = Palette {
|
||||
c50: Color::from_u32(0xf7fee7),
|
||||
c100: Color::from_u32(0xecfccb),
|
||||
c200: Color::from_u32(0xd9f99d),
|
||||
c300: Color::from_u32(0xbef264),
|
||||
c400: Color::from_u32(0xa3e635),
|
||||
c500: Color::from_u32(0x84cc16),
|
||||
c600: Color::from_u32(0x65a30d),
|
||||
c700: Color::from_u32(0x4d7c0f),
|
||||
c800: Color::from_u32(0x3f6212),
|
||||
c900: Color::from_u32(0x365314),
|
||||
c950: Color::from_u32(0x1a2e05),
|
||||
};
|
||||
|
||||
#[rustfmt::skip]
|
||||
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #f0fdf4"></div><div style="background-color: #dcfce7"></div><div style="background-color: #bbf7d0"></div><div style="background-color: #86efac"></div><div style="background-color: #4ade80"></div><div style="background-color: #22c55e"></div><div style="background-color: #16a34a"></div><div style="background-color: #15803d"></div><div style="background-color: #166534"></div><div style="background-color: #14532d"></div><div style="background-color: #052e16"></div></div>
|
||||
pub const GREEN: Palette = Palette {
|
||||
c50: Color::from_u32(0xf0fdf4),
|
||||
c100: Color::from_u32(0xdcfce7),
|
||||
c200: Color::from_u32(0xbbf7d0),
|
||||
c300: Color::from_u32(0x86efac),
|
||||
c400: Color::from_u32(0x4ade80),
|
||||
c500: Color::from_u32(0x22c55e),
|
||||
c600: Color::from_u32(0x16a34a),
|
||||
c700: Color::from_u32(0x15803d),
|
||||
c800: Color::from_u32(0x166534),
|
||||
c900: Color::from_u32(0x14532d),
|
||||
c950: Color::from_u32(0x052e16),
|
||||
};
|
||||
|
||||
#[rustfmt::skip]
|
||||
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #f0fdfa"></div><div style="background-color: #ccfbf1"></div><div style="background-color: #99f6e4"></div><div style="background-color: #5eead4"></div><div style="background-color: #2dd4bf"></div><div style="background-color: #14b8a6"></div><div style="background-color: #0d9488"></div><div style="background-color: #0f766e"></div><div style="background-color: #115e59"></div><div style="background-color: #134e4a"></div><div style="background-color: #042f2e"></div></div>
|
||||
pub const EMERALD: Palette = Palette {
|
||||
c50: Color::from_u32(0xecfdf5),
|
||||
c100: Color::from_u32(0xd1fae5),
|
||||
c200: Color::from_u32(0xa7f3d0),
|
||||
c300: Color::from_u32(0x6ee7b7),
|
||||
c400: Color::from_u32(0x34d399),
|
||||
c500: Color::from_u32(0x10b981),
|
||||
c600: Color::from_u32(0x059669),
|
||||
c700: Color::from_u32(0x047857),
|
||||
c800: Color::from_u32(0x065f46),
|
||||
c900: Color::from_u32(0x064e3b),
|
||||
c950: Color::from_u32(0x022c22),
|
||||
};
|
||||
|
||||
#[rustfmt::skip]
|
||||
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #f5fdf4"></div><div style="background-color: #e7f9e7"></div><div style="background-color: #c6f6d5"></div><div style="background-color: #9ae6b4"></div><div style="background-color: #68d391"></div><div style="background-color: #48bb78"></div><div style="background-color: #38a169"></div><div style="background-color: #2f855a"></div><div style="background-color: #276749"></div><div style="background-color: #22543d"></div><div style="background-color: #0d3321"></div></div>
|
||||
pub const TEAL: Palette = Palette {
|
||||
c50: Color::from_u32(0xf0fdfa),
|
||||
c100: Color::from_u32(0xccfbf1),
|
||||
c200: Color::from_u32(0x99f6e4),
|
||||
c300: Color::from_u32(0x5eead4),
|
||||
c400: Color::from_u32(0x2dd4bf),
|
||||
c500: Color::from_u32(0x14b8a6),
|
||||
c600: Color::from_u32(0x0d9488),
|
||||
c700: Color::from_u32(0x0f766e),
|
||||
c800: Color::from_u32(0x115e59),
|
||||
c900: Color::from_u32(0x134e4a),
|
||||
c950: Color::from_u32(0x042f2e),
|
||||
};
|
||||
|
||||
#[rustfmt::skip]
|
||||
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #ecfeff"></div><div style="background-color: #cffafe"></div><div style="background-color: #a5f3fc"></div><div style="background-color: #67e8f9"></div><div style="background-color: #22d3ee"></div><div style="background-color: #06b6d4"></div><div style="background-color: #0891b2"></div><div style="background-color: #0e7490"></div><div style="background-color: #155e75"></div><div style="background-color: #164e63"></div><div style="background-color: #083344"></div></div>
|
||||
pub const CYAN: Palette = Palette {
|
||||
c50: Color::from_u32(0xecfeff),
|
||||
c100: Color::from_u32(0xcffafe),
|
||||
c200: Color::from_u32(0xa5f3fc),
|
||||
c300: Color::from_u32(0x67e8f9),
|
||||
c400: Color::from_u32(0x22d3ee),
|
||||
c500: Color::from_u32(0x06b6d4),
|
||||
c600: Color::from_u32(0x0891b2),
|
||||
c700: Color::from_u32(0x0e7490),
|
||||
c800: Color::from_u32(0x155e75),
|
||||
c900: Color::from_u32(0x164e63),
|
||||
c950: Color::from_u32(0x083344),
|
||||
};
|
||||
|
||||
#[rustfmt::skip]
|
||||
/// <style>.palette div{width:22rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #f0f9ff"></div><div style="background-color: #e0f2fe"></div><div style="background-color: #bae6fd"></div><div style="background-color: #7dd3fc"></div><div style="background-color: #38bdf8"></div><div style="background-color: #0ea5e9"></div><div style="background-color: #0284c7"></div><div style="background-color: #0369a1"></div><div style="background-color: #075985"></div><div style="background-color: #0c4a6e"></div><div style="background-color: #082f49"></div></div>
|
||||
pub const SKY: Palette = Palette {
|
||||
c50: Color::from_u32(0xf0f9ff),
|
||||
c100: Color::from_u32(0xe0f2fe),
|
||||
c200: Color::from_u32(0xbae6fd),
|
||||
c300: Color::from_u32(0x7dd3fc),
|
||||
c400: Color::from_u32(0x38bdf8),
|
||||
c500: Color::from_u32(0x0ea5e9),
|
||||
c600: Color::from_u32(0x0284c7),
|
||||
c700: Color::from_u32(0x0369a1),
|
||||
c800: Color::from_u32(0x075985),
|
||||
c900: Color::from_u32(0x0c4a6e),
|
||||
c950: Color::from_u32(0x082f49),
|
||||
};
|
||||
|
||||
#[rustfmt::skip]
|
||||
/// <style>.palette div{width:22rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #eff6ff"></div><div style="background-color: #dbeafe"></div><div style="background-color: #bfdbfe"></div><div style="background-color: #93c5fd"></div><div style="background-color: #60a5fa"></div><div style="background-color: #3b82f6"></div><div style="background-color: #2563eb"></div><div style="background-color: #1d4ed8"></div><div style="background-color: #1e40af"></div><div style="background-color: #1e3a8a"></div><div style="background-color: #172554"></div></div>
|
||||
pub const BLUE: Palette = Palette {
|
||||
c50: Color::from_u32(0xeff6ff),
|
||||
c100: Color::from_u32(0xdbeafe),
|
||||
c200: Color::from_u32(0xbfdbfe),
|
||||
c300: Color::from_u32(0x93c5fd),
|
||||
c400: Color::from_u32(0x60a5fa),
|
||||
c500: Color::from_u32(0x3b82f6),
|
||||
c600: Color::from_u32(0x2563eb),
|
||||
c700: Color::from_u32(0x1d4ed8),
|
||||
c800: Color::from_u32(0x1e40af),
|
||||
c900: Color::from_u32(0x1e3a8a),
|
||||
c950: Color::from_u32(0x172554),
|
||||
};
|
||||
|
||||
#[rustfmt::skip]
|
||||
/// <style>.palette div{width:22rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #eef2ff"></div><div style="background-color: #e0e7ff"></div><div style="background-color: #c7d2fe"></div><div style="background-color: #a5b4fc"></div><div style="background-color: #818cf8"></div><div style="background-color: #6366f1"></div><div style="background-color: #4f46e5"></div><div style="background-color: #4338ca"></div><div style="background-color: #3730a3"></div><div style="background-color: #312e81"></div><div style="background-color: #1e1b4b"></div></div>
|
||||
pub const INDIGO: Palette = Palette {
|
||||
c50: Color::from_u32(0xeef2ff),
|
||||
c100: Color::from_u32(0xe0e7ff),
|
||||
c200: Color::from_u32(0xc7d2fe),
|
||||
c300: Color::from_u32(0xa5b4fc),
|
||||
c400: Color::from_u32(0x818cf8),
|
||||
c500: Color::from_u32(0x6366f1),
|
||||
c600: Color::from_u32(0x4f46e5),
|
||||
c700: Color::from_u32(0x4338ca),
|
||||
c800: Color::from_u32(0x3730a3),
|
||||
c900: Color::from_u32(0x312e81),
|
||||
c950: Color::from_u32(0x1e1b4b),
|
||||
};
|
||||
|
||||
#[rustfmt::skip]
|
||||
/// <style>.palette div{width:22rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #f5f3ff"></div><div style="background-color: #ede9fe"></div><div style="background-color: #ddd6fe"></div><div style="background-color: #c4b5fd"></div><div style="background-color: #a78bfa"></div><div style="background-color: #8b5cf6"></div><div style="background-color: #7c3aed"></div><div style="background-color: #6d28d9"></div><div style="background-color: #5b21b6"></div><div style="background-color: #4c1d95"></div><div style="background-color: #2e1065"></div></div>
|
||||
pub const VIOLET: Palette = Palette {
|
||||
c50: Color::from_u32(0xf5f3ff),
|
||||
c100: Color::from_u32(0xede9fe),
|
||||
c200: Color::from_u32(0xddd6fe),
|
||||
c300: Color::from_u32(0xc4b5fd),
|
||||
c400: Color::from_u32(0xa78bfa),
|
||||
c500: Color::from_u32(0x8b5cf6),
|
||||
c600: Color::from_u32(0x7c3aed),
|
||||
c700: Color::from_u32(0x6d28d9),
|
||||
c800: Color::from_u32(0x5b21b6),
|
||||
c900: Color::from_u32(0x4c1d95),
|
||||
c950: Color::from_u32(0x2e1065),
|
||||
};
|
||||
|
||||
#[rustfmt::skip]
|
||||
/// <style>.palette div{width:22rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #faf5ff"></div><div style="background-color: #f3e8ff"></div><div style="background-color: #e9d5ff"></div><div style="background-color: #d8b4fe"></div><div style="background-color: #c084fc"></div><div style="background-color: #a855f7"></div><div style="background-color: #9333ea"></div><div style="background-color: #7e22ce"></div><div style="background-color: #6b21a8"></div><div style="background-color: #581c87"></div><div style="background-color: #3b0764"></div></div>
|
||||
pub const PURPLE: Palette = Palette {
|
||||
c50: Color::from_u32(0xfaf5ff),
|
||||
c100: Color::from_u32(0xf3e8ff),
|
||||
c200: Color::from_u32(0xe9d5ff),
|
||||
c300: Color::from_u32(0xd8b4fe),
|
||||
c400: Color::from_u32(0xc084fc),
|
||||
c500: Color::from_u32(0xa855f7),
|
||||
c600: Color::from_u32(0x9333ea),
|
||||
c700: Color::from_u32(0x7e22ce),
|
||||
c800: Color::from_u32(0x6b21a8),
|
||||
c900: Color::from_u32(0x581c87),
|
||||
c950: Color::from_u32(0x3b0764),
|
||||
};
|
||||
|
||||
#[rustfmt::skip]
|
||||
/// <style>.palette div{width:22rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #fdf4ff"></div><div style="background-color: #fae8ff"></div><div style="background-color: #f5d0fe"></div><div style="background-color: #f0abfc"></div><div style="background-color: #e879f9"></div><div style="background-color: #d946ef"></div><div style="background-color: #c026d3"></div><div style="background-color: #a21caf"></div><div style="background-color: #86198f"></div><div style="background-color: #701a75"></div><div style="background-color: #4a044e"></div></div>
|
||||
pub const FUCHSIA: Palette = Palette {
|
||||
c50: Color::from_u32(0xfdf4ff),
|
||||
c100: Color::from_u32(0xfae8ff),
|
||||
c200: Color::from_u32(0xf5d0fe),
|
||||
c300: Color::from_u32(0xf0abfc),
|
||||
c400: Color::from_u32(0xe879f9),
|
||||
c500: Color::from_u32(0xd946ef),
|
||||
c600: Color::from_u32(0xc026d3),
|
||||
c700: Color::from_u32(0xa21caf),
|
||||
c800: Color::from_u32(0x86198f),
|
||||
c900: Color::from_u32(0x701a75),
|
||||
c950: Color::from_u32(0x4a044e),
|
||||
};
|
||||
|
||||
#[rustfmt::skip]
|
||||
/// <style>.palette div{width:22rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #fdf2f8"></div><div style="background-color: #fce7f3"></div><div style="background-color: #fbcfe8"></div><div style="background-color: #f9a8d4"></div><div style="background-color: #f472b6"></div><div style="background-color: #ec4899"></div><div style="background-color: #db2777"></div><div style="background-color: #be185d"></div><div style="background-color: #9d174d"></div><div style="background-color: #831843"></div><div style="background-color: #500724"></div></div>
|
||||
pub const PINK: Palette = Palette {
|
||||
c50: Color::from_u32(0xfdf2f8),
|
||||
c100: Color::from_u32(0xfce7f3),
|
||||
c200: Color::from_u32(0xfbcfe8),
|
||||
c300: Color::from_u32(0xf9a8d4),
|
||||
c400: Color::from_u32(0xf472b6),
|
||||
c500: Color::from_u32(0xec4899),
|
||||
c600: Color::from_u32(0xdb2777),
|
||||
c700: Color::from_u32(0xbe185d),
|
||||
c800: Color::from_u32(0x9d174d),
|
||||
c900: Color::from_u32(0x831843),
|
||||
c950: Color::from_u32(0x500724),
|
||||
};
|
||||
|
||||
#[rustfmt::skip]
|
||||
/// <style>.palette div{width:22rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #fff1f2"></div><div style="background-color: #ffe4e6"></div><div style="background-color: #fecdd3"></div><div style="background-color: #fda4af"></div><div style="background-color: #fb7185"></div><div style="background-color: #f43f5e"></div><div style="background-color: #e11d48"></div><div style="background-color: #be123c"></div><div style="background-color: #9f1239"></div><div style="background-color: #881337"></div><div style="background-color: #4c0519"></div></div>
|
||||
pub const ROSE: Palette = Palette {
|
||||
c50: Color::from_u32(0xfff1f2),
|
||||
c100: Color::from_u32(0xffe4e6),
|
||||
c200: Color::from_u32(0xfecdd3),
|
||||
c300: Color::from_u32(0xfda4af),
|
||||
c400: Color::from_u32(0xfb7185),
|
||||
c500: Color::from_u32(0xf43f5e),
|
||||
c600: Color::from_u32(0xe11d48),
|
||||
c700: Color::from_u32(0xbe123c),
|
||||
c800: Color::from_u32(0x9f1239),
|
||||
c900: Color::from_u32(0x881337),
|
||||
c950: Color::from_u32(0x4c0519),
|
||||
};
|
||||
@@ -13,8 +13,14 @@ use crate::{
|
||||
pub trait Styled {
|
||||
type Item;
|
||||
|
||||
/// Returns the style of the object.
|
||||
fn style(&self) -> Style;
|
||||
fn set_style(self, style: Style) -> Self::Item;
|
||||
|
||||
/// Sets the style of the object.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item;
|
||||
}
|
||||
|
||||
/// Generates two methods for each color, one for setting the foreground color (`red()`, `blue()`,
|
||||
@@ -40,11 +46,13 @@ macro_rules! color {
|
||||
( $color:ident ) => {
|
||||
paste! {
|
||||
#[doc = "Sets the foreground color to [`" $color "`](Color::" $color:camel ")."]
|
||||
#[must_use = concat!("`", stringify!($color), "` returns the modified style without modifying the original")]
|
||||
fn $color(self) -> T {
|
||||
self.fg(Color::[<$color:camel>])
|
||||
}
|
||||
|
||||
#[doc = "Sets the background color to [`" $color "`](Color::" $color:camel ")."]
|
||||
#[must_use = concat!("`on_", stringify!($color), "` returns the modified style without modifying the original")]
|
||||
fn [<on_ $color>](self) -> T {
|
||||
self.bg(Color::[<$color:camel>])
|
||||
}
|
||||
@@ -76,6 +84,7 @@ macro_rules! modifier {
|
||||
( $modifier:ident ) => {
|
||||
paste! {
|
||||
#[doc = "Adds the [`" $modifier:upper "`](Modifier::" $modifier:upper ") modifier."]
|
||||
#[must_use = concat!("`", stringify!($modifier), "` returns the modified style without modifying the original")]
|
||||
fn [<$modifier>](self) -> T {
|
||||
self.add_modifier(Modifier::[<$modifier:upper>])
|
||||
}
|
||||
@@ -83,6 +92,7 @@ macro_rules! modifier {
|
||||
|
||||
paste! {
|
||||
#[doc = "Removes the [`" $modifier:upper "`](Modifier::" $modifier:upper ") modifier."]
|
||||
#[must_use = concat!("`not_", stringify!($modifier), "` returns the modified style without modifying the original")]
|
||||
fn [<not_ $modifier>](self) -> T {
|
||||
self.remove_modifier(Modifier::[<$modifier:upper>])
|
||||
}
|
||||
@@ -123,13 +133,22 @@ macro_rules! modifier {
|
||||
/// "world".green().on_yellow().not_bold(),
|
||||
/// ]);
|
||||
/// let paragraph = Paragraph::new(line).italic().underlined();
|
||||
/// let block = Block::default().title("Title").borders(Borders::ALL).on_white().bold();
|
||||
/// let block = Block::default()
|
||||
/// .title("Title")
|
||||
/// .borders(Borders::ALL)
|
||||
/// .on_white()
|
||||
/// .bold();
|
||||
/// ```
|
||||
pub trait Stylize<'a, T>: Sized {
|
||||
#[must_use = "`bg` returns the modified style without modifying the original"]
|
||||
fn bg(self, color: Color) -> T;
|
||||
#[must_use = "`fg` returns the modified style without modifying the original"]
|
||||
fn fg<S: Into<Color>>(self, color: S) -> T;
|
||||
#[must_use = "`reset` returns the modified style without modifying the original"]
|
||||
fn reset(self) -> T;
|
||||
#[must_use = "`add_modifier` returns the modified style without modifying the original"]
|
||||
fn add_modifier(self, modifier: Modifier) -> T;
|
||||
#[must_use = "`remove_modifier` returns the modified style without modifying the original"]
|
||||
fn remove_modifier(self, modifier: Modifier) -> T;
|
||||
|
||||
color!(black);
|
||||
@@ -196,7 +215,7 @@ impl<'a> Styled for &'a str {
|
||||
Style::default()
|
||||
}
|
||||
|
||||
fn set_style(self, style: Style) -> Self::Item {
|
||||
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
|
||||
Span::styled(self, style)
|
||||
}
|
||||
}
|
||||
@@ -208,7 +227,7 @@ impl Styled for String {
|
||||
Style::default()
|
||||
}
|
||||
|
||||
fn set_style(self, style: Style) -> Self::Item {
|
||||
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
|
||||
Span::styled(self, style)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -381,6 +381,93 @@ pub mod border {
|
||||
horizontal_top: QUADRANT_BOTTOM_HALF,
|
||||
horizontal_bottom: QUADRANT_TOP_HALF,
|
||||
};
|
||||
|
||||
pub const ONE_EIGHTH_TOP_EIGHT: &str = "▔";
|
||||
pub const ONE_EIGHTH_BOTTOM_EIGHT: &str = "▁";
|
||||
pub const ONE_EIGHTH_LEFT_EIGHT: &str = "▏";
|
||||
pub const ONE_EIGHTH_RIGHT_EIGHT: &str = "▕";
|
||||
|
||||
/// Wide border set based on McGugan box technique
|
||||
///
|
||||
/// ```text
|
||||
/// ▁▁▁▁▁▁▁
|
||||
/// ▏xxxxx▕
|
||||
/// ▏xxxxx▕
|
||||
/// ▔▔▔▔▔▔▔
|
||||
/// ```
|
||||
pub const ONE_EIGHTH_WIDE: Set = Set {
|
||||
top_right: ONE_EIGHTH_BOTTOM_EIGHT,
|
||||
top_left: ONE_EIGHTH_BOTTOM_EIGHT,
|
||||
bottom_right: ONE_EIGHTH_TOP_EIGHT,
|
||||
bottom_left: ONE_EIGHTH_TOP_EIGHT,
|
||||
vertical_left: ONE_EIGHTH_LEFT_EIGHT,
|
||||
vertical_right: ONE_EIGHTH_RIGHT_EIGHT,
|
||||
horizontal_top: ONE_EIGHTH_BOTTOM_EIGHT,
|
||||
horizontal_bottom: ONE_EIGHTH_TOP_EIGHT,
|
||||
};
|
||||
|
||||
/// Tall border set based on McGugan box technique
|
||||
///
|
||||
/// ```text
|
||||
/// ▕▔▔▏
|
||||
/// ▕xx▏
|
||||
/// ▕xx▏
|
||||
/// ▕▁▁▏
|
||||
/// ```
|
||||
pub const ONE_EIGHTH_TALL: Set = Set {
|
||||
top_right: ONE_EIGHTH_LEFT_EIGHT,
|
||||
top_left: ONE_EIGHTH_RIGHT_EIGHT,
|
||||
bottom_right: ONE_EIGHTH_LEFT_EIGHT,
|
||||
bottom_left: ONE_EIGHTH_RIGHT_EIGHT,
|
||||
vertical_left: ONE_EIGHTH_RIGHT_EIGHT,
|
||||
vertical_right: ONE_EIGHTH_LEFT_EIGHT,
|
||||
horizontal_top: ONE_EIGHTH_TOP_EIGHT,
|
||||
horizontal_bottom: ONE_EIGHTH_BOTTOM_EIGHT,
|
||||
};
|
||||
|
||||
/// Wide proportional (visually equal width and height) border with using set of quadrants.
|
||||
///
|
||||
/// The border is created by using half blocks for top and bottom, and full
|
||||
/// blocks for right and left sides to make horizontal and vertical borders seem equal.
|
||||
///
|
||||
/// ```text
|
||||
/// ▄▄▄▄
|
||||
/// █xx█
|
||||
/// █xx█
|
||||
/// ▀▀▀▀
|
||||
/// ```
|
||||
pub const PROPORTIONAL_WIDE: Set = Set {
|
||||
top_right: QUADRANT_BOTTOM_HALF,
|
||||
top_left: QUADRANT_BOTTOM_HALF,
|
||||
bottom_right: QUADRANT_TOP_HALF,
|
||||
bottom_left: QUADRANT_TOP_HALF,
|
||||
vertical_left: QUADRANT_BLOCK,
|
||||
vertical_right: QUADRANT_BLOCK,
|
||||
horizontal_top: QUADRANT_BOTTOM_HALF,
|
||||
horizontal_bottom: QUADRANT_TOP_HALF,
|
||||
};
|
||||
|
||||
/// Tall proportional (visually equal width and height) border with using set of quadrants.
|
||||
///
|
||||
/// The border is created by using full blocks for all sides, except for the top and bottom,
|
||||
/// which use half blocks to make horizontal and vertical borders seem equal.
|
||||
///
|
||||
/// ```text
|
||||
/// ▕█▀▀█
|
||||
/// ▕█xx█
|
||||
/// ▕█xx█
|
||||
/// ▕█▄▄█
|
||||
/// ```
|
||||
pub const PROPORTIONAL_TALL: Set = Set {
|
||||
top_right: QUADRANT_BLOCK,
|
||||
top_left: QUADRANT_BLOCK,
|
||||
bottom_right: QUADRANT_BLOCK,
|
||||
bottom_left: QUADRANT_BLOCK,
|
||||
vertical_left: QUADRANT_BLOCK,
|
||||
vertical_right: QUADRANT_BLOCK,
|
||||
horizontal_top: QUADRANT_TOP_HALF,
|
||||
horizontal_bottom: QUADRANT_BOTTOM_HALF,
|
||||
};
|
||||
}
|
||||
|
||||
pub const DOT: &str = "•";
|
||||
@@ -398,12 +485,12 @@ pub mod braille {
|
||||
/// Marker to use when plotting data points
|
||||
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub enum Marker {
|
||||
/// One point per cell in shape of dot ("•")
|
||||
/// One point per cell in shape of dot (`•`)
|
||||
#[default]
|
||||
Dot,
|
||||
/// One point per cell in shape of a block ("█")
|
||||
/// One point per cell in shape of a block (`█`)
|
||||
Block,
|
||||
/// One point per cell in the shape of a bar ("▄")
|
||||
/// One point per cell in the shape of a bar (`▄`)
|
||||
Bar,
|
||||
/// Use the [Unicode Braille Patterns](https://en.wikipedia.org/wiki/Braille_Patterns) block to
|
||||
/// represent data points.
|
||||
@@ -412,9 +499,9 @@ pub enum Marker {
|
||||
///
|
||||
/// Note: Support for this marker is limited to terminals and fonts that support Unicode
|
||||
/// Braille Patterns. If your terminal does not support this, you will see unicode replacement
|
||||
/// characters (<EFBFBD>) instead of Braille dots.
|
||||
/// characters (`<60>`) instead of Braille dots (`⠓`, `⣇`, `⣿`).
|
||||
Braille,
|
||||
/// Use the unicode block and half block characters ("█", "▄", and "▀") to represent points in
|
||||
/// Use the unicode block and half block characters (`█`, `▄`, and `▀`) to represent points in
|
||||
/// a grid that is double the resolution of the terminal. Because each terminal cell is
|
||||
/// generally about twice as tall as it is wide, this allows for a square grid of pixels.
|
||||
HalfBlock,
|
||||
|
||||
650
src/terminal.rs
650
src/terminal.rs
@@ -12,6 +12,7 @@
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use std::io::stdout;
|
||||
//!
|
||||
//! use ratatui::{prelude::*, widgets::Paragraph};
|
||||
//!
|
||||
//! let backend = CrosstermBackend::new(stdout());
|
||||
@@ -29,647 +30,12 @@
|
||||
//! [`backend`]: crate::backend
|
||||
//! [`Backend`]: crate::backend::Backend
|
||||
//! [`Buffer`]: crate::buffer::Buffer
|
||||
use std::{fmt, io};
|
||||
|
||||
use crate::{
|
||||
backend::{Backend, ClearType},
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
widgets::{StatefulWidget, Widget},
|
||||
};
|
||||
mod frame;
|
||||
#[allow(clippy::module_inception)]
|
||||
mod terminal;
|
||||
mod viewport;
|
||||
|
||||
/// Represents the viewport of the terminal. The viewport is the area of the terminal that is
|
||||
/// currently visible to the user. It can be either fullscreen, inline or fixed.
|
||||
///
|
||||
/// When the viewport is fullscreen, the whole terminal is used to draw the application.
|
||||
///
|
||||
/// When the viewport is inline, it is drawn inline with the rest of the terminal. The height of
|
||||
/// the viewport is fixed, but the width is the same as the terminal width.
|
||||
///
|
||||
/// When the viewport is fixed, it is drawn in a fixed area of the terminal. The area is specified
|
||||
/// by a [`Rect`].
|
||||
///
|
||||
/// See [`Terminal::with_options`] for more information.
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum Viewport {
|
||||
/// The viewport is fullscreen
|
||||
#[default]
|
||||
Fullscreen,
|
||||
/// The viewport is inline with the rest of the terminal.
|
||||
///
|
||||
/// The viewport's height is fixed and specified in number of lines. The width is the same as
|
||||
/// the terminal's width. The viewport is drawn below the cursor position.
|
||||
Inline(u16),
|
||||
/// The viewport is drawn in a fixed area of the terminal. The area is specified by a [`Rect`].
|
||||
Fixed(Rect),
|
||||
}
|
||||
|
||||
impl fmt::Display for Viewport {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Viewport::Fullscreen => write!(f, "Fullscreen"),
|
||||
Viewport::Inline(height) => write!(f, "Inline({})", height),
|
||||
Viewport::Fixed(area) => write!(f, "Fixed({})", area),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Options to pass to [`Terminal::with_options`]
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct TerminalOptions {
|
||||
/// Viewport used to draw to the terminal
|
||||
pub viewport: Viewport,
|
||||
}
|
||||
|
||||
/// An interface to interact and draw [`Frame`]s on the user's terminal.
|
||||
///
|
||||
/// This is the main entry point for Ratatui. It is responsible for drawing and maintaining the
|
||||
/// state of the buffers, cursor and viewport.
|
||||
///
|
||||
/// The [`Terminal`] is generic over a [`Backend`] implementation which is used to interface with
|
||||
/// the underlying terminal library. The [`Backend`] trait is implemented for three popular Rust
|
||||
/// terminal libraries: [Crossterm], [Termion] and [Termwiz]. See the [`backend`] module for more
|
||||
/// information.
|
||||
///
|
||||
/// The `Terminal` struct maintains two buffers: the current and the previous.
|
||||
/// When the widgets are drawn, the changes are accumulated in the current buffer.
|
||||
/// At the end of each draw pass, the two buffers are compared, and only the changes
|
||||
/// between these buffers are written to the terminal, avoiding any redundant operations.
|
||||
/// After flushing these changes, the buffers are swapped to prepare for the next draw cycle./
|
||||
///
|
||||
/// The terminal also has a viewport which is the area of the terminal that is currently visible to
|
||||
/// the user. It can be either fullscreen, inline or fixed. See [`Viewport`] for more information.
|
||||
///
|
||||
/// Applications should detect terminal resizes and call [`Terminal::draw`] to redraw the
|
||||
/// application with the new size. This will automatically resize the internal buffers to match the
|
||||
/// new size for inline and fullscreen viewports. Fixed viewports are not resized automatically.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use std::io::stdout;
|
||||
/// use ratatui::{prelude::*, widgets::Paragraph};
|
||||
///
|
||||
/// let backend = CrosstermBackend::new(stdout());
|
||||
/// let mut terminal = Terminal::new(backend)?;
|
||||
/// terminal.draw(|frame| {
|
||||
/// let area = frame.size();
|
||||
/// frame.render_widget(Paragraph::new("Hello World!"), area);
|
||||
/// frame.set_cursor(0, 0);
|
||||
/// })?;
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
///
|
||||
/// [Crossterm]: https://crates.io/crates/crossterm
|
||||
/// [Termion]: https://crates.io/crates/termion
|
||||
/// [Termwiz]: https://crates.io/crates/termwiz
|
||||
/// [`backend`]: crate::backend
|
||||
/// [`Backend`]: crate::backend::Backend
|
||||
/// [`Buffer`]: crate::buffer::Buffer
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Terminal<B>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
/// The backend used to interface with the terminal
|
||||
backend: B,
|
||||
/// Holds the results of the current and previous draw calls. The two are compared at the end
|
||||
/// of each draw pass to output the necessary updates to the terminal
|
||||
buffers: [Buffer; 2],
|
||||
/// Index of the current buffer in the previous array
|
||||
current: usize,
|
||||
/// Whether the cursor is currently hidden
|
||||
hidden_cursor: bool,
|
||||
/// Viewport
|
||||
viewport: Viewport,
|
||||
/// Area of the viewport
|
||||
viewport_area: Rect,
|
||||
/// Last known size of the terminal. Used to detect if the internal buffers have to be resized.
|
||||
last_known_size: Rect,
|
||||
/// Last known position of the cursor. Used to find the new area when the viewport is inlined
|
||||
/// and the terminal resized.
|
||||
last_known_cursor_pos: (u16, u16),
|
||||
}
|
||||
|
||||
impl<B> Drop for Terminal<B>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
fn drop(&mut self) {
|
||||
// Attempt to restore the cursor state
|
||||
if self.hidden_cursor {
|
||||
if let Err(err) = self.show_cursor() {
|
||||
eprintln!("Failed to show the cursor: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> Terminal<B>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
/// Creates a new [`Terminal`] with the given [`Backend`] with a full screen viewport.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use std::io::stdout;
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let backend = CrosstermBackend::new(stdout());
|
||||
/// let terminal = Terminal::new(backend)?;
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
pub fn new(backend: B) -> io::Result<Terminal<B>> {
|
||||
Terminal::with_options(
|
||||
backend,
|
||||
TerminalOptions {
|
||||
viewport: Viewport::Fullscreen,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a new [`Terminal`] with the given [`Backend`] and [`TerminalOptions`].
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use std::io::stdout;
|
||||
/// # use ratatui::{prelude::*, backend::TestBackend};
|
||||
/// let backend = CrosstermBackend::new(stdout());
|
||||
/// let viewport = Viewport::Fixed(Rect::new(0, 0, 10, 10));
|
||||
/// let terminal = Terminal::with_options(
|
||||
/// backend,
|
||||
/// TerminalOptions { viewport },
|
||||
/// )?;
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
pub fn with_options(mut backend: B, options: TerminalOptions) -> io::Result<Terminal<B>> {
|
||||
let size = match options.viewport {
|
||||
Viewport::Fullscreen | Viewport::Inline(_) => backend.size()?,
|
||||
Viewport::Fixed(area) => area,
|
||||
};
|
||||
let (viewport_area, cursor_pos) = match options.viewport {
|
||||
Viewport::Fullscreen => (size, (0, 0)),
|
||||
Viewport::Inline(height) => compute_inline_size(&mut backend, height, size, 0)?,
|
||||
Viewport::Fixed(area) => (area, (area.left(), area.top())),
|
||||
};
|
||||
Ok(Terminal {
|
||||
backend,
|
||||
buffers: [Buffer::empty(viewport_area), Buffer::empty(viewport_area)],
|
||||
current: 0,
|
||||
hidden_cursor: false,
|
||||
viewport: options.viewport,
|
||||
viewport_area,
|
||||
last_known_size: size,
|
||||
last_known_cursor_pos: cursor_pos,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get a Frame object which provides a consistent view into the terminal state for rendering.
|
||||
pub fn get_frame(&mut self) -> Frame {
|
||||
Frame {
|
||||
cursor_position: None,
|
||||
viewport_area: self.viewport_area,
|
||||
buffer: self.current_buffer_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the current buffer as a mutable reference.
|
||||
pub fn current_buffer_mut(&mut self) -> &mut Buffer {
|
||||
&mut self.buffers[self.current]
|
||||
}
|
||||
|
||||
/// Gets the backend
|
||||
pub fn backend(&self) -> &B {
|
||||
&self.backend
|
||||
}
|
||||
|
||||
/// Gets the backend as a mutable reference
|
||||
pub fn backend_mut(&mut self) -> &mut B {
|
||||
&mut self.backend
|
||||
}
|
||||
|
||||
/// Obtains a difference between the previous and the current buffer and passes it to the
|
||||
/// current backend for drawing.
|
||||
pub fn flush(&mut self) -> io::Result<()> {
|
||||
let previous_buffer = &self.buffers[1 - self.current];
|
||||
let current_buffer = &self.buffers[self.current];
|
||||
let updates = previous_buffer.diff(current_buffer);
|
||||
if let Some((col, row, _)) = updates.last() {
|
||||
self.last_known_cursor_pos = (*col, *row);
|
||||
}
|
||||
self.backend.draw(updates.into_iter())
|
||||
}
|
||||
|
||||
/// Updates the Terminal so that internal buffers match the requested size.
|
||||
///
|
||||
/// Requested size will be saved so the size can remain consistent when rendering. This leads
|
||||
/// to a full clear of the screen.
|
||||
pub fn resize(&mut self, size: Rect) -> io::Result<()> {
|
||||
let next_area = match self.viewport {
|
||||
Viewport::Fullscreen => size,
|
||||
Viewport::Inline(height) => {
|
||||
let offset_in_previous_viewport = self
|
||||
.last_known_cursor_pos
|
||||
.1
|
||||
.saturating_sub(self.viewport_area.top());
|
||||
compute_inline_size(&mut self.backend, height, size, offset_in_previous_viewport)?.0
|
||||
}
|
||||
Viewport::Fixed(area) => area,
|
||||
};
|
||||
self.set_viewport_area(next_area);
|
||||
self.clear()?;
|
||||
|
||||
self.last_known_size = size;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_viewport_area(&mut self, area: Rect) {
|
||||
self.buffers[self.current].resize(area);
|
||||
self.buffers[1 - self.current].resize(area);
|
||||
self.viewport_area = area;
|
||||
}
|
||||
|
||||
/// Queries the backend for size and resizes if it doesn't match the previous size.
|
||||
pub fn autoresize(&mut self) -> io::Result<()> {
|
||||
// fixed viewports do not get autoresized
|
||||
if matches!(self.viewport, Viewport::Fullscreen | Viewport::Inline(_)) {
|
||||
let size = self.size()?;
|
||||
if size != self.last_known_size {
|
||||
self.resize(size)?;
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Synchronizes terminal size, calls the rendering closure, flushes the current internal state
|
||||
/// and prepares for the next draw call.
|
||||
///
|
||||
/// This is the main entry point for drawing to the terminal.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use std::io::stdout;
|
||||
/// # use ratatui::{prelude::*, widgets::Paragraph};
|
||||
/// let backend = CrosstermBackend::new(stdout());
|
||||
/// let mut terminal = Terminal::new(backend)?;
|
||||
/// terminal.draw(|frame| {
|
||||
/// let area = frame.size();
|
||||
/// frame.render_widget(Paragraph::new("Hello World!"), area);
|
||||
/// frame.set_cursor(0, 0);
|
||||
/// })?;
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
pub fn draw<F>(&mut self, f: F) -> io::Result<CompletedFrame>
|
||||
where
|
||||
F: FnOnce(&mut Frame),
|
||||
{
|
||||
// Autoresize - otherwise we get glitches if shrinking or potential desync between widgets
|
||||
// and the terminal (if growing), which may OOB.
|
||||
self.autoresize()?;
|
||||
|
||||
let mut frame = self.get_frame();
|
||||
f(&mut frame);
|
||||
// We can't change the cursor position right away because we have to flush the frame to
|
||||
// stdout first. But we also can't keep the frame around, since it holds a &mut to
|
||||
// Buffer. Thus, we're taking the important data out of the Frame and dropping it.
|
||||
let cursor_position = frame.cursor_position;
|
||||
|
||||
// Draw to stdout
|
||||
self.flush()?;
|
||||
|
||||
match cursor_position {
|
||||
None => self.hide_cursor()?,
|
||||
Some((x, y)) => {
|
||||
self.show_cursor()?;
|
||||
self.set_cursor(x, y)?;
|
||||
}
|
||||
}
|
||||
|
||||
self.swap_buffers();
|
||||
|
||||
// Flush
|
||||
self.backend.flush()?;
|
||||
|
||||
Ok(CompletedFrame {
|
||||
buffer: &self.buffers[1 - self.current],
|
||||
area: self.last_known_size,
|
||||
})
|
||||
}
|
||||
|
||||
/// Hides the cursor.
|
||||
pub fn hide_cursor(&mut self) -> io::Result<()> {
|
||||
self.backend.hide_cursor()?;
|
||||
self.hidden_cursor = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Shows the cursor.
|
||||
pub fn show_cursor(&mut self) -> io::Result<()> {
|
||||
self.backend.show_cursor()?;
|
||||
self.hidden_cursor = false;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gets the current cursor position.
|
||||
///
|
||||
/// This is the position of the cursor after the last draw call and is returned as a tuple of
|
||||
/// `(x, y)` coordinates.
|
||||
pub fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
|
||||
self.backend.get_cursor()
|
||||
}
|
||||
|
||||
/// Sets the cursor position.
|
||||
pub fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
||||
self.backend.set_cursor(x, y)?;
|
||||
self.last_known_cursor_pos = (x, y);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Clear the terminal and force a full redraw on the next draw call.
|
||||
pub fn clear(&mut self) -> io::Result<()> {
|
||||
match self.viewport {
|
||||
Viewport::Fullscreen => self.backend.clear_region(ClearType::All)?,
|
||||
Viewport::Inline(_) => {
|
||||
self.backend
|
||||
.set_cursor(self.viewport_area.left(), self.viewport_area.top())?;
|
||||
self.backend.clear_region(ClearType::AfterCursor)?;
|
||||
}
|
||||
Viewport::Fixed(area) => {
|
||||
for row in area.top()..area.bottom() {
|
||||
self.backend.set_cursor(0, row)?;
|
||||
self.backend.clear_region(ClearType::AfterCursor)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Reset the back buffer to make sure the next update will redraw everything.
|
||||
self.buffers[1 - self.current].reset();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Clears the inactive buffer and swaps it with the current buffer
|
||||
pub fn swap_buffers(&mut self) {
|
||||
self.buffers[1 - self.current].reset();
|
||||
self.current = 1 - self.current;
|
||||
}
|
||||
|
||||
/// Queries the real size of the backend.
|
||||
pub fn size(&self) -> io::Result<Rect> {
|
||||
self.backend.size()
|
||||
}
|
||||
|
||||
/// Insert some content before the current inline viewport. This has no effect when the
|
||||
/// viewport is fullscreen.
|
||||
///
|
||||
/// This function scrolls down the current viewport by the given height. The newly freed space
|
||||
/// is then made available to the `draw_fn` closure through a writable `Buffer`.
|
||||
///
|
||||
/// Before:
|
||||
/// ```ignore
|
||||
/// +-------------------+
|
||||
/// | |
|
||||
/// | viewport |
|
||||
/// | |
|
||||
/// +-------------------+
|
||||
/// ```
|
||||
///
|
||||
/// After:
|
||||
/// ```ignore
|
||||
/// +-------------------+
|
||||
/// | buffer |
|
||||
/// +-------------------+
|
||||
/// +-------------------+
|
||||
/// | |
|
||||
/// | viewport |
|
||||
/// | |
|
||||
/// +-------------------+
|
||||
/// ```
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ## Insert a single line before the current viewport
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{backend::TestBackend, prelude::*, widgets::*};
|
||||
/// # let backend = TestBackend::new(10, 10);
|
||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||
/// terminal.insert_before(1, |buf| {
|
||||
/// Paragraph::new(Line::from(vec![
|
||||
/// Span::raw("This line will be added "),
|
||||
/// Span::styled("before", Style::default().fg(Color::Blue)),
|
||||
/// Span::raw(" the current viewport")
|
||||
/// ])).render(buf.area, buf);
|
||||
/// });
|
||||
/// ```
|
||||
pub fn insert_before<F>(&mut self, height: u16, draw_fn: F) -> io::Result<()>
|
||||
where
|
||||
F: FnOnce(&mut Buffer),
|
||||
{
|
||||
if !matches!(self.viewport, Viewport::Inline(_)) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.clear()?;
|
||||
let height = height.min(self.last_known_size.height);
|
||||
self.backend.append_lines(height)?;
|
||||
let missing_lines =
|
||||
height.saturating_sub(self.last_known_size.bottom() - self.viewport_area.top());
|
||||
let area = Rect {
|
||||
x: self.viewport_area.left(),
|
||||
y: self.viewport_area.top().saturating_sub(missing_lines),
|
||||
width: self.viewport_area.width,
|
||||
height,
|
||||
};
|
||||
let mut buffer = Buffer::empty(area);
|
||||
|
||||
draw_fn(&mut buffer);
|
||||
|
||||
let iter = buffer.content.iter().enumerate().map(|(i, c)| {
|
||||
let (x, y) = buffer.pos_of(i);
|
||||
(x, y, c)
|
||||
});
|
||||
self.backend.draw(iter)?;
|
||||
self.backend.flush()?;
|
||||
|
||||
let remaining_lines = self.last_known_size.height - area.bottom();
|
||||
let missing_lines = self.viewport_area.height.saturating_sub(remaining_lines);
|
||||
self.backend.append_lines(self.viewport_area.height)?;
|
||||
|
||||
self.set_viewport_area(Rect {
|
||||
x: area.left(),
|
||||
y: area.bottom().saturating_sub(missing_lines),
|
||||
width: area.width,
|
||||
height: self.viewport_area.height,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_inline_size<B: Backend>(
|
||||
backend: &mut B,
|
||||
height: u16,
|
||||
size: Rect,
|
||||
offset_in_previous_viewport: u16,
|
||||
) -> io::Result<(Rect, (u16, u16))> {
|
||||
let pos = backend.get_cursor()?;
|
||||
let mut row = pos.1;
|
||||
|
||||
let max_height = size.height.min(height);
|
||||
|
||||
let lines_after_cursor = height
|
||||
.saturating_sub(offset_in_previous_viewport)
|
||||
.saturating_sub(1);
|
||||
|
||||
backend.append_lines(lines_after_cursor)?;
|
||||
|
||||
let available_lines = size.height.saturating_sub(row).saturating_sub(1);
|
||||
let missing_lines = lines_after_cursor.saturating_sub(available_lines);
|
||||
if missing_lines > 0 {
|
||||
row = row.saturating_sub(missing_lines);
|
||||
}
|
||||
row = row.saturating_sub(offset_in_previous_viewport);
|
||||
|
||||
Ok((
|
||||
Rect {
|
||||
x: 0,
|
||||
y: row,
|
||||
width: size.width,
|
||||
height: max_height,
|
||||
},
|
||||
pos,
|
||||
))
|
||||
}
|
||||
|
||||
/// A consistent view into the terminal state for rendering a single frame.
|
||||
///
|
||||
/// This is obtained via the closure argument of [`Terminal::draw`]. It is used to render widgets
|
||||
/// to the terminal and control the cursor position.
|
||||
///
|
||||
/// The changes drawn to the frame are applied only to the current [`Buffer`].
|
||||
/// After the closure returns, the current buffer is compared to the previous
|
||||
/// buffer and only the changes are applied to the terminal.
|
||||
///
|
||||
/// [`Buffer`]: crate::buffer::Buffer
|
||||
#[derive(Debug, Hash)]
|
||||
pub struct Frame<'a> {
|
||||
/// Where should the cursor be after drawing this frame?
|
||||
///
|
||||
/// If `None`, the cursor is hidden and its position is controlled by the backend. If `Some((x,
|
||||
/// y))`, the cursor is shown and placed at `(x, y)` after the call to `Terminal::draw()`.
|
||||
cursor_position: Option<(u16, u16)>,
|
||||
/// The area of the viewport
|
||||
viewport_area: Rect,
|
||||
|
||||
/// The buffer that is used to draw the current frame
|
||||
buffer: &'a mut Buffer,
|
||||
}
|
||||
|
||||
impl Frame<'_> {
|
||||
/// The size of the current frame
|
||||
///
|
||||
/// This is guaranteed not to change when rendering.
|
||||
pub fn size(&self) -> Rect {
|
||||
self.viewport_area
|
||||
}
|
||||
|
||||
/// Render a [`Widget`] to the current buffer using [`Widget::render`].
|
||||
///
|
||||
/// Usually the area argument is the size of the current frame or a sub-area of the current
|
||||
/// frame (which can be obtained using [`Layout`] to split the total area).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{backend::TestBackend, prelude::*, widgets::Block};
|
||||
/// # let backend = TestBackend::new(5, 5);
|
||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||
/// # let mut frame = terminal.get_frame();
|
||||
/// let block = Block::default();
|
||||
/// let area = Rect::new(0, 0, 5, 5);
|
||||
/// frame.render_widget(block, area);
|
||||
/// ```
|
||||
///
|
||||
/// [`Layout`]: crate::layout::Layout
|
||||
pub fn render_widget<W>(&mut self, widget: W, area: Rect)
|
||||
where
|
||||
W: Widget,
|
||||
{
|
||||
widget.render(area, self.buffer);
|
||||
}
|
||||
|
||||
/// Render a [`StatefulWidget`] to the current buffer using [`StatefulWidget::render`].
|
||||
///
|
||||
/// Usually the area argument is the size of the current frame or a sub-area of the current
|
||||
/// frame (which can be obtained using [`Layout`] to split the total area).
|
||||
///
|
||||
/// The last argument should be an instance of the [`StatefulWidget::State`] associated to the
|
||||
/// given [`StatefulWidget`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{backend::TestBackend, prelude::*, widgets::*};
|
||||
/// # let backend = TestBackend::new(5, 5);
|
||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||
/// # let mut frame = terminal.get_frame();
|
||||
/// let mut state = ListState::default().with_selected(Some(1));
|
||||
/// let list = List::new(vec![
|
||||
/// ListItem::new("Item 1"),
|
||||
/// ListItem::new("Item 2"),
|
||||
/// ]);
|
||||
/// let area = Rect::new(0, 0, 5, 5);
|
||||
/// frame.render_stateful_widget(list, area, &mut state);
|
||||
/// ```
|
||||
///
|
||||
/// [`Layout`]: crate::layout::Layout
|
||||
pub fn render_stateful_widget<W>(&mut self, widget: W, area: Rect, state: &mut W::State)
|
||||
where
|
||||
W: StatefulWidget,
|
||||
{
|
||||
widget.render(area, self.buffer, state);
|
||||
}
|
||||
|
||||
/// After drawing this frame, make the cursor visible and put it at the specified (x, y)
|
||||
/// coordinates. If this method is not called, the cursor will be hidden.
|
||||
///
|
||||
/// Note that this will interfere with calls to `Terminal::hide_cursor()`,
|
||||
/// `Terminal::show_cursor()`, and `Terminal::set_cursor()`. Pick one of the APIs and stick
|
||||
/// with it.
|
||||
pub fn set_cursor(&mut self, x: u16, y: u16) {
|
||||
self.cursor_position = Some((x, y));
|
||||
}
|
||||
|
||||
/// Gets the buffer that this `Frame` draws into as a mutable reference.
|
||||
pub fn buffer_mut(&mut self) -> &mut Buffer {
|
||||
self.buffer
|
||||
}
|
||||
}
|
||||
|
||||
/// `CompletedFrame` represents the state of the terminal after all changes performed in the last
|
||||
/// [`Terminal::draw`] call have been applied. Therefore, it is only valid until the next call to
|
||||
/// [`Terminal::draw`].
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct CompletedFrame<'a> {
|
||||
/// The buffer that was used to draw the last frame.
|
||||
pub buffer: &'a Buffer,
|
||||
/// The size of the last frame.
|
||||
pub area: Rect,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn viewport_to_string() {
|
||||
assert_eq!(Viewport::Fullscreen.to_string(), "Fullscreen");
|
||||
assert_eq!(Viewport::Inline(5).to_string(), "Inline(5)");
|
||||
assert_eq!(
|
||||
Viewport::Fixed(Rect::new(0, 0, 5, 5)).to_string(),
|
||||
"Fixed(5x5+0+0)"
|
||||
);
|
||||
}
|
||||
}
|
||||
pub use frame::{CompletedFrame, Frame};
|
||||
pub use terminal::{Options as TerminalOptions, Terminal};
|
||||
pub use viewport::Viewport;
|
||||
|
||||
155
src/terminal/frame.rs
Normal file
155
src/terminal/frame.rs
Normal file
@@ -0,0 +1,155 @@
|
||||
use crate::{
|
||||
prelude::*,
|
||||
widgets::{StatefulWidget, Widget},
|
||||
};
|
||||
|
||||
/// A consistent view into the terminal state for rendering a single frame.
|
||||
///
|
||||
/// This is obtained via the closure argument of [`Terminal::draw`]. It is used to render widgets
|
||||
/// to the terminal and control the cursor position.
|
||||
///
|
||||
/// The changes drawn to the frame are applied only to the current [`Buffer`].
|
||||
/// After the closure returns, the current buffer is compared to the previous
|
||||
/// buffer and only the changes are applied to the terminal.
|
||||
///
|
||||
/// [`Buffer`]: crate::buffer::Buffer
|
||||
#[derive(Debug, Hash)]
|
||||
pub struct Frame<'a> {
|
||||
/// Where should the cursor be after drawing this frame?
|
||||
///
|
||||
/// If `None`, the cursor is hidden and its position is controlled by the backend. If `Some((x,
|
||||
/// y))`, the cursor is shown and placed at `(x, y)` after the call to `Terminal::draw()`.
|
||||
pub(crate) cursor_position: Option<(u16, u16)>,
|
||||
|
||||
/// The area of the viewport
|
||||
pub(crate) viewport_area: Rect,
|
||||
|
||||
/// The buffer that is used to draw the current frame
|
||||
pub(crate) buffer: &'a mut Buffer,
|
||||
|
||||
/// The frame count indicating the sequence number of this frame.
|
||||
pub(crate) count: usize,
|
||||
}
|
||||
|
||||
/// `CompletedFrame` represents the state of the terminal after all changes performed in the last
|
||||
/// [`Terminal::draw`] call have been applied. Therefore, it is only valid until the next call to
|
||||
/// [`Terminal::draw`].
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct CompletedFrame<'a> {
|
||||
/// The buffer that was used to draw the last frame.
|
||||
pub buffer: &'a Buffer,
|
||||
/// The size of the last frame.
|
||||
pub area: Rect,
|
||||
/// The frame count indicating the sequence number of this frame.
|
||||
pub count: usize,
|
||||
}
|
||||
|
||||
impl Frame<'_> {
|
||||
/// The size of the current frame
|
||||
///
|
||||
/// This is guaranteed not to change during rendering, so may be called multiple times.
|
||||
///
|
||||
/// If your app listens for a resize event from the backend, it should ignore the values from
|
||||
/// the event for any calculations that are used to render the current frame and use this value
|
||||
/// instead as this is the size of the buffer that is used to render the current frame.
|
||||
pub fn size(&self) -> Rect {
|
||||
self.viewport_area
|
||||
}
|
||||
|
||||
/// Render a [`Widget`] to the current buffer using [`Widget::render`].
|
||||
///
|
||||
/// Usually the area argument is the size of the current frame or a sub-area of the current
|
||||
/// frame (which can be obtained using [`Layout`] to split the total area).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{backend::TestBackend, prelude::*, widgets::Block};
|
||||
/// # let backend = TestBackend::new(5, 5);
|
||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||
/// # let mut frame = terminal.get_frame();
|
||||
/// let block = Block::default();
|
||||
/// let area = Rect::new(0, 0, 5, 5);
|
||||
/// frame.render_widget(block, area);
|
||||
/// ```
|
||||
///
|
||||
/// [`Layout`]: crate::layout::Layout
|
||||
pub fn render_widget<W>(&mut self, widget: W, area: Rect)
|
||||
where
|
||||
W: Widget,
|
||||
{
|
||||
widget.render(area, self.buffer);
|
||||
}
|
||||
|
||||
/// Render a [`StatefulWidget`] to the current buffer using [`StatefulWidget::render`].
|
||||
///
|
||||
/// Usually the area argument is the size of the current frame or a sub-area of the current
|
||||
/// frame (which can be obtained using [`Layout`] to split the total area).
|
||||
///
|
||||
/// The last argument should be an instance of the [`StatefulWidget::State`] associated to the
|
||||
/// given [`StatefulWidget`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{backend::TestBackend, prelude::*, widgets::*};
|
||||
/// # let backend = TestBackend::new(5, 5);
|
||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||
/// # let mut frame = terminal.get_frame();
|
||||
/// let mut state = ListState::default().with_selected(Some(1));
|
||||
/// let list = List::new(vec![ListItem::new("Item 1"), ListItem::new("Item 2")]);
|
||||
/// let area = Rect::new(0, 0, 5, 5);
|
||||
/// frame.render_stateful_widget(list, area, &mut state);
|
||||
/// ```
|
||||
///
|
||||
/// [`Layout`]: crate::layout::Layout
|
||||
pub fn render_stateful_widget<W>(&mut self, widget: W, area: Rect, state: &mut W::State)
|
||||
where
|
||||
W: StatefulWidget,
|
||||
{
|
||||
widget.render(area, self.buffer, state);
|
||||
}
|
||||
|
||||
/// After drawing this frame, make the cursor visible and put it at the specified (x, y)
|
||||
/// coordinates. If this method is not called, the cursor will be hidden.
|
||||
///
|
||||
/// Note that this will interfere with calls to `Terminal::hide_cursor()`,
|
||||
/// `Terminal::show_cursor()`, and `Terminal::set_cursor()`. Pick one of the APIs and stick
|
||||
/// with it.
|
||||
pub fn set_cursor(&mut self, x: u16, y: u16) {
|
||||
self.cursor_position = Some((x, y));
|
||||
}
|
||||
|
||||
/// Gets the buffer that this `Frame` draws into as a mutable reference.
|
||||
pub fn buffer_mut(&mut self) -> &mut Buffer {
|
||||
self.buffer
|
||||
}
|
||||
|
||||
/// Returns the current frame count.
|
||||
///
|
||||
/// This method provides access to the frame count, which is a sequence number indicating
|
||||
/// how many frames have been rendered up to (but not including) this one. It can be used
|
||||
/// for purposes such as animation, performance tracking, or debugging.
|
||||
///
|
||||
/// Each time a frame has been rendered, this count is incremented,
|
||||
/// providing a consistent way to reference the order and number of frames processed by the
|
||||
/// terminal. When count reaches its maximum value (usize::MAX), it wraps around to zero.
|
||||
///
|
||||
/// This count is particularly useful when dealing with dynamic content or animations where the
|
||||
/// state of the display changes over time. By tracking the frame count, developers can
|
||||
/// synchronize updates or changes to the content with the rendering process.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{backend::TestBackend, prelude::*, widgets::*};
|
||||
/// # let backend = TestBackend::new(5, 5);
|
||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||
/// # let mut frame = terminal.get_frame();
|
||||
/// let current_count = frame.count();
|
||||
/// println!("Current frame count: {}", current_count);
|
||||
/// ```
|
||||
pub fn count(&self) -> usize {
|
||||
self.count
|
||||
}
|
||||
}
|
||||
494
src/terminal/terminal.rs
Normal file
494
src/terminal/terminal.rs
Normal file
@@ -0,0 +1,494 @@
|
||||
use std::io;
|
||||
|
||||
use crate::{backend::ClearType, prelude::*};
|
||||
|
||||
/// An interface to interact and draw [`Frame`]s on the user's terminal.
|
||||
///
|
||||
/// This is the main entry point for Ratatui. It is responsible for drawing and maintaining the
|
||||
/// state of the buffers, cursor and viewport.
|
||||
///
|
||||
/// The [`Terminal`] is generic over a [`Backend`] implementation which is used to interface with
|
||||
/// the underlying terminal library. The [`Backend`] trait is implemented for three popular Rust
|
||||
/// terminal libraries: [Crossterm], [Termion] and [Termwiz]. See the [`backend`] module for more
|
||||
/// information.
|
||||
///
|
||||
/// The `Terminal` struct maintains two buffers: the current and the previous.
|
||||
/// When the widgets are drawn, the changes are accumulated in the current buffer.
|
||||
/// At the end of each draw pass, the two buffers are compared, and only the changes
|
||||
/// between these buffers are written to the terminal, avoiding any redundant operations.
|
||||
/// After flushing these changes, the buffers are swapped to prepare for the next draw cycle./
|
||||
///
|
||||
/// The terminal also has a viewport which is the area of the terminal that is currently visible to
|
||||
/// the user. It can be either fullscreen, inline or fixed. See [`Viewport`] for more information.
|
||||
///
|
||||
/// Applications should detect terminal resizes and call [`Terminal::draw`] to redraw the
|
||||
/// application with the new size. This will automatically resize the internal buffers to match the
|
||||
/// new size for inline and fullscreen viewports. Fixed viewports are not resized automatically.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use std::io::stdout;
|
||||
///
|
||||
/// use ratatui::{prelude::*, widgets::Paragraph};
|
||||
///
|
||||
/// let backend = CrosstermBackend::new(stdout());
|
||||
/// let mut terminal = Terminal::new(backend)?;
|
||||
/// terminal.draw(|frame| {
|
||||
/// let area = frame.size();
|
||||
/// frame.render_widget(Paragraph::new("Hello World!"), area);
|
||||
/// frame.set_cursor(0, 0);
|
||||
/// })?;
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
///
|
||||
/// [Crossterm]: https://crates.io/crates/crossterm
|
||||
/// [Termion]: https://crates.io/crates/termion
|
||||
/// [Termwiz]: https://crates.io/crates/termwiz
|
||||
/// [`backend`]: crate::backend
|
||||
/// [`Backend`]: crate::backend::Backend
|
||||
/// [`Buffer`]: crate::buffer::Buffer
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Terminal<B>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
/// The backend used to interface with the terminal
|
||||
backend: B,
|
||||
/// Holds the results of the current and previous draw calls. The two are compared at the end
|
||||
/// of each draw pass to output the necessary updates to the terminal
|
||||
buffers: [Buffer; 2],
|
||||
/// Index of the current buffer in the previous array
|
||||
current: usize,
|
||||
/// Whether the cursor is currently hidden
|
||||
hidden_cursor: bool,
|
||||
/// Viewport
|
||||
viewport: Viewport,
|
||||
/// Area of the viewport
|
||||
viewport_area: Rect,
|
||||
/// Last known size of the terminal. Used to detect if the internal buffers have to be resized.
|
||||
last_known_size: Rect,
|
||||
/// Last known position of the cursor. Used to find the new area when the viewport is inlined
|
||||
/// and the terminal resized.
|
||||
last_known_cursor_pos: (u16, u16),
|
||||
/// Number of frames rendered up until current time.
|
||||
frame_count: usize,
|
||||
}
|
||||
|
||||
/// Options to pass to [`Terminal::with_options`]
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Options {
|
||||
/// Viewport used to draw to the terminal
|
||||
pub viewport: Viewport,
|
||||
}
|
||||
|
||||
impl<B> Drop for Terminal<B>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
fn drop(&mut self) {
|
||||
// Attempt to restore the cursor state
|
||||
if self.hidden_cursor {
|
||||
if let Err(err) = self.show_cursor() {
|
||||
eprintln!("Failed to show the cursor: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> Terminal<B>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
/// Creates a new [`Terminal`] with the given [`Backend`] with a full screen viewport.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use std::io::stdout;
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let backend = CrosstermBackend::new(stdout());
|
||||
/// let terminal = Terminal::new(backend)?;
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
pub fn new(backend: B) -> io::Result<Terminal<B>> {
|
||||
Terminal::with_options(
|
||||
backend,
|
||||
TerminalOptions {
|
||||
viewport: Viewport::Fullscreen,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a new [`Terminal`] with the given [`Backend`] and [`TerminalOptions`].
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use std::io::stdout;
|
||||
/// # use ratatui::{prelude::*, backend::TestBackend};
|
||||
/// let backend = CrosstermBackend::new(stdout());
|
||||
/// let viewport = Viewport::Fixed(Rect::new(0, 0, 10, 10));
|
||||
/// let terminal = Terminal::with_options(backend, TerminalOptions { viewport })?;
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
pub fn with_options(mut backend: B, options: TerminalOptions) -> io::Result<Terminal<B>> {
|
||||
let size = match options.viewport {
|
||||
Viewport::Fullscreen | Viewport::Inline(_) => backend.size()?,
|
||||
Viewport::Fixed(area) => area,
|
||||
};
|
||||
let (viewport_area, cursor_pos) = match options.viewport {
|
||||
Viewport::Fullscreen => (size, (0, 0)),
|
||||
Viewport::Inline(height) => compute_inline_size(&mut backend, height, size, 0)?,
|
||||
Viewport::Fixed(area) => (area, (area.left(), area.top())),
|
||||
};
|
||||
Ok(Terminal {
|
||||
backend,
|
||||
buffers: [Buffer::empty(viewport_area), Buffer::empty(viewport_area)],
|
||||
current: 0,
|
||||
hidden_cursor: false,
|
||||
viewport: options.viewport,
|
||||
viewport_area,
|
||||
last_known_size: size,
|
||||
last_known_cursor_pos: cursor_pos,
|
||||
frame_count: 0,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get a Frame object which provides a consistent view into the terminal state for rendering.
|
||||
pub fn get_frame(&mut self) -> Frame {
|
||||
let count = self.frame_count;
|
||||
Frame {
|
||||
cursor_position: None,
|
||||
viewport_area: self.viewport_area,
|
||||
buffer: self.current_buffer_mut(),
|
||||
count,
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the current buffer as a mutable reference.
|
||||
pub fn current_buffer_mut(&mut self) -> &mut Buffer {
|
||||
&mut self.buffers[self.current]
|
||||
}
|
||||
|
||||
/// Gets the backend
|
||||
pub fn backend(&self) -> &B {
|
||||
&self.backend
|
||||
}
|
||||
|
||||
/// Gets the backend as a mutable reference
|
||||
pub fn backend_mut(&mut self) -> &mut B {
|
||||
&mut self.backend
|
||||
}
|
||||
|
||||
/// Obtains a difference between the previous and the current buffer and passes it to the
|
||||
/// current backend for drawing.
|
||||
pub fn flush(&mut self) -> io::Result<()> {
|
||||
let previous_buffer = &self.buffers[1 - self.current];
|
||||
let current_buffer = &self.buffers[self.current];
|
||||
let updates = previous_buffer.diff(current_buffer);
|
||||
if let Some((col, row, _)) = updates.last() {
|
||||
self.last_known_cursor_pos = (*col, *row);
|
||||
}
|
||||
self.backend.draw(updates.into_iter())
|
||||
}
|
||||
|
||||
/// Updates the Terminal so that internal buffers match the requested size.
|
||||
///
|
||||
/// Requested size will be saved so the size can remain consistent when rendering. This leads
|
||||
/// to a full clear of the screen.
|
||||
pub fn resize(&mut self, size: Rect) -> io::Result<()> {
|
||||
let next_area = match self.viewport {
|
||||
Viewport::Fullscreen => size,
|
||||
Viewport::Inline(height) => {
|
||||
let offset_in_previous_viewport = self
|
||||
.last_known_cursor_pos
|
||||
.1
|
||||
.saturating_sub(self.viewport_area.top());
|
||||
compute_inline_size(&mut self.backend, height, size, offset_in_previous_viewport)?.0
|
||||
}
|
||||
Viewport::Fixed(area) => area,
|
||||
};
|
||||
self.set_viewport_area(next_area);
|
||||
self.clear()?;
|
||||
|
||||
self.last_known_size = size;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_viewport_area(&mut self, area: Rect) {
|
||||
self.buffers[self.current].resize(area);
|
||||
self.buffers[1 - self.current].resize(area);
|
||||
self.viewport_area = area;
|
||||
}
|
||||
|
||||
/// Queries the backend for size and resizes if it doesn't match the previous size.
|
||||
pub fn autoresize(&mut self) -> io::Result<()> {
|
||||
// fixed viewports do not get autoresized
|
||||
if matches!(self.viewport, Viewport::Fullscreen | Viewport::Inline(_)) {
|
||||
let size = self.size()?;
|
||||
if size != self.last_known_size {
|
||||
self.resize(size)?;
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Synchronizes terminal size, calls the rendering closure, flushes the current internal state
|
||||
/// and prepares for the next draw call.
|
||||
///
|
||||
/// This is the main entry point for drawing to the terminal.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use std::io::stdout;
|
||||
/// # use ratatui::{prelude::*, widgets::Paragraph};
|
||||
/// let backend = CrosstermBackend::new(stdout());
|
||||
/// let mut terminal = Terminal::new(backend)?;
|
||||
/// terminal.draw(|frame| {
|
||||
/// let area = frame.size();
|
||||
/// frame.render_widget(Paragraph::new("Hello World!"), area);
|
||||
/// frame.set_cursor(0, 0);
|
||||
/// })?;
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
pub fn draw<F>(&mut self, f: F) -> io::Result<CompletedFrame>
|
||||
where
|
||||
F: FnOnce(&mut Frame),
|
||||
{
|
||||
// Autoresize - otherwise we get glitches if shrinking or potential desync between widgets
|
||||
// and the terminal (if growing), which may OOB.
|
||||
self.autoresize()?;
|
||||
|
||||
let mut frame = self.get_frame();
|
||||
f(&mut frame);
|
||||
// We can't change the cursor position right away because we have to flush the frame to
|
||||
// stdout first. But we also can't keep the frame around, since it holds a &mut to
|
||||
// Buffer. Thus, we're taking the important data out of the Frame and dropping it.
|
||||
let cursor_position = frame.cursor_position;
|
||||
|
||||
// Draw to stdout
|
||||
self.flush()?;
|
||||
|
||||
match cursor_position {
|
||||
None => self.hide_cursor()?,
|
||||
Some((x, y)) => {
|
||||
self.show_cursor()?;
|
||||
self.set_cursor(x, y)?;
|
||||
}
|
||||
}
|
||||
|
||||
self.swap_buffers();
|
||||
|
||||
// Flush
|
||||
self.backend.flush()?;
|
||||
|
||||
let completed_frame = CompletedFrame {
|
||||
buffer: &self.buffers[1 - self.current],
|
||||
area: self.last_known_size,
|
||||
count: self.frame_count,
|
||||
};
|
||||
|
||||
// increment frame count before returning from draw
|
||||
self.frame_count = self.frame_count.wrapping_add(1);
|
||||
|
||||
Ok(completed_frame)
|
||||
}
|
||||
|
||||
/// Hides the cursor.
|
||||
pub fn hide_cursor(&mut self) -> io::Result<()> {
|
||||
self.backend.hide_cursor()?;
|
||||
self.hidden_cursor = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Shows the cursor.
|
||||
pub fn show_cursor(&mut self) -> io::Result<()> {
|
||||
self.backend.show_cursor()?;
|
||||
self.hidden_cursor = false;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gets the current cursor position.
|
||||
///
|
||||
/// This is the position of the cursor after the last draw call and is returned as a tuple of
|
||||
/// `(x, y)` coordinates.
|
||||
pub fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
|
||||
self.backend.get_cursor()
|
||||
}
|
||||
|
||||
/// Sets the cursor position.
|
||||
pub fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
||||
self.backend.set_cursor(x, y)?;
|
||||
self.last_known_cursor_pos = (x, y);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Clear the terminal and force a full redraw on the next draw call.
|
||||
pub fn clear(&mut self) -> io::Result<()> {
|
||||
match self.viewport {
|
||||
Viewport::Fullscreen => self.backend.clear_region(ClearType::All)?,
|
||||
Viewport::Inline(_) => {
|
||||
self.backend
|
||||
.set_cursor(self.viewport_area.left(), self.viewport_area.top())?;
|
||||
self.backend.clear_region(ClearType::AfterCursor)?;
|
||||
}
|
||||
Viewport::Fixed(area) => {
|
||||
for row in area.top()..area.bottom() {
|
||||
self.backend.set_cursor(0, row)?;
|
||||
self.backend.clear_region(ClearType::AfterCursor)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Reset the back buffer to make sure the next update will redraw everything.
|
||||
self.buffers[1 - self.current].reset();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Clears the inactive buffer and swaps it with the current buffer
|
||||
pub fn swap_buffers(&mut self) {
|
||||
self.buffers[1 - self.current].reset();
|
||||
self.current = 1 - self.current;
|
||||
}
|
||||
|
||||
/// Queries the real size of the backend.
|
||||
pub fn size(&self) -> io::Result<Rect> {
|
||||
self.backend.size()
|
||||
}
|
||||
|
||||
/// Insert some content before the current inline viewport. This has no effect when the
|
||||
/// viewport is fullscreen.
|
||||
///
|
||||
/// This function scrolls down the current viewport by the given height. The newly freed space
|
||||
/// is then made available to the `draw_fn` closure through a writable `Buffer`.
|
||||
///
|
||||
/// Before:
|
||||
/// ```ignore
|
||||
/// +-------------------+
|
||||
/// | |
|
||||
/// | viewport |
|
||||
/// | |
|
||||
/// +-------------------+
|
||||
/// ```
|
||||
///
|
||||
/// After:
|
||||
/// ```ignore
|
||||
/// +-------------------+
|
||||
/// | buffer |
|
||||
/// +-------------------+
|
||||
/// +-------------------+
|
||||
/// | |
|
||||
/// | viewport |
|
||||
/// | |
|
||||
/// +-------------------+
|
||||
/// ```
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ## Insert a single line before the current viewport
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{backend::TestBackend, prelude::*, widgets::*};
|
||||
/// # let backend = TestBackend::new(10, 10);
|
||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||
/// terminal.insert_before(1, |buf| {
|
||||
/// Paragraph::new(Line::from(vec![
|
||||
/// Span::raw("This line will be added "),
|
||||
/// Span::styled("before", Style::default().fg(Color::Blue)),
|
||||
/// Span::raw(" the current viewport"),
|
||||
/// ]))
|
||||
/// .render(buf.area, buf);
|
||||
/// });
|
||||
/// ```
|
||||
pub fn insert_before<F>(&mut self, height: u16, draw_fn: F) -> io::Result<()>
|
||||
where
|
||||
F: FnOnce(&mut Buffer),
|
||||
{
|
||||
if !matches!(self.viewport, Viewport::Inline(_)) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Clear the viewport off the screen
|
||||
self.clear()?;
|
||||
|
||||
// Move the viewport by height, but don't move it past the bottom of the terminal
|
||||
let viewport_at_bottom = self.last_known_size.bottom() - self.viewport_area.height;
|
||||
self.set_viewport_area(Rect {
|
||||
y: self
|
||||
.viewport_area
|
||||
.y
|
||||
.saturating_add(height)
|
||||
.min(viewport_at_bottom),
|
||||
..self.viewport_area
|
||||
});
|
||||
|
||||
// Draw contents into buffer
|
||||
let area = Rect {
|
||||
x: self.viewport_area.left(),
|
||||
y: 0,
|
||||
width: self.viewport_area.width,
|
||||
height,
|
||||
};
|
||||
let mut buffer = Buffer::empty(area);
|
||||
draw_fn(&mut buffer);
|
||||
|
||||
// Split buffer into screen-sized chunks and draw
|
||||
let max_chunk_size = (self.viewport_area.top() * area.width).into();
|
||||
for buffer_content_chunk in buffer.content.chunks(max_chunk_size) {
|
||||
let chunk_size = buffer_content_chunk.len() as u16 / area.width;
|
||||
|
||||
self.backend
|
||||
.append_lines(self.viewport_area.height.saturating_sub(1) + chunk_size)?;
|
||||
|
||||
let iter = buffer_content_chunk.iter().enumerate().map(|(i, c)| {
|
||||
let (x, y) = buffer.pos_of(i);
|
||||
(
|
||||
x,
|
||||
self.viewport_area.top().saturating_sub(chunk_size) + y,
|
||||
c,
|
||||
)
|
||||
});
|
||||
self.backend.draw(iter)?;
|
||||
self.backend.flush()?;
|
||||
self.set_cursor(self.viewport_area.left(), self.viewport_area.top())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_inline_size<B: Backend>(
|
||||
backend: &mut B,
|
||||
height: u16,
|
||||
size: Rect,
|
||||
offset_in_previous_viewport: u16,
|
||||
) -> io::Result<(Rect, (u16, u16))> {
|
||||
let pos = backend.get_cursor()?;
|
||||
let mut row = pos.1;
|
||||
|
||||
let max_height = size.height.min(height);
|
||||
|
||||
let lines_after_cursor = height
|
||||
.saturating_sub(offset_in_previous_viewport)
|
||||
.saturating_sub(1);
|
||||
|
||||
backend.append_lines(lines_after_cursor)?;
|
||||
|
||||
let available_lines = size.height.saturating_sub(row).saturating_sub(1);
|
||||
let missing_lines = lines_after_cursor.saturating_sub(available_lines);
|
||||
if missing_lines > 0 {
|
||||
row = row.saturating_sub(missing_lines);
|
||||
}
|
||||
row = row.saturating_sub(offset_in_previous_viewport);
|
||||
|
||||
Ok((
|
||||
Rect {
|
||||
x: 0,
|
||||
y: row,
|
||||
width: size.width,
|
||||
height: max_height,
|
||||
},
|
||||
pos,
|
||||
))
|
||||
}
|
||||
54
src/terminal/viewport.rs
Normal file
54
src/terminal/viewport.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
use std::fmt;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
/// Represents the viewport of the terminal. The viewport is the area of the terminal that is
|
||||
/// currently visible to the user. It can be either fullscreen, inline or fixed.
|
||||
///
|
||||
/// When the viewport is fullscreen, the whole terminal is used to draw the application.
|
||||
///
|
||||
/// When the viewport is inline, it is drawn inline with the rest of the terminal. The height of
|
||||
/// the viewport is fixed, but the width is the same as the terminal width.
|
||||
///
|
||||
/// When the viewport is fixed, it is drawn in a fixed area of the terminal. The area is specified
|
||||
/// by a [`Rect`].
|
||||
///
|
||||
/// See [`Terminal::with_options`] for more information.
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum Viewport {
|
||||
/// The viewport is fullscreen
|
||||
#[default]
|
||||
Fullscreen,
|
||||
/// The viewport is inline with the rest of the terminal.
|
||||
///
|
||||
/// The viewport's height is fixed and specified in number of lines. The width is the same as
|
||||
/// the terminal's width. The viewport is drawn below the cursor position.
|
||||
Inline(u16),
|
||||
/// The viewport is drawn in a fixed area of the terminal. The area is specified by a [`Rect`].
|
||||
Fixed(Rect),
|
||||
}
|
||||
|
||||
impl fmt::Display for Viewport {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Viewport::Fullscreen => write!(f, "Fullscreen"),
|
||||
Viewport::Inline(height) => write!(f, "Inline({})", height),
|
||||
Viewport::Fixed(area) => write!(f, "Fixed({})", area),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn viewport_to_string() {
|
||||
assert_eq!(Viewport::Fullscreen.to_string(), "Fullscreen");
|
||||
assert_eq!(Viewport::Inline(5).to_string(), "Inline(5)");
|
||||
assert_eq!(
|
||||
Viewport::Fixed(Rect::new(0, 0, 5, 5)).to_string(),
|
||||
"Fixed(5x5+0+0)"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -31,9 +31,8 @@
|
||||
//! // Converted to Line(vec![
|
||||
//! // Span { content: Cow::Borrowed("My title"), style: Style { fg: Some(Color::Yellow), .. }
|
||||
//! // ])
|
||||
//! let block = Block::default().title(
|
||||
//! Span::styled("My title", Style::default().fg(Color::Yellow))
|
||||
//! );
|
||||
//! let block =
|
||||
//! Block::default().title(Span::styled("My title", Style::default().fg(Color::Yellow)));
|
||||
//!
|
||||
//! // A string with multiple styles.
|
||||
//! // Converted to Line(vec![
|
||||
@@ -46,8 +45,6 @@
|
||||
//! ]);
|
||||
//! ```
|
||||
|
||||
use crate::style::Style;
|
||||
|
||||
mod grapheme;
|
||||
pub use grapheme::StyledGrapheme;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::style::{Style, Styled};
|
||||
use crate::prelude::*;
|
||||
|
||||
/// A grapheme associated to a style.
|
||||
/// Note that, although `StyledGrapheme` is the smallest divisible unit of text,
|
||||
@@ -12,8 +12,15 @@ pub struct StyledGrapheme<'a> {
|
||||
}
|
||||
|
||||
impl<'a> StyledGrapheme<'a> {
|
||||
pub fn new(symbol: &'a str, style: Style) -> StyledGrapheme<'a> {
|
||||
StyledGrapheme { symbol, style }
|
||||
/// Creates a new `StyledGrapheme` with the given symbol and style.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
pub fn new<S: Into<Style>>(symbol: &'a str, style: S) -> StyledGrapheme<'a> {
|
||||
StyledGrapheme {
|
||||
symbol,
|
||||
style: style.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,8 +31,8 @@ impl<'a> Styled for StyledGrapheme<'a> {
|
||||
self.style
|
||||
}
|
||||
|
||||
fn set_style(mut self, style: Style) -> Self::Item {
|
||||
self.style = style;
|
||||
fn set_style<S: Into<Style>>(mut self, style: S) -> Self::Item {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -33,7 +40,6 @@ impl<'a> Styled for StyledGrapheme<'a> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::prelude::*;
|
||||
|
||||
#[test]
|
||||
fn new() {
|
||||
|
||||
554
src/text/line.rs
554
src/text/line.rs
@@ -1,23 +1,106 @@
|
||||
#![deny(missing_docs)]
|
||||
use std::borrow::Cow;
|
||||
|
||||
use super::{Span, Style, StyledGrapheme};
|
||||
use crate::layout::Alignment;
|
||||
use super::StyledGrapheme;
|
||||
use crate::{prelude::*, widgets::Widget};
|
||||
|
||||
/// A line of text, consisting of one or more [`Span`]s.
|
||||
///
|
||||
/// [`Line`]s are used wherever text is displayed in the terminal and represent a single line of
|
||||
/// text. When a [`Line`] is rendered, it is rendered as a single line of text, with each [`Span`]
|
||||
/// being rendered in order (left to right).
|
||||
///
|
||||
/// [`Line`]s can be created from [`Span`]s, [`String`]s, and [`&str`]s. They can be styled with a
|
||||
/// [`Style`], and have an [`Alignment`].
|
||||
///
|
||||
/// The line's [`Alignment`] is used by the rendering widget to determine how to align the line
|
||||
/// within the available space. If the line is longer than the available space, the alignment is
|
||||
/// ignored and the line is truncated.
|
||||
///
|
||||
/// The line's [`Style`] is used by the rendering widget to determine how to style the line. If the
|
||||
/// line is longer than the available space, the style is applied to the entire line, and the line
|
||||
/// is truncated. Each [`Span`] in the line will be styled with the [`Style`] of the line, and then
|
||||
/// with its own [`Style`].
|
||||
///
|
||||
/// `Line` implements the [`Widget`] trait, which means it can be rendered to a [`Buffer`]. Usually
|
||||
/// apps will use the [`Paragraph`] widget instead of rendering a [`Line`] directly as it provides
|
||||
/// more functionality.
|
||||
///
|
||||
/// # Constructor Methods
|
||||
///
|
||||
/// - [`Line::default`] creates a line with empty content and the default style.
|
||||
/// - [`Line::raw`] creates a line with the given content and the default style.
|
||||
/// - [`Line::styled`] creates a line with the given content and style.
|
||||
///
|
||||
/// # Setter Methods
|
||||
///
|
||||
/// These methods are fluent setters. They return a `Line` with the property set.
|
||||
///
|
||||
/// - [`Line::spans`] sets the content of the line.
|
||||
/// - [`Line::style`] sets the style of the line.
|
||||
/// - [`Line::alignment`] sets the alignment of the line.
|
||||
///
|
||||
/// # Other Methods
|
||||
///
|
||||
/// - [`Line::patch_style`] patches the style of the line, adding modifiers from the given style.
|
||||
/// - [`Line::reset_style`] resets the style of the line.
|
||||
/// - [`Line::width`] returns the unicode width of the content held by this line.
|
||||
/// - [`Line::styled_graphemes`] returns an iterator over the graphemes held by this line.
|
||||
///
|
||||
/// # Compatibility Notes
|
||||
///
|
||||
/// Before v0.26.0, [`Line`] did not have a `style` field and instead relied on only the styles that
|
||||
/// were set on each [`Span`] contained in the `spans` field. The [`Line::patch_style`] method was
|
||||
/// the only way to set the overall style for individual lines. For this reason, this field may not
|
||||
/// be supported yet by all widgets (outside of the `ratatui` crate itself).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::prelude::*;
|
||||
///
|
||||
/// Line::raw("unstyled");
|
||||
/// Line::styled("yellow text", Style::new().yellow());
|
||||
/// Line::from("red text").style(Style::new().red());
|
||||
/// Line::from(String::from("unstyled"));
|
||||
/// Line::from(vec![
|
||||
/// Span::styled("Hello", Style::new().blue()),
|
||||
/// Span::raw(" world!"),
|
||||
/// ]);
|
||||
/// ```
|
||||
///
|
||||
/// [`Paragraph`]: crate::widgets::Paragraph
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Line<'a> {
|
||||
/// The spans that make up this line of text.
|
||||
pub spans: Vec<Span<'a>>,
|
||||
|
||||
/// The style of this line of text.
|
||||
pub style: Style,
|
||||
|
||||
/// The alignment of this line of text.
|
||||
pub alignment: Option<Alignment>,
|
||||
}
|
||||
|
||||
impl<'a> Line<'a> {
|
||||
/// Create a line with the default style.
|
||||
///
|
||||
/// `content` can be any type that is convertible to [`Cow<str>`] (e.g. [`&str`], [`String`],
|
||||
/// [`Cow<str>`], or your own type that implements [`Into<Cow<str>>`]).
|
||||
///
|
||||
/// A [`Line`] can specify a [`Style`], which will be applied before the style of each [`Span`]
|
||||
/// in the line.
|
||||
///
|
||||
/// Any newlines in the content are removed.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// # use std::borrow::Cow;
|
||||
/// Line::raw("test content");
|
||||
/// Line::raw(String::from("test content"));
|
||||
/// Line::raw(Cow::from("test content"));
|
||||
/// ```
|
||||
pub fn raw<T>(content: T) -> Line<'a>
|
||||
where
|
||||
@@ -29,38 +112,120 @@ impl<'a> Line<'a> {
|
||||
.lines()
|
||||
.map(|v| Span::raw(v.to_string()))
|
||||
.collect(),
|
||||
alignment: None,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a line with a style.
|
||||
/// Create a line with the given style.
|
||||
// `content` can be any type that is convertible to [`Cow<str>`] (e.g. [`&str`], [`String`],
|
||||
/// [`Cow<str>`], or your own type that implements [`Into<Cow<str>>`]).
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Any newlines in the content are removed.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// # use std::borrow::Cow;
|
||||
/// let style = Style::new().yellow().italic();
|
||||
/// Line::styled("My text", style);
|
||||
/// Line::styled(String::from("My text"), style);
|
||||
/// Line::styled(Cow::from("test content"), style);
|
||||
/// ```
|
||||
pub fn styled<T, S>(content: T, style: S) -> Line<'a>
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
S: Into<Style>,
|
||||
{
|
||||
Line {
|
||||
spans: content
|
||||
.into()
|
||||
.lines()
|
||||
.map(|v| Span::raw(v.to_string()))
|
||||
.collect(),
|
||||
style: style.into(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the spans of this line of text.
|
||||
///
|
||||
/// `spans` accepts any iterator that yields items that are convertible to [`Span`] (e.g.
|
||||
/// [`&str`], [`String`], [`Span`], or your own type that implements [`Into<Span>`]).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
/// Line::styled("My text", style);
|
||||
/// Line::styled(String::from("My text"), style);
|
||||
/// let line = Line::default().spans(vec!["Hello".blue(), " world!".green()]);
|
||||
/// let line = Line::default().spans([1, 2, 3].iter().map(|i| format!("Item {}", i)));
|
||||
/// ```
|
||||
pub fn styled<T>(content: T, style: Style) -> Line<'a>
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn spans<I>(mut self, spans: I) -> Self
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
I: IntoIterator,
|
||||
I::Item: Into<Span<'a>>,
|
||||
{
|
||||
Line::from(Span::styled(content, style))
|
||||
self.spans = spans.into_iter().map(Into::into).collect();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of this line of text.
|
||||
///
|
||||
/// Defaults to [`Style::default()`].
|
||||
///
|
||||
/// Note: This field was added in v0.26.0. Prior to that, the style of a line was determined
|
||||
/// only by the style of each [`Span`] contained in the line. For this reason, this field may
|
||||
/// not be supported by all widgets (outside of the `ratatui` crate itself).
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let mut line = Line::from("foo").style(Style::new().red());
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the target alignment for this line of text.
|
||||
///
|
||||
/// Defaults to: [`None`], meaning the alignment is determined by the rendering widget.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let mut line = Line::from("Hi, what's up?");
|
||||
/// assert_eq!(None, line.alignment);
|
||||
/// assert_eq!(
|
||||
/// Some(Alignment::Right),
|
||||
/// line.alignment(Alignment::Right).alignment
|
||||
/// )
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn alignment(self, alignment: Alignment) -> Self {
|
||||
Self {
|
||||
alignment: Some(alignment),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the width of the underlying string.
|
||||
///
|
||||
/// ## Examples
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let line = Line::from(vec![
|
||||
/// Span::styled("My", Style::default().fg(Color::Yellow)),
|
||||
/// Span::raw(" text"),
|
||||
/// ]);
|
||||
/// assert_eq!(7, line.width());
|
||||
/// let line = Line::from(vec!["Hello".blue(), " world!".green()]);
|
||||
/// assert_eq!(12, line.width());
|
||||
/// ```
|
||||
pub fn width(&self) -> usize {
|
||||
self.spans.iter().map(Span::width).sum()
|
||||
@@ -71,16 +236,21 @@ impl<'a> Line<'a> {
|
||||
/// `base_style` is the [`Style`] that will be patched with each grapheme [`Style`] to get
|
||||
/// the resulting [`Style`].
|
||||
///
|
||||
/// ## Examples
|
||||
/// `base_style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`],
|
||||
/// or your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use std::iter::Iterator;
|
||||
///
|
||||
/// use ratatui::{prelude::*, text::StyledGrapheme};
|
||||
///
|
||||
/// let line = Line::styled("Text", Style::default().fg(Color::Yellow));
|
||||
/// let style = Style::default().fg(Color::Green).bg(Color::Black);
|
||||
/// assert_eq!(
|
||||
/// line.styled_graphemes(style).collect::<Vec<StyledGrapheme>>(),
|
||||
/// line.styled_graphemes(style)
|
||||
/// .collect::<Vec<StyledGrapheme>>(),
|
||||
/// vec![
|
||||
/// StyledGrapheme::new("T", Style::default().fg(Color::Yellow).bg(Color::Black)),
|
||||
/// StyledGrapheme::new("e", Style::default().fg(Color::Yellow).bg(Color::Black)),
|
||||
@@ -89,80 +259,61 @@ impl<'a> Line<'a> {
|
||||
/// ]
|
||||
/// );
|
||||
/// ```
|
||||
pub fn styled_graphemes(
|
||||
pub fn styled_graphemes<S: Into<Style>>(
|
||||
&'a self,
|
||||
base_style: Style,
|
||||
base_style: S,
|
||||
) -> impl Iterator<Item = StyledGrapheme<'a>> {
|
||||
let style = base_style.into().patch(self.style);
|
||||
self.spans
|
||||
.iter()
|
||||
.flat_map(move |span| span.styled_graphemes(base_style))
|
||||
.flat_map(move |span| span.styled_graphemes(style))
|
||||
}
|
||||
|
||||
/// Patches the style of each Span in an existing Line, adding modifiers from the given style.
|
||||
/// Patches the style of this Line, adding modifiers from the given style.
|
||||
///
|
||||
/// ## Examples
|
||||
/// This is useful for when you want to apply a style to a line that already has some styling.
|
||||
/// In contrast to [`Line::style`], this method will not overwrite the existing style, but
|
||||
/// instead will add the given style's modifiers to this Line's style.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
/// let mut raw_line = Line::from(vec![
|
||||
/// Span::raw("My"),
|
||||
/// Span::raw(" text"),
|
||||
/// ]);
|
||||
/// let mut styled_line = Line::from(vec![
|
||||
/// Span::styled("My", style),
|
||||
/// Span::styled(" text", style),
|
||||
/// ]);
|
||||
/// let line = Line::styled("My text", Modifier::ITALIC);
|
||||
///
|
||||
/// assert_ne!(raw_line, styled_line);
|
||||
/// let styled_line = Line::styled("My text", (Color::Yellow, Modifier::ITALIC));
|
||||
///
|
||||
/// raw_line.patch_style(style);
|
||||
/// assert_eq!(raw_line, styled_line);
|
||||
/// assert_eq!(styled_line, line.patch_style(Color::Yellow));
|
||||
/// ```
|
||||
pub fn patch_style(&mut self, style: Style) {
|
||||
for span in &mut self.spans {
|
||||
span.patch_style(style);
|
||||
}
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn patch_style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.style = self.style.patch(style);
|
||||
self
|
||||
}
|
||||
|
||||
/// Resets the style of each Span in the Line.
|
||||
/// Resets the style of this Line.
|
||||
///
|
||||
/// Equivalent to calling `patch_style(Style::reset())`.
|
||||
///
|
||||
/// ## Examples
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let mut line = Line::from(vec![
|
||||
/// Span::styled("My", Style::default().fg(Color::Yellow)),
|
||||
/// Span::styled(" text", Style::default().add_modifier(Modifier::BOLD)),
|
||||
/// ]);
|
||||
/// # let style = Style::default().yellow();
|
||||
/// let line = Line::styled("My text", style);
|
||||
///
|
||||
/// line.reset_style();
|
||||
/// assert_eq!(Style::reset(), line.spans[0].style);
|
||||
/// assert_eq!(Style::reset(), line.spans[1].style);
|
||||
/// assert_eq!(Style::reset(), line.reset_style().style);
|
||||
/// ```
|
||||
pub fn reset_style(&mut self) {
|
||||
for span in &mut self.spans {
|
||||
span.reset_style();
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the target alignment for this line of text.
|
||||
/// Defaults to: [`None`], meaning the alignment is determined by the rendering widget.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let mut line = Line::from("Hi, what's up?");
|
||||
/// assert_eq!(None, line.alignment);
|
||||
/// assert_eq!(Some(Alignment::Right), line.alignment(Alignment::Right).alignment)
|
||||
/// ```
|
||||
pub fn alignment(self, alignment: Alignment) -> Self {
|
||||
Self {
|
||||
alignment: Some(alignment),
|
||||
..self
|
||||
}
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn reset_style(self) -> Self {
|
||||
self.patch_style(Style::reset())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,16 +353,127 @@ impl<'a> From<Line<'a>> for String {
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Line<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let area = area.intersection(buf.area);
|
||||
buf.set_style(area, self.style);
|
||||
let width = self.width() as u16;
|
||||
let offset = match self.alignment {
|
||||
Some(Alignment::Left) => 0,
|
||||
Some(Alignment::Center) => (area.width.saturating_sub(width)) / 2,
|
||||
Some(Alignment::Right) => area.width.saturating_sub(width),
|
||||
None => 0,
|
||||
};
|
||||
let mut x = area.left().saturating_add(offset);
|
||||
for span in self.spans {
|
||||
let span_width = span.width() as u16;
|
||||
let span_area = Rect {
|
||||
x,
|
||||
width: span_width.min(area.right() - x),
|
||||
..area
|
||||
};
|
||||
span.render(span_area, buf);
|
||||
x = x.saturating_add(span_width);
|
||||
if x >= area.right() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Line<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
for span in &self.spans {
|
||||
write!(f, "{span}")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
layout::Alignment,
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Line, Span, StyledGrapheme},
|
||||
};
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_width() {
|
||||
fn raw_str() {
|
||||
let line = Line::raw("test content");
|
||||
assert_eq!(line.spans, vec![Span::raw("test content")]);
|
||||
assert_eq!(line.alignment, None);
|
||||
|
||||
let line = Line::raw("a\nb");
|
||||
assert_eq!(line.spans, vec![Span::raw("a"), Span::raw("b")]);
|
||||
assert_eq!(line.alignment, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn styled_str() {
|
||||
let style = Style::new().yellow();
|
||||
let content = "Hello, world!";
|
||||
let line = Line::styled(content, style);
|
||||
assert_eq!(line.spans, vec![Span::raw(content)]);
|
||||
assert_eq!(line.style, style);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn styled_string() {
|
||||
let style = Style::new().yellow();
|
||||
let content = String::from("Hello, world!");
|
||||
let line = Line::styled(content.clone(), style);
|
||||
assert_eq!(line.spans, vec![Span::raw(content)]);
|
||||
assert_eq!(line.style, style);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn styled_cow() {
|
||||
let style = Style::new().yellow();
|
||||
let content = Cow::from("Hello, world!");
|
||||
let line = Line::styled(content.clone(), style);
|
||||
assert_eq!(line.spans, vec![Span::raw(content)]);
|
||||
assert_eq!(line.style, style);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spans_vec() {
|
||||
let line = Line::default().spans(vec!["Hello".blue(), " world!".green()]);
|
||||
assert_eq!(
|
||||
line.spans,
|
||||
vec![
|
||||
Span::styled("Hello", Style::new().blue()),
|
||||
Span::styled(" world!", Style::new().green()),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spans_iter() {
|
||||
let line = Line::default().spans([1, 2, 3].iter().map(|i| format!("Item {i}")));
|
||||
assert_eq!(
|
||||
line.spans,
|
||||
vec![
|
||||
Span::raw("Item 1"),
|
||||
Span::raw("Item 2"),
|
||||
Span::raw("Item 3"),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn style() {
|
||||
let line = Line::default().style(Style::new().red());
|
||||
assert_eq!(line.style, Style::new().red());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alignment() {
|
||||
let line = Line::from("This is left").alignment(Alignment::Left);
|
||||
assert_eq!(Some(Alignment::Left), line.alignment);
|
||||
|
||||
let line = Line::from("This is default");
|
||||
assert_eq!(None, line.alignment);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn width() {
|
||||
let line = Line::from(vec![
|
||||
Span::styled("My", Style::default().fg(Color::Yellow)),
|
||||
Span::raw(" text"),
|
||||
@@ -223,50 +485,40 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_patch_style() {
|
||||
let style = Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::ITALIC);
|
||||
let mut raw_line = Line::from(vec![Span::raw("My"), Span::raw(" text")]);
|
||||
let styled_line = Line::from(vec![
|
||||
Span::styled("My", style),
|
||||
Span::styled(" text", style),
|
||||
]);
|
||||
fn patch_style() {
|
||||
let raw_line = Line::styled("foobar", Color::Yellow);
|
||||
let styled_line = Line::styled("foobar", (Color::Yellow, Modifier::ITALIC));
|
||||
|
||||
assert_ne!(raw_line, styled_line);
|
||||
|
||||
raw_line.patch_style(style);
|
||||
let raw_line = raw_line.patch_style(Modifier::ITALIC);
|
||||
assert_eq!(raw_line, styled_line);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reset_style() {
|
||||
let mut line = Line::from(vec![
|
||||
Span::styled("My", Style::default().fg(Color::Yellow)),
|
||||
Span::styled(" text", Style::default().add_modifier(Modifier::BOLD)),
|
||||
]);
|
||||
fn reset_style() {
|
||||
let line =
|
||||
Line::styled("foobar", Style::default().yellow().on_red().italic()).reset_style();
|
||||
|
||||
line.reset_style();
|
||||
assert_eq!(Style::reset(), line.spans[0].style);
|
||||
assert_eq!(Style::reset(), line.spans[1].style);
|
||||
assert_eq!(Style::reset(), line.style);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_string() {
|
||||
fn from_string() {
|
||||
let s = String::from("Hello, world!");
|
||||
let line = Line::from(s);
|
||||
assert_eq!(vec![Span::from("Hello, world!")], line.spans);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_str() {
|
||||
fn from_str() {
|
||||
let s = "Hello, world!";
|
||||
let line = Line::from(s);
|
||||
assert_eq!(vec![Span::from("Hello, world!")], line.spans);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_vec() {
|
||||
fn from_vec() {
|
||||
let spans = vec![
|
||||
Span::styled("Hello,", Style::default().fg(Color::Red)),
|
||||
Span::styled(" world!", Style::default().fg(Color::Green)),
|
||||
@@ -276,14 +528,14 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_span() {
|
||||
fn from_span() {
|
||||
let span = Span::styled("Hello, world!", Style::default().fg(Color::Yellow));
|
||||
let line = Line::from(span.clone());
|
||||
assert_eq!(vec![span], line.spans);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_into_string() {
|
||||
fn into_string() {
|
||||
let line = Line::from(vec![
|
||||
Span::styled("Hello,", Style::default().fg(Color::Red)),
|
||||
Span::styled(" world!", Style::default().fg(Color::Green)),
|
||||
@@ -292,15 +544,6 @@ mod tests {
|
||||
assert_eq!("Hello, world!", s);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_alignment() {
|
||||
let line = Line::from("This is left").alignment(Alignment::Left);
|
||||
assert_eq!(Some(Alignment::Left), line.alignment);
|
||||
|
||||
let line = Line::from("This is default");
|
||||
assert_eq!(None, line.alignment);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn styled_graphemes() {
|
||||
const RED: Style = Style::new().fg(Color::Red);
|
||||
@@ -332,13 +575,94 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_str() {
|
||||
let line = Line::raw("test content");
|
||||
assert_eq!(line.spans, vec![Span::raw("test content")]);
|
||||
assert_eq!(line.alignment, None);
|
||||
fn display_line_from_vec() {
|
||||
let line_from_vec = Line::from(vec![Span::raw("Hello,"), Span::raw(" world!")]);
|
||||
|
||||
let line = Line::raw("a\nb");
|
||||
assert_eq!(line.spans, vec![Span::raw("a"), Span::raw("b")]);
|
||||
assert_eq!(line.alignment, None);
|
||||
assert_eq!(format!("{line_from_vec}"), "Hello, world!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_styled_line() {
|
||||
let styled_line = Line::styled("Hello, world!", Style::new().green().italic());
|
||||
|
||||
assert_eq!(format!("{styled_line}"), "Hello, world!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_line_from_styled_span() {
|
||||
let styled_span = Span::styled("Hello, world!", Style::new().green().italic());
|
||||
let line_from_styled_span = Line::from(styled_span);
|
||||
|
||||
assert_eq!(format!("{line_from_styled_span}"), "Hello, world!");
|
||||
}
|
||||
|
||||
mod widget {
|
||||
use super::*;
|
||||
use crate::assert_buffer_eq;
|
||||
const BLUE: Style = Style::new().fg(Color::Blue);
|
||||
const GREEN: Style = Style::new().fg(Color::Green);
|
||||
const ITALIC: Style = Style::new().add_modifier(Modifier::ITALIC);
|
||||
|
||||
fn hello_world() -> Line<'static> {
|
||||
Line::from(vec![
|
||||
Span::styled("Hello ", BLUE),
|
||||
Span::styled("world!", GREEN),
|
||||
])
|
||||
.style(ITALIC)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
|
||||
hello_world().render(Rect::new(0, 0, 15, 1), &mut buf);
|
||||
let mut expected = Buffer::with_lines(vec!["Hello world! "]);
|
||||
expected.set_style(Rect::new(0, 0, 15, 1), ITALIC);
|
||||
expected.set_style(Rect::new(0, 0, 6, 1), BLUE);
|
||||
expected.set_style(Rect::new(6, 0, 6, 1), GREEN);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_only_styles_line_area() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 1));
|
||||
hello_world().render(Rect::new(0, 0, 15, 1), &mut buf);
|
||||
let mut expected = Buffer::with_lines(vec!["Hello world! "]);
|
||||
expected.set_style(Rect::new(0, 0, 15, 1), ITALIC);
|
||||
expected.set_style(Rect::new(0, 0, 6, 1), BLUE);
|
||||
expected.set_style(Rect::new(6, 0, 6, 1), GREEN);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_truncates() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 1));
|
||||
Line::from("Hello world!").render(Rect::new(0, 0, 5, 1), &mut buf);
|
||||
let expected = Buffer::with_lines(vec!["Hello "]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_centered() {
|
||||
let line = hello_world().alignment(Alignment::Center);
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
|
||||
line.render(Rect::new(0, 0, 15, 1), &mut buf);
|
||||
let mut expected = Buffer::with_lines(vec![" Hello world! "]);
|
||||
expected.set_style(Rect::new(0, 0, 15, 1), ITALIC);
|
||||
expected.set_style(Rect::new(1, 0, 6, 1), BLUE);
|
||||
expected.set_style(Rect::new(7, 0, 6, 1), GREEN);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_right_aligned() {
|
||||
let line = hello_world().alignment(Alignment::Right);
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
|
||||
line.render(Rect::new(0, 0, 15, 1), &mut buf);
|
||||
let mut expected = Buffer::with_lines(vec![" Hello world!"]);
|
||||
expected.set_style(Rect::new(0, 0, 15, 1), ITALIC);
|
||||
expected.set_style(Rect::new(3, 0, 6, 1), BLUE);
|
||||
expected.set_style(Rect::new(9, 0, 6, 1), GREEN);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
368
src/text/span.rs
368
src/text/span.rs
@@ -4,13 +4,33 @@ use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use super::StyledGrapheme;
|
||||
use crate::style::{Style, Styled};
|
||||
use crate::{prelude::*, widgets::Widget};
|
||||
|
||||
/// Represents a part of a line that is contiguous and where all characters share the same style.
|
||||
///
|
||||
/// A `Span` is the smallest unit of text that can be styled. It is usually combined in the [`Line`]
|
||||
/// type to represent a line of text where each `Span` may have a different style.
|
||||
///
|
||||
/// # Constructor Methods
|
||||
///
|
||||
/// - [`Span::default`] creates an span with empty content and the default style.
|
||||
/// - [`Span::raw`] creates an span with the specified content and the default style.
|
||||
/// - [`Span::styled`] creates an span with the specified content and style.
|
||||
///
|
||||
/// # Setter Methods
|
||||
///
|
||||
/// These methods are fluent setters. They return a new `Span` with the specified property set.
|
||||
///
|
||||
/// - [`Span::content`] sets the content of the span.
|
||||
/// - [`Span::style`] sets the style of the span.
|
||||
///
|
||||
/// # Other Methods
|
||||
///
|
||||
/// - [`Span::patch_style`] patches the style of the span, adding modifiers from the given style.
|
||||
/// - [`Span::reset_style`] resets the style of the span.
|
||||
/// - [`Span::width`] returns the unicode width of the content held by this span.
|
||||
/// - [`Span::styled_graphemes`] returns an iterator over the graphemes held by this span.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// A `Span` with `style` set to [`Style::default()`] can be created from a `&str`, a `String`, or
|
||||
@@ -35,21 +55,38 @@ use crate::style::{Style, Styled};
|
||||
///
|
||||
/// let span = Span::styled("test content", Style::new().green());
|
||||
/// let span = Span::styled(String::from("test content"), Style::new().green());
|
||||
///
|
||||
/// // using Stylize trait shortcuts
|
||||
/// let span = "test content".green();
|
||||
/// let span = String::from("test content").green();
|
||||
/// ```
|
||||
///
|
||||
/// `Span` implements [`Stylize`], which allows it to be styled using the shortcut methods. Styles
|
||||
/// applied are additive.
|
||||
/// `Span` implements the [`Styled`] trait, which allows it to be styled using the shortcut methods
|
||||
/// defined in the [`Stylize`] trait.
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::prelude::*;
|
||||
///
|
||||
/// let span = Span::raw("test content").green().on_yellow().italic();
|
||||
/// let span = Span::raw(String::from("test content")).green().on_yellow().italic();
|
||||
/// let span = Span::raw(String::from("test content"))
|
||||
/// .green()
|
||||
/// .on_yellow()
|
||||
/// .italic();
|
||||
/// ```
|
||||
///
|
||||
/// `Span` implements the [`Widget`] trait, which allows it to be rendered to a [`Buffer`]. Usually
|
||||
/// apps will use the [`Paragraph`] widget instead of rendering `Span` directly, as it handles text
|
||||
/// wrapping and alignment for you.
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::prelude::*;
|
||||
///
|
||||
/// # fn render_frame(frame: &mut Frame) {
|
||||
/// frame.render_widget("test content".green().on_yellow().italic(), frame.size());
|
||||
/// # }
|
||||
/// ```
|
||||
/// [`Line`]: crate::text::Line
|
||||
/// [`Paragraph`]: crate::widgets::Paragraph
|
||||
/// [`Stylize`]: crate::style::Stylize
|
||||
/// [`Cow<str>`]: std::borrow::Cow
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
@@ -82,6 +119,12 @@ impl<'a> Span<'a> {
|
||||
|
||||
/// Create a span with the specified style.
|
||||
///
|
||||
/// `content` accepts any type that is convertible to [`Cow<str>`] (e.g. `&str`, `String`,
|
||||
/// `&String`, etc.).
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
@@ -90,16 +133,104 @@ impl<'a> Span<'a> {
|
||||
/// Span::styled("test content", style);
|
||||
/// Span::styled(String::from("test content"), style);
|
||||
/// ```
|
||||
pub fn styled<T>(content: T, style: Style) -> Span<'a>
|
||||
pub fn styled<T, S>(content: T, style: S) -> Span<'a>
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
S: Into<Style>,
|
||||
{
|
||||
Span {
|
||||
content: content.into(),
|
||||
style,
|
||||
style: style.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the content of the span.
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
///
|
||||
/// Accepts any type that can be converted to [`Cow<str>`] (e.g. `&str`, `String`, `&String`,
|
||||
/// etc.).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let mut span = Span::default().content("content");
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn content<T>(mut self, content: T) -> Self
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
{
|
||||
self.content = content.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the span.
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
///
|
||||
/// In contrast to [`Span::patch_style`], this method replaces the style of the span instead of
|
||||
/// patching it.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let mut span = Span::default().style(Style::new().green());
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Patches the style of the Span, adding modifiers from the given style.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let span = Span::styled("test content", Style::new().green().italic())
|
||||
/// .patch_style(Style::new().red().on_yellow().bold());
|
||||
/// assert_eq!(span.style, Style::new().red().on_yellow().italic().bold());
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn patch_style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.style = self.style.patch(style);
|
||||
self
|
||||
}
|
||||
|
||||
/// Resets the style of the Span.
|
||||
///
|
||||
/// This is Equivalent to calling `patch_style(Style::reset())`.
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let span = Span::styled(
|
||||
/// "Test Content",
|
||||
/// Style::new().dark_gray().on_yellow().italic(),
|
||||
/// )
|
||||
/// .reset_style();
|
||||
/// assert_eq!(span.style, Style::reset());
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn reset_style(self) -> Self {
|
||||
self.patch_style(Style::reset())
|
||||
}
|
||||
|
||||
/// Returns the unicode width of the content held by this span.
|
||||
pub fn width(&self) -> usize {
|
||||
self.content.width()
|
||||
@@ -110,16 +241,21 @@ impl<'a> Span<'a> {
|
||||
/// `base_style` is the [`Style`] that will be patched with the `Span`'s `style` to get the
|
||||
/// resulting [`Style`].
|
||||
///
|
||||
/// `base_style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`],
|
||||
/// or your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use std::iter::Iterator;
|
||||
///
|
||||
/// use ratatui::{prelude::*, text::StyledGrapheme};
|
||||
///
|
||||
/// let span = Span::styled("Test", Style::new().green().italic());
|
||||
/// let style = Style::new().red().on_yellow();
|
||||
/// assert_eq!(
|
||||
/// span.styled_graphemes(style).collect::<Vec<StyledGrapheme>>(),
|
||||
/// span.styled_graphemes(style)
|
||||
/// .collect::<Vec<StyledGrapheme>>(),
|
||||
/// vec![
|
||||
/// StyledGrapheme::new("T", Style::new().green().on_yellow().italic()),
|
||||
/// StyledGrapheme::new("e", Style::new().green().on_yellow().italic()),
|
||||
@@ -128,48 +264,16 @@ impl<'a> Span<'a> {
|
||||
/// ],
|
||||
/// );
|
||||
/// ```
|
||||
pub fn styled_graphemes(
|
||||
pub fn styled_graphemes<S: Into<Style>>(
|
||||
&'a self,
|
||||
base_style: Style,
|
||||
base_style: S,
|
||||
) -> impl Iterator<Item = StyledGrapheme<'a>> {
|
||||
let style = base_style.into().patch(self.style);
|
||||
self.content
|
||||
.as_ref()
|
||||
.graphemes(true)
|
||||
.filter(|g| *g != "\n")
|
||||
.map(move |g| StyledGrapheme {
|
||||
symbol: g,
|
||||
style: base_style.patch(self.style),
|
||||
})
|
||||
}
|
||||
|
||||
/// Patches the style of the Span, adding modifiers from the given style.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let mut span = Span::styled("test content", Style::new().green().italic());
|
||||
/// span.patch_style(Style::new().red().on_yellow().bold());
|
||||
/// assert_eq!(span.style, Style::new().red().on_yellow().italic().bold());
|
||||
/// ```
|
||||
pub fn patch_style(&mut self, style: Style) {
|
||||
self.style = self.style.patch(style);
|
||||
}
|
||||
|
||||
/// Resets the style of the Span.
|
||||
///
|
||||
/// This is Equivalent to calling `patch_style(Style::reset())`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let mut span = Span::styled("Test Content", Style::new().green().on_yellow().italic());
|
||||
/// span.reset_style();
|
||||
/// assert_eq!(span.style, Style::reset());
|
||||
/// ```
|
||||
pub fn reset_style(&mut self) {
|
||||
self.patch_style(Style::reset());
|
||||
.map(move |g| StyledGrapheme { symbol: g, style })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,16 +293,53 @@ impl<'a> Styled for Span<'a> {
|
||||
self.style
|
||||
}
|
||||
|
||||
fn set_style(mut self, style: Style) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
|
||||
self.style(style)
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Span<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let Rect {
|
||||
x: mut current_x,
|
||||
y,
|
||||
width,
|
||||
..
|
||||
} = area;
|
||||
let max_x = Ord::min(current_x.saturating_add(width), buf.area.right());
|
||||
for g in self.styled_graphemes(Style::default()) {
|
||||
let symbol_width = g.symbol.width();
|
||||
let next_x = current_x.saturating_add(symbol_width as u16);
|
||||
if next_x > max_x {
|
||||
break;
|
||||
}
|
||||
buf.get_mut(current_x, y)
|
||||
.set_symbol(g.symbol)
|
||||
.set_style(g.style);
|
||||
|
||||
// multi-width graphemes must clear the cells of characters that are hidden by the
|
||||
// grapheme, otherwise the hidden characters will be re-rendered if the grapheme is
|
||||
// overwritten.
|
||||
for i in (current_x + 1)..next_x {
|
||||
buf.get_mut(i, y).reset();
|
||||
// it may seem odd that the style of the hidden cells are not set to the style of
|
||||
// the grapheme, but this is how the existing buffer.set_span() method works.
|
||||
// buf.get_mut(i, y).set_style(g.style);
|
||||
}
|
||||
current_x = next_x;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Span<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", &self.content)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::style::Stylize;
|
||||
|
||||
#[test]
|
||||
fn default() {
|
||||
@@ -239,6 +380,18 @@ mod tests {
|
||||
assert_eq!(span.style, style);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_content() {
|
||||
let span = Span::default().content("test content");
|
||||
assert_eq!(span.content, Cow::Borrowed("test content"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_style() {
|
||||
let span = Span::default().style(Style::new().green());
|
||||
assert_eq!(span.style, Style::new().green());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_ref_str_borrowed_cow() {
|
||||
let content = "test content";
|
||||
@@ -273,15 +426,14 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn reset_style() {
|
||||
let mut span = Span::styled("test content", Style::new().green());
|
||||
span.reset_style();
|
||||
let span = Span::styled("test content", Style::new().green()).reset_style();
|
||||
assert_eq!(span.style, Style::reset());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn patch_style() {
|
||||
let mut span = Span::styled("test content", Style::new().green().on_yellow());
|
||||
span.patch_style(Style::new().red().bold());
|
||||
let span = Span::styled("test content", Style::new().green().on_yellow())
|
||||
.patch_style(Style::new().red().bold());
|
||||
assert_eq!(span.style, Style::new().red().on_yellow().bold());
|
||||
}
|
||||
|
||||
@@ -303,4 +455,118 @@ mod tests {
|
||||
assert_eq!(stylized.content, Cow::Borrowed("test content"));
|
||||
assert_eq!(stylized.style, Style::new().green().on_yellow().bold());
|
||||
}
|
||||
#[test]
|
||||
fn display_span() {
|
||||
let span = Span::raw("test content");
|
||||
|
||||
assert_eq!(format!("{span}"), "test content");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_styled_span() {
|
||||
let stylized_span = Span::styled("stylized test content", Style::new().green());
|
||||
|
||||
assert_eq!(format!("{stylized_span}"), "stylized test content");
|
||||
}
|
||||
|
||||
mod widget {
|
||||
use super::*;
|
||||
use crate::{assert_buffer_eq, style::Stylize};
|
||||
|
||||
#[test]
|
||||
fn render() {
|
||||
let style = Style::new().green().on_yellow();
|
||||
let span = Span::styled("test content", style);
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
|
||||
span.render(buf.area, &mut buf);
|
||||
|
||||
let expected = Buffer::with_lines(vec![Line::from(vec![
|
||||
"test content".green().on_yellow(),
|
||||
" ".into(),
|
||||
])]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
}
|
||||
|
||||
/// When the content of the span is longer than the area passed to render, the content
|
||||
/// should be truncated
|
||||
#[test]
|
||||
fn render_truncates_too_long_content() {
|
||||
let style = Style::new().green().on_yellow();
|
||||
let span = Span::styled("test content", style);
|
||||
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 1));
|
||||
span.render(Rect::new(0, 0, 5, 1), &mut buf);
|
||||
|
||||
let mut expected = Buffer::with_lines(vec![Line::from("test ")]);
|
||||
expected.set_style(Rect::new(0, 0, 5, 1), (Color::Green, Color::Yellow));
|
||||
|
||||
assert_buffer_eq!(buf, expected);
|
||||
}
|
||||
|
||||
/// When there is already a style set on the buffer, the style of the span should be
|
||||
/// patched with the existing style
|
||||
#[test]
|
||||
fn render_patches_existing_style() {
|
||||
let style = Style::new().green().on_yellow();
|
||||
let span = Span::styled("test content", style);
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
|
||||
buf.set_style(buf.area, Style::new().italic());
|
||||
span.render(buf.area, &mut buf);
|
||||
|
||||
let expected = Buffer::with_lines(vec![Line::from(vec![
|
||||
"test content".green().on_yellow().italic(),
|
||||
" ".italic(),
|
||||
])]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
}
|
||||
|
||||
/// When the span contains a multi-width grapheme, the grapheme will ensure that the cells
|
||||
/// of the hidden characters are cleared.
|
||||
#[test]
|
||||
fn render_multi_width_symbol() {
|
||||
let style = Style::new().green().on_yellow();
|
||||
let span = Span::styled("test 😃 content", style);
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
|
||||
span.render(buf.area, &mut buf);
|
||||
|
||||
// The existing code in buffer.set_line() handles multi-width graphemes by clearing the
|
||||
// cells of the hidden characters. This test ensures that the existing behavior is
|
||||
// preserved.
|
||||
let expected = Buffer::with_lines(vec!["test 😃 content".green().on_yellow()]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
}
|
||||
|
||||
/// When the span contains a multi-width grapheme that does not fit in the area passed to
|
||||
/// render, the entire grapheme will be truncated.
|
||||
#[test]
|
||||
fn render_multi_width_symbol_truncates_entire_symbol() {
|
||||
// the 😃 emoji is 2 columns wide so it will be truncated
|
||||
let style = Style::new().green().on_yellow();
|
||||
let span = Span::styled("test 😃 content", style);
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 6, 1));
|
||||
span.render(buf.area, &mut buf);
|
||||
|
||||
let expected = Buffer::with_lines(vec![Line::from(vec![
|
||||
"test ".green().on_yellow(),
|
||||
" ".into(),
|
||||
])]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
}
|
||||
|
||||
/// When the area passed to render overflows the buffer, the content should be truncated
|
||||
/// to fit the buffer.
|
||||
#[test]
|
||||
fn render_overflowing_area_truncates() {
|
||||
let style = Style::new().green().on_yellow();
|
||||
let span = Span::styled("test content", style);
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
|
||||
span.render(Rect::new(10, 0, 20, 1), &mut buf);
|
||||
|
||||
let expected = Buffer::with_lines(vec![Line::from(vec![
|
||||
" ".into(),
|
||||
"test ".green().on_yellow(),
|
||||
])]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
425
src/text/text.rs
425
src/text/text.rs
@@ -1,19 +1,38 @@
|
||||
#![warn(missing_docs)]
|
||||
use std::borrow::Cow;
|
||||
|
||||
use super::{Line, Span};
|
||||
use crate::style::Style;
|
||||
use itertools::{Itertools, Position};
|
||||
|
||||
use crate::{prelude::*, widgets::Widget};
|
||||
|
||||
/// A string split over multiple lines where each line is composed of several clusters, each with
|
||||
/// their own style.
|
||||
///
|
||||
/// A [`Text`], like a [`Span`], can be constructed using one of the many `From` implementations
|
||||
/// A [`Text`], like a [`Line`], can be constructed using one of the many `From` implementations
|
||||
/// or via the [`Text::raw`] and [`Text::styled`] methods. Helpfully, [`Text`] also implements
|
||||
/// [`core::iter::Extend`] which enables the concatenation of several [`Text`] blocks.
|
||||
///
|
||||
/// The text's [`Style`] is used by the rendering widget to determine how to style the text. Each
|
||||
/// [`Line`] in the text will be styled with the [`Style`] of the text, and then with its own
|
||||
/// [`Style`]. `Text` also implements [`Styled`] which means you can use the methods of the
|
||||
/// [`Stylize`] trait.
|
||||
///
|
||||
/// The text's [`Alignment`] can be set using [`Text::alignment`]. Lines composing the text can
|
||||
/// also be individually aligned with [`Line::alignment`].
|
||||
///
|
||||
/// `Text` implements the [`Widget`] trait, which means it can be rendered to a [`Buffer`].
|
||||
/// Usually apps will use the [`Paragraph`] widget instead of rendering a `Text` directly as it
|
||||
/// provides more functionality.
|
||||
///
|
||||
/// [`Paragraph`]: crate::widgets::Paragraph
|
||||
/// [`Widget`]: crate::widgets::Widget
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::prelude::*;
|
||||
///
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
/// let style = Style::default()
|
||||
/// .fg(Color::Yellow)
|
||||
/// .add_modifier(Modifier::ITALIC);
|
||||
///
|
||||
/// // An initial two lines of `Text` built from a `&str`
|
||||
/// let mut text = Text::from("The first line\nThe second line");
|
||||
@@ -29,7 +48,12 @@ use crate::style::Style;
|
||||
/// ```
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Text<'a> {
|
||||
/// The lines that make up this piece of text.
|
||||
pub lines: Vec<Line<'a>>,
|
||||
/// The style of this text.
|
||||
pub style: Style,
|
||||
/// The alignment of this text.
|
||||
pub alignment: Option<Alignment>,
|
||||
}
|
||||
|
||||
impl<'a> Text<'a> {
|
||||
@@ -58,21 +82,25 @@ impl<'a> Text<'a> {
|
||||
|
||||
/// Create some text (potentially multiple lines) with a style.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
/// let style = Style::default()
|
||||
/// .fg(Color::Yellow)
|
||||
/// .add_modifier(Modifier::ITALIC);
|
||||
/// Text::styled("The first line\nThe second line", style);
|
||||
/// Text::styled(String::from("The first line\nThe second line"), style);
|
||||
/// ```
|
||||
pub fn styled<T>(content: T, style: Style) -> Text<'a>
|
||||
pub fn styled<T, S>(content: T, style: S) -> Text<'a>
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
S: Into<Style>,
|
||||
{
|
||||
let mut text = Text::raw(content);
|
||||
text.patch_style(style);
|
||||
text
|
||||
Text::raw(content).patch_style(style)
|
||||
}
|
||||
|
||||
/// Returns the max width of all the lines.
|
||||
@@ -101,46 +129,131 @@ impl<'a> Text<'a> {
|
||||
self.lines.len()
|
||||
}
|
||||
|
||||
/// Patches the style of each line in an existing Text, adding modifiers from the given style.
|
||||
/// Sets the style of this text.
|
||||
///
|
||||
/// Defaults to [`Style::default()`].
|
||||
///
|
||||
/// Note: This field was added in v0.26.0. Prior to that, the style of a text was determined
|
||||
/// only by the style of each [`Line`] contained in the line. For this reason, this field may
|
||||
/// not be supported by all widgets (outside of the `ratatui` crate itself).
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let mut line = Text::from("foo").style(Style::new().red());
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Patches the style of this Text, adding modifiers from the given style.
|
||||
///
|
||||
/// This is useful for when you want to apply a style to a text that already has some styling.
|
||||
/// In contrast to [`Text::style`], this method will not overwrite the existing style, but
|
||||
/// instead will add the given style's modifiers to this text's style.
|
||||
///
|
||||
/// `Text` also implements [`Styled`] which means you can use the methods of the [`Stylize`]
|
||||
/// trait.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
/// let mut raw_text = Text::raw("The first line\nThe second line");
|
||||
/// let styled_text = Text::styled(String::from("The first line\nThe second line"), style);
|
||||
/// let raw_text = Text::styled("The first line\nThe second line", Modifier::ITALIC);
|
||||
/// let styled_text = Text::styled(
|
||||
/// String::from("The first line\nThe second line"),
|
||||
/// (Color::Yellow, Modifier::ITALIC),
|
||||
/// );
|
||||
/// assert_ne!(raw_text, styled_text);
|
||||
///
|
||||
/// raw_text.patch_style(style);
|
||||
/// let raw_text = raw_text.patch_style(Color::Yellow);
|
||||
/// assert_eq!(raw_text, styled_text);
|
||||
/// ```
|
||||
pub fn patch_style(&mut self, style: Style) {
|
||||
for line in &mut self.lines {
|
||||
line.patch_style(style);
|
||||
}
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn patch_style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.style = self.style.patch(style);
|
||||
self
|
||||
}
|
||||
|
||||
/// Resets the style of the Text.
|
||||
/// Equivalent to calling `patch_style(Style::reset())`.
|
||||
///
|
||||
/// Equivalent to calling [`patch_style(Style::reset())`](Text::patch_style).
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
/// let mut text = Text::styled("The first line\nThe second line", style);
|
||||
/// let text = Text::styled(
|
||||
/// "The first line\nThe second line",
|
||||
/// (Color::Yellow, Modifier::ITALIC),
|
||||
/// );
|
||||
///
|
||||
/// text.reset_style();
|
||||
/// for line in &text.lines {
|
||||
/// for span in &line.spans {
|
||||
/// assert_eq!(Style::reset(), span.style);
|
||||
/// }
|
||||
/// }
|
||||
/// let text = text.reset_style();
|
||||
/// assert_eq!(Style::reset(), text.style);
|
||||
/// ```
|
||||
pub fn reset_style(&mut self) {
|
||||
for line in &mut self.lines {
|
||||
line.reset_style();
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn reset_style(self) -> Self {
|
||||
self.patch_style(Style::reset())
|
||||
}
|
||||
|
||||
/// Sets the alignment for this text.
|
||||
///
|
||||
/// Defaults to: [`None`], meaning the alignment is determined by the rendering widget.
|
||||
///
|
||||
/// Alignment can be set individually on each line to override this text's alignment.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Set alignment to the whole text.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let mut text = Text::from("Hi, what's up?");
|
||||
/// assert_eq!(None, text.alignment);
|
||||
/// assert_eq!(
|
||||
/// Some(Alignment::Right),
|
||||
/// text.alignment(Alignment::Right).alignment
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// Set a default alignment and override it on a per line basis.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let text = Text::from(vec![
|
||||
/// Line::from("left").alignment(Alignment::Left),
|
||||
/// Line::from("default"),
|
||||
/// Line::from("default"),
|
||||
/// Line::from("right").alignment(Alignment::Right),
|
||||
/// ])
|
||||
/// .alignment(Alignment::Center);
|
||||
/// ```
|
||||
///
|
||||
/// Will render the following
|
||||
///
|
||||
/// ```plain
|
||||
/// left
|
||||
/// default
|
||||
/// default
|
||||
/// right
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn alignment(self, alignment: Alignment) -> Self {
|
||||
Self {
|
||||
alignment: Some(alignment),
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -167,19 +280,26 @@ impl<'a> From<Span<'a>> for Text<'a> {
|
||||
fn from(span: Span<'a>) -> Text<'a> {
|
||||
Text {
|
||||
lines: vec![Line::from(span)],
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Line<'a>> for Text<'a> {
|
||||
fn from(line: Line<'a>) -> Text<'a> {
|
||||
Text { lines: vec![line] }
|
||||
Text {
|
||||
lines: vec![line],
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Vec<Line<'a>>> for Text<'a> {
|
||||
fn from(lines: Vec<Line<'a>>) -> Text<'a> {
|
||||
Text { lines }
|
||||
Text {
|
||||
lines,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,6 +322,55 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Text<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
for (position, line) in self.lines.iter().with_position() {
|
||||
if position == Position::Last {
|
||||
write!(f, "{line}")?;
|
||||
} else {
|
||||
writeln!(f, "{line}")?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for Text<'a> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
buf.set_style(area, self.style);
|
||||
for (line, row) in self.lines.into_iter().zip(area.rows()) {
|
||||
let line_width = line.width() as u16;
|
||||
|
||||
let x_offset = match (self.alignment, line.alignment) {
|
||||
(Some(Alignment::Center), None) => area.width.saturating_sub(line_width) / 2,
|
||||
(Some(Alignment::Right), None) => area.width.saturating_sub(line_width),
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
let line_area = Rect {
|
||||
x: area.x + x_offset,
|
||||
y: row.y,
|
||||
width: area.width - x_offset,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
line.render(line_area, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Styled for Text<'a> {
|
||||
type Item = Text<'a>;
|
||||
|
||||
fn style(&self) -> Style {
|
||||
self.style
|
||||
}
|
||||
|
||||
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
|
||||
self.style(style)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -219,14 +388,12 @@ mod tests {
|
||||
#[test]
|
||||
fn styled() {
|
||||
let style = Style::new().yellow().italic();
|
||||
let text = Text::styled("The first line\nThe second line", style);
|
||||
assert_eq!(
|
||||
text.lines,
|
||||
vec![
|
||||
Line::from(Span::styled("The first line", style)),
|
||||
Line::from(Span::styled("The second line", style))
|
||||
]
|
||||
);
|
||||
let styled_text = Text::styled("The first line\nThe second line", style);
|
||||
|
||||
let mut text = Text::raw("The first line\nThe second line");
|
||||
text.style = style;
|
||||
|
||||
assert_eq!(styled_text, text);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -245,32 +412,20 @@ mod tests {
|
||||
fn patch_style() {
|
||||
let style = Style::new().yellow().italic();
|
||||
let style2 = Style::new().red().underlined();
|
||||
let mut text = Text::styled("The first line\nThe second line", style);
|
||||
let text = Text::styled("The first line\nThe second line", style).patch_style(style2);
|
||||
|
||||
text.patch_style(style2);
|
||||
let expected_style = Style::new().red().italic().underlined();
|
||||
assert_eq!(
|
||||
text.lines,
|
||||
vec![
|
||||
Line::from(Span::styled("The first line", expected_style)),
|
||||
Line::from(Span::styled("The second line", expected_style))
|
||||
]
|
||||
);
|
||||
let expected_text = Text::styled("The first line\nThe second line", expected_style);
|
||||
|
||||
assert_eq!(text, expected_text);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_style() {
|
||||
let style = Style::new().yellow().italic();
|
||||
let mut text = Text::styled("The first line\nThe second line", style);
|
||||
let text = Text::styled("The first line\nThe second line", style).reset_style();
|
||||
|
||||
text.reset_style();
|
||||
assert_eq!(
|
||||
text.lines,
|
||||
vec![
|
||||
Line::from(Span::styled("The first line", Style::reset())),
|
||||
Line::from(Span::styled("The second line", Style::reset()))
|
||||
]
|
||||
);
|
||||
assert_eq!(text.style, Style::reset());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -390,4 +545,158 @@ mod tests {
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_raw_text() {
|
||||
let text = Text::raw("The first line\nThe second line");
|
||||
|
||||
assert_eq!(format!("{text}"), "The first line\nThe second line");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_styled_text() {
|
||||
let styled_text = Text::styled(
|
||||
"The first line\nThe second line",
|
||||
Style::new().yellow().italic(),
|
||||
);
|
||||
|
||||
assert_eq!(format!("{styled_text}"), "The first line\nThe second line");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_text_from_vec() {
|
||||
let text_from_vec = Text::from(vec![
|
||||
Line::from("The first line"),
|
||||
Line::from("The second line"),
|
||||
]);
|
||||
|
||||
assert_eq!(
|
||||
format!("{text_from_vec}"),
|
||||
"The first line\nThe second line"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_extended_text() {
|
||||
let mut text = Text::from("The first line\nThe second line");
|
||||
|
||||
assert_eq!(format!("{text}"), "The first line\nThe second line");
|
||||
|
||||
text.extend(vec![
|
||||
Line::from("The third line"),
|
||||
Line::from("The fourth line"),
|
||||
]);
|
||||
|
||||
assert_eq!(
|
||||
format!("{text}"),
|
||||
"The first line\nThe second line\nThe third line\nThe fourth line"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stylize() {
|
||||
assert_eq!(Text::default().green().style, Color::Green.into());
|
||||
assert_eq!(
|
||||
Text::default().on_green().style,
|
||||
Style::new().bg(Color::Green)
|
||||
);
|
||||
assert_eq!(Text::default().italic().style, Modifier::ITALIC.into());
|
||||
}
|
||||
|
||||
mod widget {
|
||||
use super::*;
|
||||
use crate::{assert_buffer_eq, style::Color};
|
||||
|
||||
#[test]
|
||||
fn render() {
|
||||
let text = Text::from("foo");
|
||||
|
||||
let area = Rect::new(0, 0, 5, 1);
|
||||
let mut buf = Buffer::empty(area);
|
||||
text.render(area, &mut buf);
|
||||
|
||||
let expected_buf = Buffer::with_lines(vec!["foo "]);
|
||||
|
||||
assert_buffer_eq!(buf, expected_buf);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_right_aligned() {
|
||||
let text = Text::from("foo").alignment(Alignment::Right);
|
||||
|
||||
let area = Rect::new(0, 0, 5, 1);
|
||||
let mut buf = Buffer::empty(area);
|
||||
text.render(area, &mut buf);
|
||||
|
||||
let expected_buf = Buffer::with_lines(vec![" foo"]);
|
||||
|
||||
assert_buffer_eq!(buf, expected_buf);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_centered_odd() {
|
||||
let text = Text::from("foo").alignment(Alignment::Center);
|
||||
|
||||
let area = Rect::new(0, 0, 5, 1);
|
||||
let mut buf = Buffer::empty(area);
|
||||
text.render(area, &mut buf);
|
||||
|
||||
let expected_buf = Buffer::with_lines(vec![" foo "]);
|
||||
|
||||
assert_buffer_eq!(buf, expected_buf);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_centered_even() {
|
||||
let text = Text::from("foo").alignment(Alignment::Center);
|
||||
|
||||
let area = Rect::new(0, 0, 6, 1);
|
||||
let mut buf = Buffer::empty(area);
|
||||
text.render(area, &mut buf);
|
||||
|
||||
let expected_buf = Buffer::with_lines(vec![" foo "]);
|
||||
|
||||
assert_buffer_eq!(buf, expected_buf);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_one_line_right() {
|
||||
let text = Text::from(vec![
|
||||
"foo".into(),
|
||||
Line::from("bar").alignment(Alignment::Center),
|
||||
])
|
||||
.alignment(Alignment::Right);
|
||||
|
||||
let area = Rect::new(0, 0, 5, 2);
|
||||
let mut buf = Buffer::empty(area);
|
||||
text.render(area, &mut buf);
|
||||
|
||||
let expected_buf = Buffer::with_lines(vec![" foo", " bar "]);
|
||||
|
||||
assert_buffer_eq!(buf, expected_buf);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_only_styles_line_area() {
|
||||
let area = Rect::new(0, 0, 5, 1);
|
||||
let mut buf = Buffer::empty(area);
|
||||
Text::from("foo".on_blue()).render(area, &mut buf);
|
||||
|
||||
let mut expected = Buffer::with_lines(vec!["foo "]);
|
||||
expected.set_style(Rect::new(0, 0, 3, 1), Style::new().bg(Color::Blue));
|
||||
|
||||
assert_buffer_eq!(buf, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_truncates() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 6, 1));
|
||||
Text::from("foobar".on_blue()).render(Rect::new(0, 0, 3, 1), &mut buf);
|
||||
|
||||
let mut expected = Buffer::with_lines(vec!["foo "]);
|
||||
expected.set_style(Rect::new(0, 0, 3, 1), Style::new().bg(Color::Blue));
|
||||
|
||||
assert_buffer_eq!(buf, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
134
src/widgets.rs
134
src/widgets.rs
@@ -1,7 +1,7 @@
|
||||
//! `widgets` is a collection of types that implement [`Widget`] or [`StatefulWidget`] or both.
|
||||
//!
|
||||
//! All widgets are implemented using the builder pattern and are consumable objects. They are not
|
||||
//! meant to be stored but used as *commands* to draw common figures in the UI.
|
||||
//! Widgets are created for each frame as they are consumed after rendered.
|
||||
//! They are not meant to be stored but used as *commands* to draw common figures in the UI.
|
||||
//!
|
||||
//! The available widgets are:
|
||||
//! - [`Block`]: a basic widget that draws a block with optional borders, titles and styles.
|
||||
@@ -22,6 +22,7 @@
|
||||
//! [`Canvas`]: crate::widgets::canvas::Canvas
|
||||
mod barchart;
|
||||
pub mod block;
|
||||
mod borders;
|
||||
#[cfg(feature = "widget-calendar")]
|
||||
pub mod calendar;
|
||||
pub mod canvas;
|
||||
@@ -36,17 +37,14 @@ mod sparkline;
|
||||
mod table;
|
||||
mod tabs;
|
||||
|
||||
use std::fmt::{self, Debug};
|
||||
|
||||
use bitflags::bitflags;
|
||||
|
||||
pub use self::{
|
||||
barchart::{Bar, BarChart, BarGroup},
|
||||
block::{Block, BorderType, Padding},
|
||||
chart::{Axis, Chart, Dataset, GraphType},
|
||||
borders::*,
|
||||
chart::{Axis, Chart, Dataset, GraphType, LegendPosition},
|
||||
clear::Clear,
|
||||
gauge::{Gauge, LineGauge},
|
||||
list::{List, ListItem, ListState},
|
||||
list::{List, ListDirection, ListItem, ListState},
|
||||
paragraph::{Paragraph, Wrap},
|
||||
scrollbar::{ScrollDirection, Scrollbar, ScrollbarOrientation, ScrollbarState},
|
||||
sparkline::{RenderDirection, Sparkline},
|
||||
@@ -55,55 +53,6 @@ pub use self::{
|
||||
};
|
||||
use crate::{buffer::Buffer, layout::Rect};
|
||||
|
||||
bitflags! {
|
||||
/// Bitflags that can be composed to set the visible borders essentially on the block widget.
|
||||
#[derive(Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub struct Borders: u8 {
|
||||
/// Show no border (default)
|
||||
const NONE = 0b0000;
|
||||
/// Show the top border
|
||||
const TOP = 0b0001;
|
||||
/// Show the right border
|
||||
const RIGHT = 0b0010;
|
||||
/// Show the bottom border
|
||||
const BOTTOM = 0b0100;
|
||||
/// Show the left border
|
||||
const LEFT = 0b1000;
|
||||
/// Show all borders
|
||||
const ALL = Self::TOP.bits() | Self::RIGHT.bits() | Self::BOTTOM.bits() | Self::LEFT.bits();
|
||||
}
|
||||
}
|
||||
|
||||
/// Implement the `Debug` trait for the `Borders` bitflags. This is a manual implementation to
|
||||
/// display the flags in a more readable way. The default implementation would display the
|
||||
/// flags as 'Border(0x0)' for `Borders::NONE` for example.
|
||||
impl Debug for Borders {
|
||||
/// Display the Borders bitflags as a list of names. For example, `Borders::NONE` will be
|
||||
/// displayed as `NONE` and `Borders::ALL` will be displayed as `ALL`. If multiple flags are
|
||||
/// set, they will be displayed separated by a pipe character.
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if self.is_empty() {
|
||||
return write!(f, "NONE");
|
||||
}
|
||||
if self.is_all() {
|
||||
return write!(f, "ALL");
|
||||
}
|
||||
let mut first = true;
|
||||
for (name, border) in self.iter_names() {
|
||||
if border == Borders::NONE {
|
||||
continue;
|
||||
}
|
||||
if first {
|
||||
write!(f, "{name}")?;
|
||||
first = false;
|
||||
} else {
|
||||
write!(f, " | {name}")?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Base requirements for a Widget
|
||||
pub trait Widget {
|
||||
/// Draws the current state of the widget in the given buffer. That is the only method required
|
||||
@@ -130,6 +79,7 @@ pub trait Widget {
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use std::io;
|
||||
///
|
||||
/// use ratatui::{backend::TestBackend, prelude::*, widgets::*};
|
||||
///
|
||||
/// // Let's say we have some events to display.
|
||||
@@ -139,7 +89,7 @@ pub trait Widget {
|
||||
/// // `state` is the state that can be modified by the UI. It stores the index of the selected
|
||||
/// // item as well as the offset computed during the previous draw call (used to implement
|
||||
/// // natural scrolling).
|
||||
/// state: ListState
|
||||
/// state: ListState,
|
||||
/// }
|
||||
///
|
||||
/// impl Events {
|
||||
@@ -199,16 +149,17 @@ pub trait Widget {
|
||||
/// # let backend = TestBackend::new(5, 5);
|
||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||
///
|
||||
/// let mut events = Events::new(vec![
|
||||
/// String::from("Item 1"),
|
||||
/// String::from("Item 2")
|
||||
/// ]);
|
||||
/// let mut events = Events::new(vec![String::from("Item 1"), String::from("Item 2")]);
|
||||
///
|
||||
/// loop {
|
||||
/// terminal.draw(|f| {
|
||||
/// // The items managed by the application are transformed to something
|
||||
/// // that is understood by ratatui.
|
||||
/// let items: Vec<ListItem>= events.items.iter().map(|i| ListItem::new(i.as_str())).collect();
|
||||
/// let items: Vec<ListItem> = events
|
||||
/// .items
|
||||
/// .iter()
|
||||
/// .map(|i| ListItem::new(i.as_str()))
|
||||
/// .collect();
|
||||
/// // The `List` widget is then built with those items.
|
||||
/// let list = List::new(items);
|
||||
/// // Finally the widget is rendered using the associated state. `events.state` is
|
||||
@@ -224,60 +175,3 @@ pub trait StatefulWidget {
|
||||
type State;
|
||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State);
|
||||
}
|
||||
|
||||
/// Macro that constructs and returns a [`Borders`] object from TOP, BOTTOM, LEFT, RIGHT, NONE, and
|
||||
/// ALL. Internally it creates an empty `Borders` object and then inserts each bit flag specified
|
||||
/// into it using `Borders::insert()`.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
///```
|
||||
/// use ratatui::{border, prelude::*, widgets::*};
|
||||
///
|
||||
/// Block::default()
|
||||
/// //Construct a `Borders` object and use it in place
|
||||
/// .borders(border!(TOP, BOTTOM));
|
||||
///
|
||||
/// //`border!` can be called with any order of individual sides
|
||||
/// let bottom_first = border!(BOTTOM, LEFT, TOP);
|
||||
/// //with the ALL keyword which works as expected
|
||||
/// let all = border!(ALL);
|
||||
/// //or with nothing to return a `Borders::NONE' bitflag.
|
||||
/// let none = border!(NONE);
|
||||
/// ```
|
||||
#[cfg(feature = "macros")]
|
||||
#[macro_export]
|
||||
macro_rules! border {
|
||||
( $($b:tt), +) => {{
|
||||
let mut border = Borders::empty();
|
||||
$(
|
||||
border.insert(Borders::$b);
|
||||
)*
|
||||
border
|
||||
}};
|
||||
() =>{
|
||||
Borders::NONE
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_borders_debug() {
|
||||
assert_eq!(format!("{:?}", Borders::empty()), "NONE");
|
||||
assert_eq!(format!("{:?}", Borders::NONE), "NONE");
|
||||
assert_eq!(format!("{:?}", Borders::TOP), "TOP");
|
||||
assert_eq!(format!("{:?}", Borders::BOTTOM), "BOTTOM");
|
||||
assert_eq!(format!("{:?}", Borders::LEFT), "LEFT");
|
||||
assert_eq!(format!("{:?}", Borders::RIGHT), "RIGHT");
|
||||
assert_eq!(format!("{:?}", Borders::ALL), "ALL");
|
||||
assert_eq!(format!("{:?}", Borders::all()), "ALL");
|
||||
|
||||
assert_eq!(
|
||||
format!("{:?}", Borders::TOP | Borders::BOTTOM),
|
||||
"TOP | BOTTOM"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user