Compare commits
148 Commits
v0.26.0
...
kd/multi-s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ccf9d92f10 | ||
|
|
b712034644 | ||
|
|
38fca62fa9 | ||
|
|
3a51a027a6 | ||
|
|
5b30f2275c | ||
|
|
9bd89c218a | ||
|
|
2cfe82a47e | ||
|
|
b4c27c744c | ||
|
|
977a4899c8 | ||
|
|
cd27b4829a | ||
|
|
feee871519 | ||
|
|
1a4bb1cbb8 | ||
|
|
839cca20bf | ||
|
|
f945a0bcff | ||
|
|
eb281df974 | ||
|
|
28e81c0714 | ||
|
|
0051bb2037 | ||
|
|
477217c77a | ||
|
|
31de3586f7 | ||
|
|
f702025b75 | ||
|
|
76e5fe5a9a | ||
|
|
366cbae09f | ||
|
|
ec763af851 | ||
|
|
699c2d7c8d | ||
|
|
3cc29bdada | ||
|
|
8de3d52469 | ||
|
|
b30411d1c7 | ||
|
|
aa4260f92c | ||
|
|
5f1e119563 | ||
|
|
4d1784f2de | ||
|
|
baedc39494 | ||
|
|
366c2a0e6d | ||
|
|
bef2bc1e7c | ||
|
|
64eb3913a4 | ||
|
|
e95230beda | ||
|
|
f4637d40c3 | ||
|
|
da1ade7b2e | ||
|
|
1706b0a3e4 | ||
|
|
4392759501 | ||
|
|
20fc0ddfca | ||
|
|
3687f78f6a | ||
|
|
5fbb77ad20 | ||
|
|
97ee102f17 | ||
|
|
c442dfd1ad | ||
|
|
0a164965ea | ||
|
|
bf0923473c | ||
|
|
81b96338ea | ||
|
|
2e71c1874e | ||
|
|
c75aa1990f | ||
|
|
326a461f9a | ||
|
|
f3172c59d4 | ||
|
|
bef5bcf750 | ||
|
|
11264787d0 | ||
|
|
9b3c260b76 | ||
|
|
363c4c54e8 | ||
|
|
b7778e5cd1 | ||
|
|
b5061c5250 | ||
|
|
359204c929 | ||
|
|
14461c3a35 | ||
|
|
3b002fdcab | ||
|
|
0207160784 | ||
|
|
26af65043e | ||
|
|
07da90a718 | ||
|
|
125ee929ee | ||
|
|
742a5ead06 | ||
|
|
8719608bda | ||
|
|
f6c4e447e6 | ||
|
|
c56f49b9fb | ||
|
|
078e97e4ff | ||
|
|
8e68db9e2f | ||
|
|
541f0f9953 | ||
|
|
88bfb5a430 | ||
|
|
c4ce7e8ff6 | ||
|
|
3be189e3c6 | ||
|
|
5c4efacd1d | ||
|
|
bbb6d65e06 | ||
|
|
fdb14dc7cd | ||
|
|
9b3b23ac14 | ||
|
|
58b6e0be0f | ||
|
|
6e6ba27a12 | ||
|
|
c870a41057 | ||
|
|
a6036ad789 | ||
|
|
060d26b6dc | ||
|
|
a4e84a6a7f | ||
|
|
fcbea9ee68 | ||
|
|
14b24e7585 | ||
|
|
5ed1f43c62 | ||
|
|
c8c7924e0c | ||
|
|
e3afe7c8a1 | ||
|
|
a1f54de7d6 | ||
|
|
b8ea190bf2 | ||
|
|
0de5238ed3 | ||
|
|
df5dddfbc9 | ||
|
|
f1398ae6cb | ||
|
|
525848ff4e | ||
|
|
660c7183c7 | ||
|
|
ab951fae81 | ||
|
|
3cd4369176 | ||
|
|
9bc014d7f1 | ||
|
|
36a0cd56e5 | ||
|
|
b831c5688c | ||
|
|
8195f526cb | ||
|
|
f7f66928a8 | ||
|
|
01418eb7c2 | ||
|
|
8536760e78 | ||
|
|
a558b19c9a | ||
|
|
183c07ef43 | ||
|
|
a13867ffce | ||
|
|
5b00e3aae9 | ||
|
|
38c17e091c | ||
|
|
3834374652 | ||
|
|
27680c05ce | ||
|
|
6fd5f631bb | ||
|
|
37b957c7e1 | ||
|
|
e02f4768ce | ||
|
|
c12bcfefa2 | ||
|
|
94f4547dcf | ||
|
|
3a6b8808ed | ||
|
|
1cff511934 | ||
|
|
b5bdde079e | ||
|
|
654949bb00 | ||
|
|
943c0431d9 | ||
|
|
65e7923753 | ||
|
|
d0067c8815 | ||
|
|
35e971f7eb | ||
|
|
b0314c5731 | ||
|
|
12f67e810f | ||
|
|
11b452d56f | ||
|
|
efd1e47642 | ||
|
|
410d08b2b5 | ||
|
|
a4892ad444 | ||
|
|
18870ce990 | ||
|
|
1f208ffd03 | ||
|
|
e51ca6e0d2 | ||
|
|
9182f47026 | ||
|
|
91040c0865 | ||
|
|
2202059259 | ||
|
|
8fb46301a0 | ||
|
|
0dcdbea083 | ||
|
|
74a051147a | ||
|
|
c3fb25898f | ||
|
|
fae5862c6e | ||
|
|
788e6d9fb8 | ||
|
|
14c67fbb52 | ||
|
|
096346350e | ||
|
|
61a827821d | ||
|
|
fbb5dfaaa9 | ||
|
|
d2d91f754c |
@@ -2,6 +2,12 @@
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.rs]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
5
.github/CODEOWNERS
vendored
5
.github/CODEOWNERS
vendored
@@ -5,4 +5,7 @@
|
||||
# https://git-scm.com/docs/gitignore#_pattern_format
|
||||
|
||||
# Maintainers
|
||||
* @orhun @mindoodoo @sayanarijit @joshka @kdheepak @Valentin271
|
||||
* @orhun @joshka @kdheepak @Valentin271 @EdJoPaTo
|
||||
|
||||
# Past maintainers
|
||||
# @mindoodoo @sayanarijit
|
||||
|
||||
2
.github/workflows/cd.yml
vendored
2
.github/workflows/cd.yml
vendored
@@ -37,7 +37,7 @@ jobs:
|
||||
args: --allow-dirty --token ${{ secrets.CARGO_TOKEN }}
|
||||
|
||||
- name: Generate a changelog
|
||||
uses: orhun/git-cliff-action@v2
|
||||
uses: orhun/git-cliff-action@v3
|
||||
with:
|
||||
config: cliff.toml
|
||||
args: --unreleased --tag ${{ env.NEXT_TAG }} --strip header
|
||||
|
||||
19
.github/workflows/check-pr.yml
vendored
19
.github/workflows/check-pr.yml
vendored
@@ -44,24 +44,6 @@ 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:
|
||||
@@ -102,4 +84,3 @@ jobs:
|
||||
echo "Pull request is labeled as 'do not merge'"
|
||||
echo "This workflow fails so that the pull request cannot be merged"
|
||||
exit 1
|
||||
|
||||
|
||||
17
.github/workflows/ci.yml
vendored
17
.github/workflows/ci.yml
vendored
@@ -29,13 +29,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: actions/checkout@v4
|
||||
- name: Checkout
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- name: Install Rust nightly
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
with:
|
||||
@@ -48,11 +42,6 @@ jobs:
|
||||
run: cargo make lint-format
|
||||
- name: Check documentation
|
||||
run: cargo make lint-docs
|
||||
- name: Check conventional commits
|
||||
uses: crate-ci/committed@master
|
||||
with:
|
||||
args: "-vv"
|
||||
commits: HEAD
|
||||
- name: Check typos
|
||||
uses: crate-ci/typos@master
|
||||
- name: Lint dependencies
|
||||
@@ -92,7 +81,7 @@ jobs:
|
||||
- name: Generate coverage
|
||||
run: cargo make coverage
|
||||
- name: Upload to codecov.io
|
||||
uses: codecov/codecov-action@v3
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: true
|
||||
@@ -102,7 +91,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
toolchain: ["1.70.0", "stable"]
|
||||
toolchain: ["1.74.0", "stable"]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -145,7 +134,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
toolchain: ["1.70.0", "stable"]
|
||||
toolchain: ["1.74.0", "stable"]
|
||||
backend: [crossterm, termion, termwiz]
|
||||
exclude:
|
||||
# termion is not supported on windows
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
This document contains a list of breaking changes in each version and some notes to help migrate
|
||||
between versions. It is compiled manually from the commit history and changelog. We also tag PRs on
|
||||
github with a [breaking change] label.
|
||||
GitHub with a [breaking change] label.
|
||||
|
||||
[breaking change]: (https://github.com/ratatui-org/ratatui/issues?q=label%3A%22breaking+change%22)
|
||||
|
||||
@@ -37,7 +37,7 @@ This is a quick summary of the sections below:
|
||||
- `Scrollbar`: symbols moved to `symbols` module
|
||||
- MSRV is now 1.67.0
|
||||
- [v0.22.0](#v0220)
|
||||
- serde representation of `Borders` and `Modifiers` has changed
|
||||
- `serde` representation of `Borders` and `Modifiers` has changed
|
||||
- [v0.21.0](#v0210)
|
||||
- MSRV is now 1.65.0
|
||||
- `terminal::ViewPort` is now an enum
|
||||
@@ -49,19 +49,19 @@ This is a quick summary of the sections below:
|
||||
|
||||
## [v0.26.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.26.0)
|
||||
|
||||
### `Flex::Start` is the new default flex mode for `Layout`
|
||||
### `Flex::Start` is the new default flex mode for `Layout` ([#881])
|
||||
|
||||
[#881]: https://github.com/ratatui-org/ratatui/pull/881
|
||||
|
||||
Previously, constraints would stretch to fill all available space, violating constraints if
|
||||
Previously, constraints would stretch to fill all available space, violating constraints if
|
||||
necessary.
|
||||
|
||||
With v0.26.0, `Flex` modes are introduced and the default is `Flex::Start`, which will align
|
||||
areas associated with constraints to be beginning of the area. With v0.26.0, additionally,
|
||||
`Min` constraints grow to fill excess space. These changes will allow users to build layouts
|
||||
With v0.26.0, `Flex` modes are introduced, and the default is `Flex::Start`, which will align
|
||||
areas associated with constraints to be beginning of the area. With v0.26.0, additionally,
|
||||
`Min` constraints grow to fill excess space. These changes will allow users to build layouts
|
||||
more easily.
|
||||
|
||||
With v0.26.0, users will most likely not need to change what constraints they use to create
|
||||
With v0.26.0, users will most likely not need to change what constraints they use to create
|
||||
existing layouts with `Flex::Start`. However, to get old behavior, use `Flex::Legacy`.
|
||||
|
||||
```diff
|
||||
@@ -108,7 +108,7 @@ by removing the call to `.collect()`.
|
||||
[#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.
|
||||
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)`.
|
||||
@@ -154,7 +154,7 @@ longer can be called from a constant context.
|
||||
[#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
|
||||
itself has a `style` field, which can be set with the `Line::styled` method. Any code that creates
|
||||
`Line`s using the struct initializer instead of constructors will fail to compile due to the added
|
||||
field. This can be easily fixed by adding `..Default::default()` to the field list or by using a
|
||||
constructor method (`Line::styled()`, `Line::raw()`) or conversion method (`Line::from()`).
|
||||
@@ -222,8 +222,8 @@ widget in the default configuration would not show any indication of the selecte
|
||||
|
||||
[#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:
|
||||
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)
|
||||
@@ -281,7 +281,7 @@ let layout = layout::new(Direction::Vertical, [Constraint::Min(1), Constraint::M
|
||||
|
||||
## [v0.24.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.24.0)
|
||||
|
||||
### ScrollbarState field type changed from `u16` to `usize` ([#456])
|
||||
### `ScrollbarState` field type changed from `u16` to `usize` ([#456])
|
||||
|
||||
[#456]: https://github.com/ratatui-org/ratatui/pull/456
|
||||
|
||||
@@ -385,12 +385,12 @@ The MSRV of ratatui is now 1.67 due to an MSRV update in a dependency (`time`).
|
||||
|
||||
## [v0.22.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.22.0)
|
||||
|
||||
### bitflags updated to 2.3 ([#205])
|
||||
### `bitflags` updated to 2.3 ([#205])
|
||||
|
||||
[#205]: https://github.com/ratatui-org/ratatui/issues/205
|
||||
|
||||
The serde representation of bitflags has changed. Any existing serialized types that have Borders or
|
||||
Modifiers will need to be re-serialized. This is documented in the [bitflags
|
||||
The `serde` representation of `bitflags` has changed. Any existing serialized types that have Borders or
|
||||
Modifiers will need to be re-serialized. This is documented in the [`bitflags`
|
||||
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)
|
||||
@@ -422,9 +422,9 @@ 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
|
||||
A new type `Masked` was introduced that implements `From<Text<'a>>`. This causes any code that
|
||||
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.:
|
||||
`to_string()` / `to_owned()` / `as_str()` on the value. E.g.:
|
||||
|
||||
```diff
|
||||
- let paragraph = Paragraph::new("".as_ref());
|
||||
|
||||
626
CHANGELOG.md
626
CHANGELOG.md
@@ -2,7 +2,616 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [0.26.0](https://github.com/ratatui-org/ratatui/releases/tag/0.26.0) - 2024-02-02
|
||||
## [0.26.2](https://github.com/ratatui-org/ratatui/releases/tag/v0.26.2) - 2024-04-15
|
||||
|
||||
This is a patch release that fixes bugs and adds enhancements, including new iterator constructors, List scroll padding, and various rendering improvements. ✨
|
||||
|
||||
✨ **Release highlights**: <https://ratatui.rs/highlights/v0262/>
|
||||
|
||||
### Features
|
||||
|
||||
- [11b452d](https://github.com/ratatui-org/ratatui/commit/11b452d56fe590188ee7a53fa2dde95513b1a4c7)
|
||||
*(layout)* Mark various functions as const by @EdJoPaTo in [#951](https://github.com/ratatui-org/ratatui/pull/951)
|
||||
|
||||
- [1cff511](https://github.com/ratatui-org/ratatui/commit/1cff51193466f5a94d202b6233d56889eccf6d7b)
|
||||
*(line)* Impl Styled for Line by @joshka in [#968](https://github.com/ratatui-org/ratatui/pull/968)
|
||||
|
||||
````text
|
||||
This adds `FromIterator` impls for `Line` and `Text` that allow creating
|
||||
`Line` and `Text` instances from iterators of `Span` and `Line`
|
||||
instances, respectively.
|
||||
|
||||
```rust
|
||||
let line = Line::from_iter(vec!["Hello".blue(), " world!".green()]);
|
||||
let line: Line = iter::once("Hello".blue())
|
||||
.chain(iter::once(" world!".green()))
|
||||
.collect();
|
||||
let text = Text::from_iter(vec!["The first line", "The second line"]);
|
||||
let text: Text = iter::once("The first line")
|
||||
.chain(iter::once("The second line"))
|
||||
.collect();
|
||||
```
|
||||
````
|
||||
|
||||
- [654949b](https://github.com/ratatui-org/ratatui/commit/654949bb00b4522130642f9ad50ab4d9095d921b)
|
||||
*(list)* Add Scroll Padding to Lists by @CameronBarnes in [#958](https://github.com/ratatui-org/ratatui/pull/958)
|
||||
|
||||
````text
|
||||
Introduces scroll padding, which allows the api user to request that a certain number of ListItems be kept visible above and below the currently selected item while scrolling.
|
||||
|
||||
```rust
|
||||
let list = List::new(items).scroll_padding(1);
|
||||
```
|
||||
````
|
||||
|
||||
Fixes:https://github.com/ratatui-org/ratatui/pull/955
|
||||
|
||||
- [26af650](https://github.com/ratatui-org/ratatui/commit/26af65043ee9f165459dec228d12eaeed9997d92)
|
||||
*(text)* Add push methods for text and line by @joshka in [#998](https://github.com/ratatui-org/ratatui/pull/998)
|
||||
|
||||
````text
|
||||
Adds the following methods to the `Text` and `Line` structs:
|
||||
- Text::push_line
|
||||
- Text::push_span
|
||||
- Line::push_span
|
||||
|
||||
This allows for adding lines and spans to a text object without having
|
||||
to call methods on the fields directly, which is useful for incremental
|
||||
construction of text objects.
|
||||
````
|
||||
|
||||
- [b5bdde0](https://github.com/ratatui-org/ratatui/commit/b5bdde079e0e1eda98b9b1bbbba011b770e5b167)
|
||||
*(text)* Add `FromIterator` impls for `Line` and `Text` by @joshka in [#967](https://github.com/ratatui-org/ratatui/pull/967)
|
||||
|
||||
````text
|
||||
This adds `FromIterator` impls for `Line` and `Text` that allow creating
|
||||
`Line` and `Text` instances from iterators of `Span` and `Line`
|
||||
instances, respectively.
|
||||
|
||||
```rust
|
||||
let line = Line::from_iter(vec!["Hello".blue(), " world!".green()]);
|
||||
let line: Line = iter::once("Hello".blue())
|
||||
.chain(iter::once(" world!".green()))
|
||||
.collect();
|
||||
let text = Text::from_iter(vec!["The first line", "The second line"]);
|
||||
let text: Text = iter::once("The first line")
|
||||
.chain(iter::once("The second line"))
|
||||
.collect();
|
||||
```
|
||||
````
|
||||
|
||||
- [12f67e8](https://github.com/ratatui-org/ratatui/commit/12f67e810fad0f907546408192a2380b590ff7bd)
|
||||
*(uncategorized)* Impl Widget for `&str` and `String` by @kdheepak in [#952](https://github.com/ratatui-org/ratatui/pull/952)
|
||||
|
||||
````text
|
||||
Currently, `f.render_widget("hello world".bold(), area)` works but
|
||||
`f.render_widget("hello world", area)` doesn't. This PR changes that my
|
||||
implementing `Widget` for `&str` and `String`. This makes it easier to
|
||||
render strings with no styles as widgets.
|
||||
|
||||
Example usage:
|
||||
|
||||
```rust
|
||||
terminal.draw(|f| f.render_widget("Hello World!", f.size()))?;
|
||||
```
|
||||
|
||||
---------
|
||||
````
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- [0207160](https://github.com/ratatui-org/ratatui/commit/02071607848c51250b4663722c52e19c8ce1c5e2)
|
||||
*(line)* Line truncation respects alignment by @TadoTheMiner in [#987](https://github.com/ratatui-org/ratatui/pull/987)
|
||||
|
||||
````text
|
||||
When rendering a `Line`, the line will be truncated:
|
||||
- on the right for left aligned lines
|
||||
- on the left for right aligned lines
|
||||
- on bot sides for centered lines
|
||||
|
||||
E.g. "Hello World" will be rendered as "Hello", "World", "lo wo" for
|
||||
left, right, centered lines respectively.
|
||||
````
|
||||
|
||||
Fixes:https://github.com/ratatui-org/ratatui/issues/932
|
||||
|
||||
- [c56f49b](https://github.com/ratatui-org/ratatui/commit/c56f49b9fb1c7f1c8c97749119e85f81882ca9a9)
|
||||
*(list)* Saturating_sub to fix highlight_symbol overflow by @mrjackwills in [#949](https://github.com/ratatui-org/ratatui/pull/949)
|
||||
|
||||
````text
|
||||
An overflow (pedantically an underflow) can occur if the
|
||||
highlight_symbol is a multi-byte char, and area is reduced to a size
|
||||
less than that char length.
|
||||
````
|
||||
|
||||
- [b7778e5](https://github.com/ratatui-org/ratatui/commit/b7778e5cd15d0d4b28f7bbb8b3c62950748e333a)
|
||||
*(paragraph)* Unit test typo by @joshka in [#1022](https://github.com/ratatui-org/ratatui/pull/1022)
|
||||
|
||||
- [943c043](https://github.com/ratatui-org/ratatui/commit/943c0431d968a82b23a2f31527f32e57f86f8a7c)
|
||||
*(scrollbar)* Dont render on 0 length track by @EdJoPaTo in [#964](https://github.com/ratatui-org/ratatui/pull/964)
|
||||
|
||||
````text
|
||||
Fixes a panic when `track_length - 1` is used. (clamp panics on `-1.0`
|
||||
being smaller than `0.0`)
|
||||
````
|
||||
|
||||
- [742a5ea](https://github.com/ratatui-org/ratatui/commit/742a5ead066bec14047f6ab7ffa3ac8307eea715)
|
||||
*(text)* Fix panic when rendering out of bounds by @joshka in [#997](https://github.com/ratatui-org/ratatui/pull/997)
|
||||
|
||||
````text
|
||||
Previously it was possible to cause a panic when rendering to an area
|
||||
outside of the buffer bounds. Instead this now correctly renders nothing
|
||||
to the buffer.
|
||||
````
|
||||
|
||||
- [f6c4e44](https://github.com/ratatui-org/ratatui/commit/f6c4e447e65fe10f4fc7fcc9e9c4312acad41096)
|
||||
*(uncategorized)* Ensure that paragraph correctly renders styled text by @joshka in [#992](https://github.com/ratatui-org/ratatui/pull/992)
|
||||
|
||||
````text
|
||||
Paragraph was ignoring the new `Text::style` field added in 0.26.0
|
||||
````
|
||||
|
||||
Fixes:https://github.com/ratatui-org/ratatui/issues/990
|
||||
|
||||
- [35e971f](https://github.com/ratatui-org/ratatui/commit/35e971f7ebb0deadc613b561b15511abd48bdb54)
|
||||
*(uncategorized)* Scrollbar thumb not visible on long lists by @ThomasMiz in [#959](https://github.com/ratatui-org/ratatui/pull/959)
|
||||
|
||||
````text
|
||||
When displaying somewhat-long lists, the `Scrollbar` widget sometimes did not display a thumb character, and only the track will be visible.
|
||||
````
|
||||
|
||||
### Refactor
|
||||
|
||||
- [6fd5f63](https://github.com/ratatui-org/ratatui/commit/6fd5f631bbd58156d9fcae196040bb0248097819)
|
||||
*(lint)* Prefer idiomatic for loops by @EdJoPaTo
|
||||
|
||||
- [37b957c](https://github.com/ratatui-org/ratatui/commit/37b957c7e167a7ecda07b8a60cee5de71efcc55e)
|
||||
*(lints)* Add lints to scrollbar by @EdJoPaTo
|
||||
|
||||
- [c12bcfe](https://github.com/ratatui-org/ratatui/commit/c12bcfefa26529610886040bd96f2b6762436b15)
|
||||
*(non-src)* Apply pedantic lints by @EdJoPaTo in [#976](https://github.com/ratatui-org/ratatui/pull/976)
|
||||
|
||||
````text
|
||||
Fixes many not yet enabled lints (mostly pedantic) on everything that is
|
||||
not the lib (examples, benches, tests). Therefore, this is not containing
|
||||
anything that can be a breaking change.
|
||||
|
||||
Lints are not enabled as that should be the job of #974. I created this
|
||||
as a separate PR as its mostly independent and would only clutter up the
|
||||
diff of #974 even more.
|
||||
|
||||
Also see
|
||||
https://github.com/ratatui-org/ratatui/pull/974#discussion_r1506458743
|
||||
|
||||
---------
|
||||
````
|
||||
|
||||
- [8719608](https://github.com/ratatui-org/ratatui/commit/8719608bdaf32ba92bdfdd60569cf73f7070a618)
|
||||
*(span)* Rename to_aligned_line into into_aligned_line by @EdJoPaTo in [#993](https://github.com/ratatui-org/ratatui/pull/993)
|
||||
|
||||
````text
|
||||
With the Rust method naming conventions these methods are into methods
|
||||
consuming the Span. Therefore, it's more consistent to use `into_`
|
||||
instead of `to_`.
|
||||
|
||||
```rust
|
||||
Span::to_centered_line
|
||||
Span::to_left_aligned_line
|
||||
Span::to_right_aligned_line
|
||||
```
|
||||
|
||||
Are marked deprecated and replaced with the following
|
||||
|
||||
```rust
|
||||
Span::into_centered_line
|
||||
Span::into_left_aligned_line
|
||||
Span::into_right_aligned_line
|
||||
```
|
||||
````
|
||||
|
||||
- [b831c56](https://github.com/ratatui-org/ratatui/commit/b831c5688c6f1fbfa6ae2bcd70d803a54fcf0196)
|
||||
*(widget-ref)* Clippy::needless_pass_by_value by @EdJoPaTo
|
||||
|
||||
- [359204c](https://github.com/ratatui-org/ratatui/commit/359204c9298cc26ea21807d886d596de0329bacc)
|
||||
*(uncategorized)* Simplify to io::Result by @EdJoPaTo in [#1016](https://github.com/ratatui-org/ratatui/pull/1016)
|
||||
|
||||
````text
|
||||
Simplifies the code, logic stays exactly the same.
|
||||
````
|
||||
|
||||
- [8e68db9](https://github.com/ratatui-org/ratatui/commit/8e68db9e2f57fcbf7cb5140006bbbd4dd80bf907)
|
||||
*(uncategorized)* Remove pointless default on internal structs by @EdJoPaTo in [#980](https://github.com/ratatui-org/ratatui/pull/980)
|
||||
|
||||
See #978
|
||||
|
||||
Also remove other derives. They are unused and just slow down
|
||||
compilation.
|
||||
|
||||
- [3be189e](https://github.com/ratatui-org/ratatui/commit/3be189e3c6ebd418d13138ff32bc4a749dc840cf)
|
||||
*(uncategorized)* Clippy::thread_local_initializer_can_be_made_const by @EdJoPaTo
|
||||
|
||||
````text
|
||||
enabled by default on nightly
|
||||
````
|
||||
|
||||
- [5c4efac](https://github.com/ratatui-org/ratatui/commit/5c4efacd1d70bb295d90ffaa73853dc206c187fb)
|
||||
*(uncategorized)* Clippy::map_err_ignore by @EdJoPaTo
|
||||
|
||||
- [bbb6d65](https://github.com/ratatui-org/ratatui/commit/bbb6d65e063df9a74ab6487b2216183c1fdd7230)
|
||||
*(uncategorized)* Clippy::else_if_without_else by @EdJoPaTo
|
||||
|
||||
- [fdb14dc](https://github.com/ratatui-org/ratatui/commit/fdb14dc7cd69788e2ed20709e767f7631b11ffa2)
|
||||
*(uncategorized)* Clippy::redundant_type_annotations by @EdJoPaTo
|
||||
|
||||
- [9b3b23a](https://github.com/ratatui-org/ratatui/commit/9b3b23ac14518a1ef23065d4a5da0fb047b18213)
|
||||
*(uncategorized)* Remove literal suffix by @EdJoPaTo
|
||||
|
||||
````text
|
||||
its not needed and can just be assumed
|
||||
````
|
||||
|
||||
related:clippy::(un)separated_literal_suffix
|
||||
|
||||
- [58b6e0b](https://github.com/ratatui-org/ratatui/commit/58b6e0be0f4db3d90005e130e4b84cd865179785)
|
||||
*(uncategorized)* Clippy::should_panic_without_expect by @EdJoPaTo
|
||||
|
||||
- [c870a41](https://github.com/ratatui-org/ratatui/commit/c870a41057ac0c14c2e72e762b37689dc32e7b23)
|
||||
*(uncategorized)* Clippy::many_single_char_names by @EdJoPaTo
|
||||
|
||||
- [a6036ad](https://github.com/ratatui-org/ratatui/commit/a6036ad78911653407f607f5efa556a055d3dce9)
|
||||
*(uncategorized)* Clippy::similar_names by @EdJoPaTo
|
||||
|
||||
- [060d26b](https://github.com/ratatui-org/ratatui/commit/060d26b6dc6e1027dbf46ae98b0ebba83701f941)
|
||||
*(uncategorized)* Clippy::match_same_arms by @EdJoPaTo
|
||||
|
||||
- [fcbea9e](https://github.com/ratatui-org/ratatui/commit/fcbea9ee68591344a29a7b2e83f1c8c878857aeb)
|
||||
*(uncategorized)* Clippy::uninlined_format_args by @EdJoPaTo
|
||||
|
||||
- [14b24e7](https://github.com/ratatui-org/ratatui/commit/14b24e75858af48f39d5880e7f6c9adeac1b1da9)
|
||||
*(uncategorized)* Clippy::if_not_else by @EdJoPaTo
|
||||
|
||||
- [5ed1f43](https://github.com/ratatui-org/ratatui/commit/5ed1f43c627053f25d9ee711677ebec6cb8fcd85)
|
||||
*(uncategorized)* Clippy::redundant_closure_for_method_calls by @EdJoPaTo
|
||||
|
||||
- [c8c7924](https://github.com/ratatui-org/ratatui/commit/c8c7924e0ca84351f5ed5c54e79611ce16d4dc37)
|
||||
*(uncategorized)* Clippy::too_many_lines by @EdJoPaTo
|
||||
|
||||
- [e3afe7c](https://github.com/ratatui-org/ratatui/commit/e3afe7c8a14c1cffd7de50782a7acf0f95f41673)
|
||||
*(uncategorized)* Clippy::unreadable_literal by @EdJoPaTo
|
||||
|
||||
- [a1f54de](https://github.com/ratatui-org/ratatui/commit/a1f54de7d60fa6c57be29bf8f02a675e58b7b9c2)
|
||||
*(uncategorized)* Clippy::bool_to_int_with_if by @EdJoPaTo
|
||||
|
||||
- [b8ea190](https://github.com/ratatui-org/ratatui/commit/b8ea190bf2cde8c18e2ac8276d2eb57d219db263)
|
||||
*(uncategorized)* Clippy::cast_lossless by @EdJoPaTo
|
||||
|
||||
- [0de5238](https://github.com/ratatui-org/ratatui/commit/0de5238ed3613f2d663f5e9628ca7b2aa205ed02)
|
||||
*(uncategorized)* Dead_code by @EdJoPaTo
|
||||
|
||||
````text
|
||||
enabled by default, only detected by nightly yet
|
||||
````
|
||||
|
||||
- [df5dddf](https://github.com/ratatui-org/ratatui/commit/df5dddfbc9c679d15a5a90ea79bb1f8946d5cb9c)
|
||||
*(uncategorized)* Unused_imports by @EdJoPaTo
|
||||
|
||||
````text
|
||||
enabled by default, only detected on nightly yet
|
||||
````
|
||||
|
||||
- [f1398ae](https://github.com/ratatui-org/ratatui/commit/f1398ae6cb1abd32106923d64844b482c7ba6f82)
|
||||
*(uncategorized)* Clippy::useless_vec by @EdJoPaTo
|
||||
|
||||
````text
|
||||
Lint enabled by default but only nightly finds this yet
|
||||
````
|
||||
|
||||
- [525848f](https://github.com/ratatui-org/ratatui/commit/525848ff4e066526d402fecf1d5b9c63cff1f22a)
|
||||
*(uncategorized)* Manually apply clippy::use_self for impl with lifetimes by @EdJoPaTo
|
||||
|
||||
- [660c718](https://github.com/ratatui-org/ratatui/commit/660c7183c7a10dc453d80dfb651d9534536960b9)
|
||||
*(uncategorized)* Clippy::empty_line_after_doc_comments by @EdJoPaTo
|
||||
|
||||
- [ab951fa](https://github.com/ratatui-org/ratatui/commit/ab951fae8166c9321728ba942b48552dfe4d9c55)
|
||||
*(uncategorized)* Clippy::return_self_not_must_use by @EdJoPaTo
|
||||
|
||||
- [3cd4369](https://github.com/ratatui-org/ratatui/commit/3cd436917649a93b4b80d0c4a0343284e0585522)
|
||||
*(uncategorized)* Clippy::doc_markdown by @EdJoPaTo
|
||||
|
||||
- [9bc014d](https://github.com/ratatui-org/ratatui/commit/9bc014d7f16efdb70fcd6b6b786fe74eac7b9bdf)
|
||||
*(uncategorized)* Clippy::items_after_statements by @EdJoPaTo
|
||||
|
||||
- [36a0cd5](https://github.com/ratatui-org/ratatui/commit/36a0cd56e5645533a1d6c2720536fa10a56b0d40)
|
||||
*(uncategorized)* Clippy::deref_by_slicing by @EdJoPaTo
|
||||
|
||||
- [f7f6692](https://github.com/ratatui-org/ratatui/commit/f7f66928a8833532a3bc97292665640285e7aafa)
|
||||
*(uncategorized)* Clippy::equatable_if_let by @EdJoPaTo
|
||||
|
||||
- [01418eb](https://github.com/ratatui-org/ratatui/commit/01418eb7c2e1874cb4070828c485d81ea171b18d)
|
||||
*(uncategorized)* Clippy::default_trait_access by @EdJoPaTo
|
||||
|
||||
- [8536760](https://github.com/ratatui-org/ratatui/commit/8536760e7802a498f7c6d9fe8fb4c7920a1c6e71)
|
||||
*(uncategorized)* Clippy::inefficient_to_string by @EdJoPaTo
|
||||
|
||||
- [a558b19](https://github.com/ratatui-org/ratatui/commit/a558b19c9a7b90a1ed3f309301f49f0b483e02ec)
|
||||
*(uncategorized)* Clippy::implicit_clone by @EdJoPaTo
|
||||
|
||||
- [5b00e3a](https://github.com/ratatui-org/ratatui/commit/5b00e3aae98cb5c20c10bec944948a75ac83f956)
|
||||
*(uncategorized)* Clippy::use_self by @EdJoPaTo
|
||||
|
||||
- [27680c0](https://github.com/ratatui-org/ratatui/commit/27680c05ce1670f026ad23c446ada321c1c755f0)
|
||||
*(uncategorized)* Clippy::semicolon_if_nothing_returned by @EdJoPaTo
|
||||
|
||||
### Documentation
|
||||
|
||||
- [14461c3](https://github.com/ratatui-org/ratatui/commit/14461c3a3554c95905ebca433fc3d4dae1e1acda)
|
||||
*(breaking-changes)* Typos and markdownlint by @EdJoPaTo in [#1009](https://github.com/ratatui-org/ratatui/pull/1009)
|
||||
|
||||
- [d0067c8](https://github.com/ratatui-org/ratatui/commit/d0067c8815d5244d319934d58a9366c8ad36b3e5)
|
||||
*(license)* Update copyright years by @orhun in [#962](https://github.com/ratatui-org/ratatui/pull/962)
|
||||
|
||||
- [88bfb5a](https://github.com/ratatui-org/ratatui/commit/88bfb5a43027cf3410ad560772c5bfdbaa3d58b7)
|
||||
*(text)* Update Text and Line docs by @joshka in [#969](https://github.com/ratatui-org/ratatui/pull/969)
|
||||
|
||||
- [3b002fd](https://github.com/ratatui-org/ratatui/commit/3b002fdcab964ce3f65f55dc8053d9678ae247a3)
|
||||
*(uncategorized)* Update incompatible code warning in examples readme by @joshka in [#1013](https://github.com/ratatui-org/ratatui/pull/1013)
|
||||
|
||||
### Performance
|
||||
|
||||
- [e02f476](https://github.com/ratatui-org/ratatui/commit/e02f4768ce2ee30473200fe98e2687e42acb9c33)
|
||||
*(borders)* Allow border!() in const by @EdJoPaTo in [#977](https://github.com/ratatui-org/ratatui/pull/977)
|
||||
|
||||
````text
|
||||
This allows more compiler optimizations when the macro is used.
|
||||
````
|
||||
|
||||
- [541f0f9](https://github.com/ratatui-org/ratatui/commit/541f0f99538762a07d68a71b2989ecc6ff6f71ef)
|
||||
*(cell)* Use const CompactString::new_inline by @EdJoPaTo in [#979](https://github.com/ratatui-org/ratatui/pull/979)
|
||||
|
||||
````text
|
||||
Some minor find when messing around trying to `const` all the things.
|
||||
|
||||
While `reset()` and `default()` can not be `const` it's still a benefit
|
||||
when their contents are.
|
||||
````
|
||||
|
||||
- [65e7923](https://github.com/ratatui-org/ratatui/commit/65e792375396c3160d76964ef0dfc4fb1e53be41)
|
||||
*(scrollbar)* Const creation by @EdJoPaTo in [#963](https://github.com/ratatui-org/ratatui/pull/963)
|
||||
|
||||
````text
|
||||
A bunch of `const fn` allow for more performance and `Default` now uses the `const` new implementations.
|
||||
````
|
||||
|
||||
- [8195f52](https://github.com/ratatui-org/ratatui/commit/8195f526cb4b321f337dcbe9e689cc7f6eb84065)
|
||||
*(uncategorized)* Clippy::needless_pass_by_value by @EdJoPaTo
|
||||
|
||||
- [183c07e](https://github.com/ratatui-org/ratatui/commit/183c07ef436cbb8fb0bec418042b44b4fedd836f)
|
||||
*(uncategorized)* Clippy::trivially_copy_pass_by_ref by @EdJoPaTo
|
||||
|
||||
- [a13867f](https://github.com/ratatui-org/ratatui/commit/a13867ffceb2f8f57f4540049754c2f916fd3efc)
|
||||
*(uncategorized)* Clippy::cloned_instead_of_copied by @EdJoPaTo
|
||||
|
||||
- [3834374](https://github.com/ratatui-org/ratatui/commit/3834374652b46c5ddbfedcf8dea2086fd762f884)
|
||||
*(uncategorized)* Clippy::missing_const_for_fn by @EdJoPaTo
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- [125ee92](https://github.com/ratatui-org/ratatui/commit/125ee929ee9009b97a270e2e105a3f1167ab13d7)
|
||||
*(docs)* Fix: fix typos in crate documentation by @orhun in [#1002](https://github.com/ratatui-org/ratatui/pull/1002)
|
||||
|
||||
- [38c17e0](https://github.com/ratatui-org/ratatui/commit/38c17e091cf3f4de2d196ecdd6a40129019eafc4)
|
||||
*(editorconfig)* Set and apply some defaults by @EdJoPaTo
|
||||
|
||||
- [07da90a](https://github.com/ratatui-org/ratatui/commit/07da90a7182035b24f870bcbf0a0ffaad75eb48b)
|
||||
*(funding)* Add eth address for receiving funds from drips.network by @BenJam in [#994](https://github.com/ratatui-org/ratatui/pull/994)
|
||||
|
||||
- [078e97e](https://github.com/ratatui-org/ratatui/commit/078e97e4ff65c02afa7c884914ecd38a6e959b58)
|
||||
*(github)* Add EdJoPaTo as a maintainer by @orhun in [#986](https://github.com/ratatui-org/ratatui/pull/986)
|
||||
|
||||
- [b0314c5](https://github.com/ratatui-org/ratatui/commit/b0314c5731b32f51f5b6ca71a5194c6d7f265972)
|
||||
*(uncategorized)* Remove conventional commit check for PR by @Valentin271 in [#950](https://github.com/ratatui-org/ratatui/pull/950)
|
||||
|
||||
````text
|
||||
This removes conventional commit check for PRs.
|
||||
|
||||
Since we use the PR title and description this is useless. It fails a
|
||||
lot of time and we ignore it.
|
||||
|
||||
IMPORTANT NOTE: This does **not** mean Ratatui abandons conventional
|
||||
commits. This only relates to commits in PRs.
|
||||
````
|
||||
|
||||
### Build
|
||||
|
||||
- [6e6ba27](https://github.com/ratatui-org/ratatui/commit/6e6ba27a122560bcf47b0efd20b7095f1bfd8714)
|
||||
*(lint)* Warn on pedantic and allow the rest by @EdJoPaTo
|
||||
|
||||
- [c4ce7e8](https://github.com/ratatui-org/ratatui/commit/c4ce7e8ff6f00875e1ead5b68052f0db737bd44d)
|
||||
*(uncategorized)* Enable more satisfied lints by @EdJoPaTo
|
||||
|
||||
````text
|
||||
These lints dont generate warnings and therefore dont need refactoring.
|
||||
I think they are useful in the future.
|
||||
````
|
||||
|
||||
- [a4e84a6](https://github.com/ratatui-org/ratatui/commit/a4e84a6a7f6f5b80903799028f30e2a4438f2807)
|
||||
*(uncategorized)* Increase msrv to 1.74.0 by @EdJoPaTo [**breaking**]
|
||||
|
||||
````text
|
||||
configure lints in Cargo.toml requires 1.74.0
|
||||
````
|
||||
|
||||
BREAKING CHANGE:rust 1.74 is required now
|
||||
|
||||
### New Contributors
|
||||
|
||||
* @TadoTheMiner made their first contribution in [#987](https://github.com/ratatui-org/ratatui/pull/987)
|
||||
* @BenJam made their first contribution in [#994](https://github.com/ratatui-org/ratatui/pull/994)
|
||||
* @CameronBarnes made their first contribution in [#958](https://github.com/ratatui-org/ratatui/pull/958)
|
||||
* @ThomasMiz made their first contribution in [#959](https://github.com/ratatui-org/ratatui/pull/959)
|
||||
|
||||
**Full Changelog**: https://github.com/ratatui-org/ratatui/compare/v0.26.1...0.26.2
|
||||
|
||||
## [0.26.1](https://github.com/ratatui-org/ratatui/releases/tag/v0.26.1) - 2024-02-12
|
||||
|
||||
This is a patch release that fixes bugs and adds enhancements, including new iterators, title options for blocks, and various rendering improvements. ✨
|
||||
|
||||
### Features
|
||||
|
||||
- [74a0511](https://github.com/ratatui-org/ratatui/commit/74a051147a4059990c31e08d96a8469d8220537b)
|
||||
*(rect)* Add Rect::positions iterator ([#928](https://github.com/ratatui-org/ratatui/issues/928))
|
||||
|
||||
````text
|
||||
Useful for performing some action on all the cells in a particular area.
|
||||
E.g.,
|
||||
|
||||
```rust
|
||||
fn render(area: Rect, buf: &mut Buffer) {
|
||||
for position in area.positions() {
|
||||
buf.get_mut(position.x, position.y).set_symbol("x");
|
||||
}
|
||||
}
|
||||
```
|
||||
````
|
||||
|
||||
- [9182f47](https://github.com/ratatui-org/ratatui/commit/9182f47026d1630cb749163b6f8b8987474312ae)
|
||||
*(uncategorized)* Add Block::title_top and Block::title_top_bottom ([#940](https://github.com/ratatui-org/ratatui/issues/940))
|
||||
|
||||
````text
|
||||
This adds the ability to add titles to the top and bottom of a block
|
||||
without having to use the `Title` struct (which will be removed in a
|
||||
future release - likely v0.28.0).
|
||||
|
||||
Fixes a subtle bug if the title was created from a right aligned Line
|
||||
and was also right aligned. The title would be rendered one cell too far
|
||||
to the right.
|
||||
|
||||
```rust
|
||||
Block::bordered()
|
||||
.title_top(Line::raw("A").left_aligned())
|
||||
.title_top(Line::raw("B").centered())
|
||||
.title_top(Line::raw("C").right_aligned())
|
||||
.title_bottom(Line::raw("D").left_aligned())
|
||||
.title_bottom(Line::raw("E").centered())
|
||||
.title_bottom(Line::raw("F").right_aligned())
|
||||
.render(buffer.area, &mut buffer);
|
||||
// renders
|
||||
"┌A─────B─────C┐",
|
||||
"│ │",
|
||||
"└D─────E─────F┘",
|
||||
```
|
||||
|
||||
Addresses part of https://github.com/ratatui-org/ratatui/issues/738
|
||||
````
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- [2202059](https://github.com/ratatui-org/ratatui/commit/220205925911ed4377358d2a28ffca9373f11bda)
|
||||
*(block)* Fix crash on empty right aligned title ([#933](https://github.com/ratatui-org/ratatui/issues/933))
|
||||
|
||||
````text
|
||||
- Simplified implementation of the rendering for block.
|
||||
- Introduces a subtle rendering change where centered titles that are
|
||||
odd in length will now be rendered one character to the left compared
|
||||
to before. This aligns with other places that we render centered text
|
||||
and is a more consistent behavior. See
|
||||
https://github.com/ratatui-org/ratatui/pull/807#discussion_r1455645954
|
||||
for another example of this.
|
||||
````
|
||||
|
||||
Fixes: https://github.com/ratatui-org/ratatui/pull/929
|
||||
|
||||
- [14c67fb](https://github.com/ratatui-org/ratatui/commit/14c67fbb52101d10b2d2e26898c408ab8dd3ec2d)
|
||||
*(list)* Highlight symbol when using a multi-bytes char ([#924](https://github.com/ratatui-org/ratatui/issues/924))
|
||||
|
||||
````text
|
||||
ratatui v0.26.0 brought a regression in the List widget, in which the
|
||||
highlight symbol width was incorrectly calculated - specifically when
|
||||
the highlight symbol was a multi-char character, e.g. `▶`.
|
||||
````
|
||||
|
||||
- [0dcdbea](https://github.com/ratatui-org/ratatui/commit/0dcdbea083aace6d531c0d505837e0911f400675)
|
||||
*(paragraph)* Render Line::styled correctly inside a paragraph ([#930](https://github.com/ratatui-org/ratatui/issues/930))
|
||||
|
||||
````text
|
||||
Renders the styled graphemes of the line instead of the contained spans.
|
||||
````
|
||||
|
||||
- [fae5862](https://github.com/ratatui-org/ratatui/commit/fae5862c6e0947ee1488a7e4775413dbead67c8b)
|
||||
*(uncategorized)* Ensure that buffer::set_line sets the line style ([#926](https://github.com/ratatui-org/ratatui/issues/926))
|
||||
|
||||
````text
|
||||
Fixes a regression in 0.26 where buffer::set_line was no longer setting
|
||||
the style. This was due to the new style field on Line instead of being
|
||||
stored only in the spans.
|
||||
|
||||
Also adds a configuration for just running unit tests to bacon.toml.
|
||||
````
|
||||
|
||||
- [fbb5dfa](https://github.com/ratatui-org/ratatui/commit/fbb5dfaaa903efde0e63114c393dc3063d5f56fd)
|
||||
*(uncategorized)* Scrollbar rendering when no track symbols are provided ([#911](https://github.com/ratatui-org/ratatui/issues/911))
|
||||
|
||||
### Refactor
|
||||
|
||||
- [c3fb258](https://github.com/ratatui-org/ratatui/commit/c3fb25898f3e3ffe485ee69631b680679874d2cb)
|
||||
*(rect)* Move iters to module and add docs ([#927](https://github.com/ratatui-org/ratatui/issues/927))
|
||||
|
||||
- [e51ca6e](https://github.com/ratatui-org/ratatui/commit/e51ca6e0d2705e6e0a96aeee78f1e80fcaaf34fc)
|
||||
*(uncategorized)* Finish tidying up table ([#942](https://github.com/ratatui-org/ratatui/issues/942))
|
||||
|
||||
- [91040c0](https://github.com/ratatui-org/ratatui/commit/91040c0865043b8d5e7387509523a41345ed5af3)
|
||||
*(uncategorized)* Rearrange block structure ([#939](https://github.com/ratatui-org/ratatui/issues/939))
|
||||
|
||||
### Documentation
|
||||
|
||||
- [61a8278](https://github.com/ratatui-org/ratatui/commit/61a827821dff2bd733377cfc143266edce1dbeec)
|
||||
*(canvas)* Add documentation to canvas module ([#913](https://github.com/ratatui-org/ratatui/issues/913))
|
||||
|
||||
````text
|
||||
Document the whole `canvas` module. With this, the whole `widgets`
|
||||
module is documented.
|
||||
````
|
||||
|
||||
- [d2d91f7](https://github.com/ratatui-org/ratatui/commit/d2d91f754c87458c6d07863eca20f3ea8ae319ce)
|
||||
*(changelog)* Add sponsors section ([#908](https://github.com/ratatui-org/ratatui/issues/908))
|
||||
|
||||
- [410d08b](https://github.com/ratatui-org/ratatui/commit/410d08b2b5812d7e29302adc0e8ddf18eb7d1d26)
|
||||
*(uncategorized)* Add link to FOSDEM 2024 talk ([#944](https://github.com/ratatui-org/ratatui/issues/944))
|
||||
|
||||
- [1f208ff](https://github.com/ratatui-org/ratatui/commit/1f208ffd0368b4d269854dc0c550686dcd2d1de0)
|
||||
*(uncategorized)* Add GitHub Sponsors badge ([#943](https://github.com/ratatui-org/ratatui/issues/943))
|
||||
|
||||
### Performance
|
||||
|
||||
- [0963463](https://github.com/ratatui-org/ratatui/commit/096346350e19c5de9a4d74bba64796997e9f40da)
|
||||
*(uncategorized)* Use drain instead of remove in chart examples ([#922](https://github.com/ratatui-org/ratatui/issues/922))
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- [a4892ad](https://github.com/ratatui-org/ratatui/commit/a4892ad444739d7a760bc45bbd954e728c66b2d2)
|
||||
*(uncategorized)* Fix typo in docsrs example ([#946](https://github.com/ratatui-org/ratatui/issues/946))
|
||||
|
||||
- [18870ce](https://github.com/ratatui-org/ratatui/commit/18870ce99063a492674de061441b2cce5dc54c60)
|
||||
*(uncategorized)* Fix the method name for setting the Line style ([#947](https://github.com/ratatui-org/ratatui/issues/947))
|
||||
|
||||
- [8fb4630](https://github.com/ratatui-org/ratatui/commit/8fb46301a00b5d065f9b890496f914d3fdc17495)
|
||||
*(uncategorized)* Remove github action bot that makes comments nudging commit signing ([#937](https://github.com/ratatui-org/ratatui/issues/937))
|
||||
|
||||
````text
|
||||
We can consider reverting this commit once this PR is merged:
|
||||
https://github.com/1Password/check-signed-commits-action/pull/9
|
||||
````
|
||||
|
||||
### 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!
|
||||
|
||||
* @mo8it
|
||||
* @m4rch3n1ng
|
||||
|
||||
## [0.26.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.26.0) - 2024-02-02
|
||||
|
||||
We are excited to announce the new version of `ratatui` - a Rust library that's all about cooking up TUIs 🐭
|
||||
|
||||
@@ -1648,7 +2257,18 @@ Here is the list of contributors who have contributed to `ratatui` for the first
|
||||
* @bblsh
|
||||
* @Eeelco
|
||||
|
||||
## [0.25.0](https://github.com/ratatui-org/ratatui/releases/tag/0.25.0) - 2023-12-18
|
||||
### Sponsors
|
||||
|
||||
Shout out to our new sponsors!
|
||||
|
||||
* @pythops
|
||||
* @DanNixon
|
||||
* @ymgyt
|
||||
* @plabayo
|
||||
* @atuinsh
|
||||
* @JeftavanderHorst!
|
||||
|
||||
## [0.25.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.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 🐭
|
||||
|
||||
@@ -2133,7 +2753,7 @@ Here is the list of contributors who have contributed to `ratatui` for the first
|
||||
* @YeungKC
|
||||
* @lyuha
|
||||
|
||||
## [0.24.0](https://github.com/ratatui-org/ratatui/releases/tag/0.24.0) - 2023-10-23
|
||||
## [0.24.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.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 🐭
|
||||
|
||||
|
||||
@@ -112,7 +112,8 @@ exist to show coverage directly in your editor. E.g.:
|
||||
|
||||
### Documentation
|
||||
|
||||
Here are some guidelines for writing documentation in Ratatui.
|
||||
Here are some guidelines for writing documentation in Ratatui.
|
||||
|
||||
Every public API **must** be documented.
|
||||
|
||||
Keep in mind that Ratatui tends to attract beginner Rust users that may not be familiar with Rust
|
||||
@@ -125,10 +126,9 @@ the concepts pointing to the various methods. Focus on interaction with various
|
||||
enough information that helps understand why you might want something.
|
||||
|
||||
Examples should help users understand a particular usage, not test a feature. They should be as
|
||||
simple as possible.
|
||||
Prefer hiding imports and using wildcards to keep things concise. Some imports may still be shown
|
||||
to demonstrate a particular non-obvious import (e.g. `Stylize` trait to use style methods).
|
||||
Speaking of `Stylize`, you should use it over the more verbose style setters:
|
||||
simple as possible. Prefer hiding imports and using wildcards to keep things concise. Some imports
|
||||
may still be shown to demonstrate a particular non-obvious import (e.g. `Stylize` trait to use style
|
||||
methods). Speaking of `Stylize`, you should use it over the more verbose style setters:
|
||||
|
||||
```rust
|
||||
let style = Style::new().red().bold();
|
||||
@@ -138,7 +138,7 @@ let style = Style::default().fg(Color::Red).add_modifier(Modifiers::BOLD);
|
||||
|
||||
#### Format
|
||||
|
||||
- First line is summary, second is blank, third onward is more detail
|
||||
- First line is summary, second is blank, third onward is more detail
|
||||
|
||||
```rust
|
||||
/// Summary
|
||||
@@ -148,10 +148,10 @@ let style = Style::default().fg(Color::Red).add_modifier(Modifiers::BOLD);
|
||||
fn foo() {}
|
||||
```
|
||||
|
||||
- Max line length is 100 characters
|
||||
- Max line length is 100 characters
|
||||
See [vscode rewrap extension](https://marketplace.visualstudio.com/items?itemName=stkb.rewrap)
|
||||
|
||||
- Doc comments are above macros
|
||||
- Doc comments are above macros
|
||||
i.e.
|
||||
|
||||
```rust
|
||||
@@ -160,7 +160,7 @@ i.e.
|
||||
struct Foo {}
|
||||
```
|
||||
|
||||
- Code items should be between backticks
|
||||
- Code items should be between backticks
|
||||
i.e. ``[`Block`]``, **NOT** ``[Block]``
|
||||
|
||||
### Deprecation notice
|
||||
|
||||
71
Cargo.toml
71
Cargo.toml
@@ -1,11 +1,13 @@
|
||||
[package]
|
||||
name = "ratatui"
|
||||
version = "0.26.0" # crate version
|
||||
version = "0.26.2" # 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/"
|
||||
keywords = ["tui", "terminal", "dashboard"]
|
||||
repository = "https://github.com/ratatui-org/ratatui"
|
||||
homepage = "https://ratatui.rs"
|
||||
keywords = ["tui", "terminal", "dashboard"]
|
||||
categories = ["command-line-interface"]
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
exclude = [
|
||||
@@ -18,29 +20,28 @@ exclude = [
|
||||
]
|
||||
autoexamples = true
|
||||
edition = "2021"
|
||||
rust-version = "1.70.0"
|
||||
rust-version = "1.74.0"
|
||||
|
||||
[badges]
|
||||
|
||||
[dependencies]
|
||||
crossterm = { version = "0.27", optional = true }
|
||||
termion = { version = "3.0", optional = true }
|
||||
termwiz = { version = "0.22.0", optional = true }
|
||||
|
||||
serde = { version = "1", optional = true, features = ["derive"] }
|
||||
bitflags = "2.3"
|
||||
cassowary = "0.3"
|
||||
indoc = "2.0"
|
||||
compact_str = "0.7.1"
|
||||
crossterm = { version = "0.27", optional = true }
|
||||
document-features = { version = "0.2.7", optional = true }
|
||||
itertools = "0.12"
|
||||
lru = "0.12.0"
|
||||
paste = "1.0.2"
|
||||
serde = { version = "1", optional = true, features = ["derive"] }
|
||||
stability = "0.2.0"
|
||||
strum = { version = "0.26", features = ["derive"] }
|
||||
termion = { version = "3.0", optional = true }
|
||||
termwiz = { version = "0.22.0", optional = true }
|
||||
time = { version = "0.3.11", optional = true, features = ["local-offset"] }
|
||||
unicode-segmentation = "1.10"
|
||||
unicode-truncate = "1"
|
||||
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"
|
||||
@@ -51,16 +52,52 @@ cargo-husky = { version = "1.5.0", default-features = false, features = [
|
||||
] }
|
||||
color-eyre = "0.6.2"
|
||||
criterion = { version = "0.5.1", features = ["html_reports"] }
|
||||
derive_builder = "0.13.0"
|
||||
derive_builder = "0.20.0"
|
||||
fakeit = "1.1"
|
||||
font8x8 = "0.3.1"
|
||||
indoc = "2"
|
||||
palette = "0.7.3"
|
||||
pretty_assertions = "1.4.0"
|
||||
rand = "0.8.5"
|
||||
rand_chacha = "0.3.1"
|
||||
rstest = "0.18.2"
|
||||
rstest = "0.19.0"
|
||||
serde_json = "1.0.109"
|
||||
|
||||
[lints.rust]
|
||||
unsafe_code = "forbid"
|
||||
|
||||
[lints.clippy]
|
||||
cargo = { level = "warn", priority = -1 }
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
cast_possible_truncation = "allow"
|
||||
cast_possible_wrap = "allow"
|
||||
cast_precision_loss = "allow"
|
||||
cast_sign_loss = "allow"
|
||||
missing_errors_doc = "allow"
|
||||
missing_panics_doc = "allow"
|
||||
module_name_repetitions = "allow"
|
||||
must_use_candidate = "allow"
|
||||
wildcard_imports = "allow"
|
||||
|
||||
# nursery or restricted
|
||||
as_underscore = "warn"
|
||||
deref_by_slicing = "warn"
|
||||
else_if_without_else = "warn"
|
||||
empty_line_after_doc_comments = "warn"
|
||||
equatable_if_let = "warn"
|
||||
fn_to_numeric_cast_any = "warn"
|
||||
format_push_string = "warn"
|
||||
map_err_ignore = "warn"
|
||||
missing_const_for_fn = "warn"
|
||||
mixed_read_write_in_expression = "warn"
|
||||
mod_module_files = "warn"
|
||||
needless_raw_strings = "warn"
|
||||
redundant_type_annotations = "warn"
|
||||
rest_pat_in_fully_bound_structs = "warn"
|
||||
string_lit_chars_any = "warn"
|
||||
string_to_string = "warn"
|
||||
use_self = "warn"
|
||||
|
||||
[features]
|
||||
#! The crate provides a set of optional features that can be enabled in your `cargo.toml` file.
|
||||
#!
|
||||
@@ -126,6 +163,10 @@ harness = false
|
||||
name = "block"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "line"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "list"
|
||||
harness = false
|
||||
|
||||
7
FUNDING.json
Normal file
7
FUNDING.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"drips": {
|
||||
"ethereum": {
|
||||
"ownedBy": "0x6053C8984f4F214Ad12c4653F28514E1E09213B5"
|
||||
}
|
||||
}
|
||||
}
|
||||
2
LICENSE
2
LICENSE
@@ -1,7 +1,7 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016-2022 Florian Dehau
|
||||
Copyright (c) 2023 The Ratatui Developers
|
||||
Copyright (c) 2023-2024 The Ratatui Developers
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -7,11 +7,12 @@ skip_core_tasks = true
|
||||
# all features except the backend ones
|
||||
ALL_FEATURES = "all-widgets,macros,serde"
|
||||
|
||||
[env.ALL_FEATURES_FLAG]
|
||||
# Windows does not support building termion, so this avoids the build failure by providing two
|
||||
# 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,unstable", mapping = { "windows" = "--features=all-widgets,macros,serde,crossterm,termwiz,unstable" } }
|
||||
source = "${CARGO_MAKE_RUST_TARGET_OS}"
|
||||
default_value = "--features=all-widgets,macros,serde,crossterm,termion,termwiz,underline-color,unstable"
|
||||
mapping = { "windows" = "--features=all-widgets,macros,serde,crossterm,termwiz,underline-color,unstable" }
|
||||
|
||||
[tasks.default]
|
||||
alias = "ci"
|
||||
|
||||
29
README.md
29
README.md
@@ -26,7 +26,7 @@
|
||||
<div align="center">
|
||||
|
||||
[![Crate Badge]][Crate] [![Docs Badge]][API Docs] [![CI Badge]][CI Workflow] [![License
|
||||
Badge]](./LICENSE)<br>
|
||||
Badge]](./LICENSE) [![Sponsors Badge]][GitHub Sponsors]<br>
|
||||
[![Codecov Badge]][Codecov] [![Deps.rs Badge]][Deps.rs] [![Discord Badge]][Discord Server]
|
||||
[![Matrix Badge]][Matrix]<br>
|
||||
|
||||
@@ -61,6 +61,9 @@ This is in contrast to the retained mode style of rendering where widgets are up
|
||||
automatically redrawn on the next frame. See the [Rendering] section of the [Ratatui Website]
|
||||
for more info.
|
||||
|
||||
You can also watch the [FOSDEM 2024 talk] about Ratatui which gives a brief introduction to
|
||||
terminal user interfaces and showcases the features of Ratatui, along with a hello world demo.
|
||||
|
||||
## Other documentation
|
||||
|
||||
- [Ratatui Website] - explains the library's concepts and provides step-by-step tutorials
|
||||
@@ -159,7 +162,7 @@ fn handle_events() -> io::Result<bool> {
|
||||
fn ui(frame: &mut Frame) {
|
||||
frame.render_widget(
|
||||
Paragraph::new("Hello World!")
|
||||
.block(Block::default().title("Greeting").borders(Borders::ALL)),
|
||||
.block(Block::bordered().title("Greeting")),
|
||||
frame.size(),
|
||||
);
|
||||
}
|
||||
@@ -204,11 +207,11 @@ fn ui(frame: &mut Frame) {
|
||||
)
|
||||
.split(main_layout[1]);
|
||||
frame.render_widget(
|
||||
Block::default().borders(Borders::ALL).title("Left"),
|
||||
Block::bordered().title("Left"),
|
||||
inner_layout[0],
|
||||
);
|
||||
frame.render_widget(
|
||||
Block::default().borders(Borders::ALL).title("Right"),
|
||||
Block::bordered().title("Right"),
|
||||
inner_layout[1],
|
||||
);
|
||||
}
|
||||
@@ -301,6 +304,7 @@ Running this example produces the following output:
|
||||
[Changelog]: https://github.com/ratatui-org/ratatui/blob/main/CHANGELOG.md
|
||||
[Contributing]: https://github.com/ratatui-org/ratatui/blob/main/CONTRIBUTING.md
|
||||
[Breaking Changes]: https://github.com/ratatui-org/ratatui/blob/main/BREAKING-CHANGES.md
|
||||
[FOSDEM 2024 talk]: https://www.youtube.com/watch?v=NU0q6NOLJ20
|
||||
[docsrs-hello]: https://github.com/ratatui-org/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-hello.png?raw=true
|
||||
[docsrs-layout]: https://github.com/ratatui-org/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-layout.png?raw=true
|
||||
[docsrs-styling]: https://github.com/ratatui-org/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-styling.png?raw=true
|
||||
@@ -322,23 +326,24 @@ Running this example produces the following output:
|
||||
[Termion]: https://crates.io/crates/termion
|
||||
[Termwiz]: https://crates.io/crates/termwiz
|
||||
[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
|
||||
[GitHub Sponsors]: https://github.com/sponsors/ratatui-org
|
||||
[Crate Badge]: https://img.shields.io/crates/v/ratatui?logo=rust&style=flat-square&logoColor=E05D44&color=E05D44
|
||||
[License Badge]: https://img.shields.io/crates/l/ratatui?style=flat-square&color=1370D3
|
||||
[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
|
||||
https://img.shields.io/codecov/c/github/ratatui-org/ratatui?logo=codecov&style=flat-square&token=BAQ8SOKEST&color=C43AC3&logoColor=C43AC3
|
||||
[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
|
||||
https://img.shields.io/discord/1070692720437383208?label=discord&logo=discord&style=flat-square&color=1370D3&logoColor=1370D3
|
||||
[Discord Server]: https://discord.gg/pMCEU9hNEj
|
||||
[Docs Badge]: https://img.shields.io/docsrs/ratatui?logo=rust&style=flat-square
|
||||
[Docs Badge]: https://img.shields.io/docsrs/ratatui?logo=rust&style=flat-square&logoColor=E05D44
|
||||
[Matrix Badge]:
|
||||
https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix
|
||||
https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix&color=C43AC3
|
||||
[Matrix]: https://matrix.to/#/#ratatui:matrix.org
|
||||
[Sponsors Badge]: https://img.shields.io/github/sponsors/ratatui-org?logo=github&style=flat-square&color=1370D3
|
||||
|
||||
<!-- cargo-rdme end -->
|
||||
|
||||
|
||||
16
bacon.toml
16
bacon.toml
@@ -44,6 +44,16 @@ command = [
|
||||
]
|
||||
need_stdout = true
|
||||
|
||||
[jobs.test-unit]
|
||||
command = [
|
||||
"cargo", "test",
|
||||
"--lib",
|
||||
"--all-features",
|
||||
"--color", "always",
|
||||
"--", "--color", "always", # see https://github.com/Canop/bacon/issues/124
|
||||
]
|
||||
need_stdout = true
|
||||
|
||||
[jobs.doc]
|
||||
command = [
|
||||
"cargo", "+nightly", "doc",
|
||||
@@ -74,7 +84,7 @@ on_success = "job:doc" # so that we don't open the browser at each change
|
||||
command = [
|
||||
"cargo", "llvm-cov",
|
||||
"--lcov", "--output-path", "target/lcov.info",
|
||||
"--all-features",
|
||||
"--all-features",
|
||||
"--color", "always",
|
||||
]
|
||||
|
||||
@@ -97,4 +107,6 @@ ctrl-c = "job:check-crossterm"
|
||||
ctrl-t = "job:check-termion"
|
||||
ctrl-w = "job:check-termwiz"
|
||||
v = "job:coverage"
|
||||
u = "job:coverage-unit-tests-only"
|
||||
ctrl-v = "job:coverage-unit-tests-only"
|
||||
u = "job:test-unit"
|
||||
n = "job:nextest"
|
||||
|
||||
@@ -8,7 +8,7 @@ use ratatui::{
|
||||
};
|
||||
|
||||
/// Benchmark for rendering a barchart.
|
||||
pub fn barchart(c: &mut Criterion) {
|
||||
fn barchart(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("barchart");
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
@@ -66,7 +66,7 @@ fn render(bencher: &mut Bencher, barchart: &BarChart) {
|
||||
bench_barchart.render(buffer.area, &mut buffer);
|
||||
},
|
||||
criterion::BatchSize::LargeInput,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
criterion_group!(benches, barchart);
|
||||
|
||||
@@ -1,44 +1,42 @@
|
||||
use criterion::{criterion_group, criterion_main, BatchSize, Bencher, BenchmarkId, Criterion};
|
||||
use criterion::{criterion_group, criterion_main, BatchSize, Bencher, Criterion};
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
prelude::Alignment,
|
||||
layout::{Alignment, Rect},
|
||||
widgets::{
|
||||
block::{Position, Title},
|
||||
Block, Borders, Padding, Widget,
|
||||
Block, Padding, Widget,
|
||||
},
|
||||
};
|
||||
|
||||
/// Benchmark for rendering a block.
|
||||
pub fn block(c: &mut Criterion) {
|
||||
fn block(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("block");
|
||||
|
||||
for buffer_size in &[
|
||||
Rect::new(0, 0, 100, 50), // vertically split screen
|
||||
Rect::new(0, 0, 200, 50), // 1080p fullscreen with medium font
|
||||
Rect::new(0, 0, 256, 256), // Max sized area
|
||||
for (width, height) in [
|
||||
(100, 50), // vertically split screen
|
||||
(200, 50), // 1080p fullscreen with medium font
|
||||
(256, 256), // Max sized area
|
||||
] {
|
||||
let buffer_area = buffer_size.area();
|
||||
let buffer_size = Rect::new(0, 0, width, height);
|
||||
|
||||
// Render an empty block
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("render_empty", buffer_area),
|
||||
format!("render_empty/{width}x{height}"),
|
||||
&Block::new(),
|
||||
|b, block| render(b, block, buffer_size),
|
||||
);
|
||||
|
||||
// Render with all features
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("render_all_feature", buffer_area),
|
||||
&Block::new()
|
||||
.borders(Borders::ALL)
|
||||
format!("render_all_feature/{width}x{height}"),
|
||||
&Block::bordered()
|
||||
.padding(Padding::new(5, 5, 2, 2))
|
||||
.title("test title")
|
||||
.title(
|
||||
Title::from("bottom left title")
|
||||
.alignment(Alignment::Right)
|
||||
.position(Position::Bottom),
|
||||
)
|
||||
.padding(Padding::new(5, 5, 2, 2)),
|
||||
),
|
||||
|b, block| render(b, block, buffer_size),
|
||||
);
|
||||
}
|
||||
@@ -47,8 +45,8 @@ pub fn block(c: &mut Criterion) {
|
||||
}
|
||||
|
||||
/// render the block into a buffer of the given `size`
|
||||
fn render(bencher: &mut Bencher, block: &Block, size: &Rect) {
|
||||
let mut buffer = Buffer::empty(*size);
|
||||
fn render(bencher: &mut Bencher, block: &Block, size: Rect) {
|
||||
let mut buffer = Buffer::empty(size);
|
||||
// We use `iter_batched` to clone the value in the setup function.
|
||||
// See https://github.com/ratatui-org/ratatui/pull/377.
|
||||
bencher.iter_batched(
|
||||
@@ -57,7 +55,7 @@ fn render(bencher: &mut Bencher, block: &Block, size: &Rect) {
|
||||
bench_block.render(buffer.area, &mut buffer);
|
||||
},
|
||||
BatchSize::SmallInput,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
criterion_group!(benches, block);
|
||||
|
||||
39
benches/line.rs
Normal file
39
benches/line.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use std::hint::black_box;
|
||||
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Rect},
|
||||
style::Stylize,
|
||||
text::Line,
|
||||
widgets::Widget,
|
||||
};
|
||||
|
||||
fn line_render(criterion: &mut Criterion) {
|
||||
for alignment in [Alignment::Left, Alignment::Center, Alignment::Right] {
|
||||
let mut group = criterion.benchmark_group(format!("line_render/{alignment}"));
|
||||
group.sample_size(1000);
|
||||
|
||||
let line = &Line::from(vec![
|
||||
"This".red(),
|
||||
" ".green(),
|
||||
"is".italic(),
|
||||
" ".blue(),
|
||||
"SPARTA!!".bold(),
|
||||
])
|
||||
.alignment(alignment);
|
||||
|
||||
for width in [0, 3, 4, 6, 7, 10, 42] {
|
||||
let area = Rect::new(0, 0, width, 1);
|
||||
|
||||
group.bench_function(width.to_string(), |bencher| {
|
||||
let mut buffer = Buffer::empty(area);
|
||||
bencher.iter(|| black_box(line).render(area, &mut buffer));
|
||||
});
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
}
|
||||
|
||||
criterion_group!(benches, line_render);
|
||||
criterion_main!(benches);
|
||||
@@ -7,7 +7,7 @@ use ratatui::{
|
||||
|
||||
/// Benchmark for rendering a list.
|
||||
/// It only benchmarks the render with a different amount of items.
|
||||
pub fn list(c: &mut Criterion) {
|
||||
fn list(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("list");
|
||||
|
||||
for line_count in [64, 2048, 16384] {
|
||||
@@ -33,7 +33,7 @@ pub fn list(c: &mut Criterion) {
|
||||
ListState::default()
|
||||
.with_offset(line_count / 2)
|
||||
.with_selected(Some(line_count / 2)),
|
||||
)
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -52,7 +52,7 @@ fn render(bencher: &mut Bencher, list: &List) {
|
||||
Widget::render(bench_list, buffer.area, &mut buffer);
|
||||
},
|
||||
BatchSize::LargeInput,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/// render the list into a common size buffer with a state
|
||||
@@ -66,7 +66,7 @@ fn render_stateful(bencher: &mut Bencher, list: &List, mut state: ListState) {
|
||||
StatefulWidget::render(bench_list, buffer.area, &mut buffer, &mut state);
|
||||
},
|
||||
BatchSize::LargeInput,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
criterion_group!(benches, list);
|
||||
|
||||
@@ -17,15 +17,15 @@ const WRAP_WIDTH: u16 = 100;
|
||||
/// Benchmark for rendering a paragraph with a given number of lines. The design of this benchmark
|
||||
/// allows comparison of the performance of rendering a paragraph with different numbers of lines.
|
||||
/// as well as comparing with the various settings on the scroll and wrap features.
|
||||
pub fn paragraph(c: &mut Criterion) {
|
||||
fn paragraph(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("paragraph");
|
||||
for &line_count in [64, 2048, MAX_SCROLL_OFFSET].iter() {
|
||||
for line_count in [64, 2048, MAX_SCROLL_OFFSET] {
|
||||
let lines = random_lines(line_count);
|
||||
let lines = lines.as_str();
|
||||
|
||||
// benchmark that measures the overhead of creating a paragraph separately from rendering
|
||||
group.bench_with_input(BenchmarkId::new("new", line_count), lines, |b, lines| {
|
||||
b.iter(|| Paragraph::new(black_box(lines)))
|
||||
b.iter(|| Paragraph::new(black_box(lines)));
|
||||
});
|
||||
|
||||
// render the paragraph with no scroll
|
||||
@@ -38,14 +38,14 @@ pub fn paragraph(c: &mut Criterion) {
|
||||
// scroll the paragraph by half the number of lines and render
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("render_scroll_half", line_count),
|
||||
&Paragraph::new(lines).scroll((0u16, line_count / 2)),
|
||||
&Paragraph::new(lines).scroll((0, line_count / 2)),
|
||||
|bencher, paragraph| render(bencher, paragraph, NO_WRAP_WIDTH),
|
||||
);
|
||||
|
||||
// scroll the paragraph by the full number of lines and render
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("render_scroll_full", line_count),
|
||||
&Paragraph::new(lines).scroll((0u16, line_count)),
|
||||
&Paragraph::new(lines).scroll((0, line_count)),
|
||||
|bencher, paragraph| render(bencher, paragraph, NO_WRAP_WIDTH),
|
||||
);
|
||||
|
||||
@@ -61,7 +61,7 @@ pub fn paragraph(c: &mut Criterion) {
|
||||
BenchmarkId::new("render_wrap_scroll_full", line_count),
|
||||
&Paragraph::new(lines)
|
||||
.wrap(Wrap { trim: false })
|
||||
.scroll((0u16, line_count)),
|
||||
.scroll((0, line_count)),
|
||||
|bencher, paragraph| render(bencher, paragraph, WRAP_WIDTH),
|
||||
);
|
||||
}
|
||||
@@ -79,7 +79,7 @@ fn render(bencher: &mut Bencher, paragraph: &Paragraph, width: u16) {
|
||||
bench_paragraph.render(buffer.area, &mut buffer);
|
||||
},
|
||||
BatchSize::LargeInput,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/// Create a string with the given number of lines filled with nonsense words
|
||||
@@ -87,7 +87,7 @@ fn render(bencher: &mut Bencher, paragraph: &Paragraph, width: u16) {
|
||||
/// English language has about 5.1 average characters per word so including the space between words
|
||||
/// this should emit around 200 characters per paragraph on average.
|
||||
fn random_lines(count: u16) -> String {
|
||||
let count = count as i64;
|
||||
let count = i64::from(count);
|
||||
let sentence_count = 3;
|
||||
let word_count = 11;
|
||||
fakeit::words::paragraph(count, sentence_count, word_count, "\n".into())
|
||||
|
||||
@@ -7,7 +7,7 @@ use ratatui::{
|
||||
};
|
||||
|
||||
/// Benchmark for rendering a sparkline.
|
||||
pub fn sparkline(c: &mut Criterion) {
|
||||
fn sparkline(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("sparkline");
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
@@ -38,7 +38,7 @@ fn render(bencher: &mut Bencher, sparkline: &Sparkline) {
|
||||
bench_sparkline.render(buffer.area, &mut buffer);
|
||||
},
|
||||
criterion::BatchSize::LargeInput,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
criterion_group!(benches, sparkline);
|
||||
|
||||
37
cliff.toml
37
cliff.toml
@@ -1,4 +1,9 @@
|
||||
# configuration for https://github.com/orhun/git-cliff
|
||||
# git-cliff ~ configuration file
|
||||
# https://git-cliff.org/docs/configuration
|
||||
|
||||
[remote.github]
|
||||
owner = "ratatui-org"
|
||||
repo = "ratatui"
|
||||
|
||||
[changelog]
|
||||
# changelog header
|
||||
@@ -21,8 +26,10 @@ body = """
|
||||
{% endif -%}
|
||||
|
||||
{% macro commit(commit) -%}
|
||||
- [{{ commit.id | truncate(length=7, end="") }}]({{ "https://github.com/ratatui-org/ratatui/commit/" ~ commit.id }})
|
||||
*({{commit.scope | default(value = "uncategorized") | lower }})* {{ commit.message | upper_first }}
|
||||
- [{{ commit.id | truncate(length=7, end="") }}]({{ "https://github.com/ratatui-org/ratatui/commit/" ~ commit.id }}) \
|
||||
*({{commit.scope | default(value = "uncategorized") | lower }})* {{ commit.message | upper_first | trim }}\
|
||||
{% if commit.github.username %} by @{{ commit.github.username }}{%- endif -%}\
|
||||
{% if commit.github.pr_number %} in [#{{ commit.github.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.github.pr_number }}){%- endif %}\
|
||||
{%- if commit.breaking %} [**breaking**]{% endif %}
|
||||
{%- if commit.body %}
|
||||
|
||||
@@ -49,6 +56,28 @@ body = """
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{%- endfor %}
|
||||
|
||||
{% if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}
|
||||
### New Contributors
|
||||
{%- endif %}\
|
||||
{% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}
|
||||
* @{{ contributor.username }} made their first contribution
|
||||
{%- if contributor.pr_number %} in \
|
||||
[#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \
|
||||
{%- endif %}
|
||||
{%- endfor -%}
|
||||
|
||||
{% if version %}
|
||||
{% if previous.version %}
|
||||
**Full Changelog**: {{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }}
|
||||
{% endif %}
|
||||
{% else -%}
|
||||
{% raw %}\n{% endraw %}
|
||||
{% endif %}
|
||||
|
||||
{%- macro remote_url() -%}
|
||||
https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}\
|
||||
{% endmacro %}
|
||||
"""
|
||||
|
||||
|
||||
@@ -68,7 +97,7 @@ filter_unconventional = true
|
||||
split_commits = false
|
||||
# regex for preprocessing the commit messages
|
||||
commit_preprocessors = [
|
||||
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/ratatui-org/ratatui/issues/${2}))" },
|
||||
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "" },
|
||||
{ pattern = '(better safe shared layout cache)', replace = "perf(layout): ${1}" },
|
||||
{ pattern = '(Clarify README.md)', replace = "docs(readme): ${1}" },
|
||||
{ pattern = '(Update README.md)', replace = "docs(readme): ${1}" },
|
||||
|
||||
17
clippy.toml
Normal file
17
clippy.toml
Normal file
@@ -0,0 +1,17 @@
|
||||
avoid-breaking-exported-api = false
|
||||
|
||||
# https://rust-lang.github.io/rust-clippy/master/index.html#/multiple_crate_versions
|
||||
# ratatui -> bitflags v2.3
|
||||
# termwiz -> wezterm-blob-leases -> mac_address -> nix -> bitflags v1.3.2
|
||||
# crossterm -> all the windows- deps https://github.com/ratatui-org/ratatui/pull/1064#issuecomment-2078848980
|
||||
allowed-duplicate-crates = [
|
||||
"bitflags",
|
||||
"windows-targets",
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
@@ -1,20 +1,20 @@
|
||||
# Examples
|
||||
|
||||
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.
|
||||
This folder might use unreleased code. View the examples for the latest release instead.
|
||||
|
||||
> [!WARNING]
|
||||
>
|
||||
> There are backwards incompatible changes in these examples, as they are designed to compile
|
||||
> There may be 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.
|
||||
> matches that version. E.g. <https://github.com/ratatui-org/ratatui/tree/v0.26.1/examples>.
|
||||
> - If you're viewing this file on GitHub, 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. E.g. `git switch --detach v0.26.1`.
|
||||
> - Use the latest [alpha version of Ratatui] in your app. 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"`
|
||||
>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! # [Ratatui] BarChart example
|
||||
//! # [Ratatui] `BarChart` example
|
||||
//!
|
||||
//! The latest version of this example is available in the [examples] folder in the repository.
|
||||
//!
|
||||
@@ -24,7 +24,10 @@ use crossterm::{
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{Bar, BarChart, BarGroup, Block, Paragraph},
|
||||
};
|
||||
|
||||
struct Company<'a> {
|
||||
revenue: [u64; 4],
|
||||
@@ -41,7 +44,7 @@ struct App<'a> {
|
||||
const TOTAL_REVENUE: &str = "Total Revenue";
|
||||
|
||||
impl<'a> App<'a> {
|
||||
fn new() -> App<'a> {
|
||||
fn new() -> Self {
|
||||
App {
|
||||
data: vec![
|
||||
("B1", 9),
|
||||
@@ -137,7 +140,7 @@ fn run_app<B: Backend>(
|
||||
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
if key.code == KeyCode::Char('q') {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
@@ -156,7 +159,7 @@ fn ui(frame: &mut Frame, app: &App) {
|
||||
let [left, right] = horizontal.areas(bottom);
|
||||
|
||||
let barchart = BarChart::default()
|
||||
.block(Block::default().title("Data1").borders(Borders::ALL))
|
||||
.block(Block::bordered().title("Data1"))
|
||||
.data(&app.data)
|
||||
.bar_width(9)
|
||||
.bar_style(Style::default().fg(Color::Yellow))
|
||||
@@ -167,6 +170,7 @@ fn ui(frame: &mut Frame, app: &App) {
|
||||
draw_horizontal_bars(frame, app, right);
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
fn create_groups<'a>(app: &'a App, combine_values_and_labels: bool) -> Vec<BarGroup<'a>> {
|
||||
app.months
|
||||
.iter()
|
||||
@@ -206,21 +210,23 @@ fn create_groups<'a>(app: &'a App, combine_values_and_labels: bool) -> Vec<BarGr
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
fn draw_bar_with_group_labels(f: &mut Frame, app: &App, area: Rect) {
|
||||
const LEGEND_HEIGHT: u16 = 6;
|
||||
|
||||
let groups = create_groups(app, false);
|
||||
|
||||
let mut barchart = BarChart::default()
|
||||
.block(Block::default().title("Data1").borders(Borders::ALL))
|
||||
.block(Block::bordered().title("Data1"))
|
||||
.bar_width(7)
|
||||
.group_gap(3);
|
||||
|
||||
for group in groups {
|
||||
barchart = barchart.data(group)
|
||||
barchart = barchart.data(group);
|
||||
}
|
||||
|
||||
f.render_widget(barchart, area);
|
||||
|
||||
const LEGEND_HEIGHT: u16 = 6;
|
||||
if area.height >= LEGEND_HEIGHT && area.width >= TOTAL_REVENUE.len() as u16 + 2 {
|
||||
let legend_width = TOTAL_REVENUE.len() as u16 + 2;
|
||||
let legend_area = Rect {
|
||||
@@ -233,23 +239,25 @@ fn draw_bar_with_group_labels(f: &mut Frame, app: &App, area: Rect) {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
fn draw_horizontal_bars(f: &mut Frame, app: &App, area: Rect) {
|
||||
const LEGEND_HEIGHT: u16 = 6;
|
||||
|
||||
let groups = create_groups(app, true);
|
||||
|
||||
let mut barchart = BarChart::default()
|
||||
.block(Block::default().title("Data1").borders(Borders::ALL))
|
||||
.block(Block::bordered().title("Data1"))
|
||||
.bar_width(1)
|
||||
.group_gap(1)
|
||||
.bar_gap(0)
|
||||
.direction(Direction::Horizontal);
|
||||
|
||||
for group in groups {
|
||||
barchart = barchart.data(group)
|
||||
barchart = barchart.data(group);
|
||||
}
|
||||
|
||||
f.render_widget(barchart, area);
|
||||
|
||||
const LEGEND_HEIGHT: u16 = 6;
|
||||
if area.height >= LEGEND_HEIGHT && area.width >= TOTAL_REVENUE.len() as u16 + 2 {
|
||||
let legend_width = TOTAL_REVENUE.len() as u16 + 2;
|
||||
let legend_area = Rect {
|
||||
@@ -278,15 +286,13 @@ fn draw_legend(f: &mut Frame, area: Rect) {
|
||||
"- Company B",
|
||||
Style::default().fg(Color::Yellow),
|
||||
)),
|
||||
Line::from(vec![Span::styled(
|
||||
Line::from(Span::styled(
|
||||
"- Company C",
|
||||
Style::default().fg(Color::White),
|
||||
)]),
|
||||
)),
|
||||
];
|
||||
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.style(Style::default().fg(Color::White));
|
||||
let block = Block::bordered().style(Style::default().fg(Color::White));
|
||||
let paragraph = Paragraph::new(text).block(block);
|
||||
f.render_widget(paragraph, area);
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ fn run(terminal: &mut Terminal) -> Result<()> {
|
||||
fn handle_events() -> Result<ControlFlow<()>> {
|
||||
if event::poll(Duration::from_millis(100))? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
if key.code == KeyCode::Char('q') {
|
||||
return Ok(ControlFlow::Break(()));
|
||||
}
|
||||
}
|
||||
@@ -150,7 +150,7 @@ fn placeholder_paragraph() -> Paragraph<'static> {
|
||||
fn render_borders(paragraph: &Paragraph, border: Borders, frame: &mut Frame, area: Rect) {
|
||||
let block = Block::new()
|
||||
.borders(border)
|
||||
.title(format!("Borders::{border:#?}", border = border));
|
||||
.title(format!("Borders::{border:#?}"));
|
||||
frame.render_widget(paragraph.clone().block(block), area);
|
||||
}
|
||||
|
||||
@@ -160,23 +160,20 @@ fn render_border_type(
|
||||
frame: &mut Frame,
|
||||
area: Rect,
|
||||
) {
|
||||
let block = Block::new()
|
||||
.borders(Borders::ALL)
|
||||
let block = Block::bordered()
|
||||
.border_type(border_type)
|
||||
.title(format!("BorderType::{border_type:#?}"));
|
||||
frame.render_widget(paragraph.clone().block(block), area);
|
||||
}
|
||||
fn render_styled_borders(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
|
||||
let block = Block::new()
|
||||
.borders(Borders::ALL)
|
||||
let block = Block::bordered()
|
||||
.border_style(Style::new().blue().on_white().bold().italic())
|
||||
.title("Styled borders");
|
||||
frame.render_widget(paragraph.clone().block(block), area);
|
||||
}
|
||||
|
||||
fn render_styled_block(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
|
||||
let block = Block::new()
|
||||
.borders(Borders::ALL)
|
||||
let block = Block::bordered()
|
||||
.style(Style::new().blue().on_white().bold().italic())
|
||||
.title("Styled block");
|
||||
frame.render_widget(paragraph.clone().block(block), area);
|
||||
@@ -184,8 +181,7 @@ fn render_styled_block(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
|
||||
|
||||
// Note: this currently renders incorrectly, see https://github.com/ratatui-org/ratatui/issues/349
|
||||
fn render_styled_title(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
|
||||
let block = Block::new()
|
||||
.borders(Borders::ALL)
|
||||
let block = Block::bordered()
|
||||
.title("Styled title")
|
||||
.title_style(Style::new().blue().on_white().bold().italic());
|
||||
frame.render_widget(paragraph.clone().block(block), area);
|
||||
@@ -196,21 +192,19 @@ fn render_styled_title_content(paragraph: &Paragraph, frame: &mut Frame, area: R
|
||||
"Styled ".blue().on_white().bold().italic(),
|
||||
"title content".red().on_white().bold().italic(),
|
||||
]);
|
||||
let block = Block::new().borders(Borders::ALL).title(title);
|
||||
let block = Block::bordered().title(title);
|
||||
frame.render_widget(paragraph.clone().block(block), area);
|
||||
}
|
||||
|
||||
fn render_multiple_titles(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
|
||||
let block = Block::new()
|
||||
.borders(Borders::ALL)
|
||||
let block = Block::bordered()
|
||||
.title("Multiple".blue().on_white().bold().italic())
|
||||
.title("Titles".red().on_white().bold().italic());
|
||||
frame.render_widget(paragraph.clone().block(block), area);
|
||||
}
|
||||
|
||||
fn render_multiple_title_positions(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
|
||||
let block = Block::new()
|
||||
.borders(Borders::ALL)
|
||||
let block = Block::bordered()
|
||||
.title(
|
||||
Title::from("top left")
|
||||
.position(Position::Top)
|
||||
@@ -245,16 +239,15 @@ fn render_multiple_title_positions(paragraph: &Paragraph, frame: &mut Frame, are
|
||||
}
|
||||
|
||||
fn render_padding(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
|
||||
let block = Block::new()
|
||||
.borders(Borders::ALL)
|
||||
.title("Padding")
|
||||
.padding(Padding::new(5, 10, 1, 2));
|
||||
let block = Block::bordered()
|
||||
.padding(Padding::new(5, 10, 1, 2))
|
||||
.title("Padding");
|
||||
frame.render_widget(paragraph.clone().block(block), area);
|
||||
}
|
||||
|
||||
fn render_nested_blocks(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
|
||||
let outer_block = Block::new().borders(Borders::ALL).title("Outer block");
|
||||
let inner_block = Block::new().borders(Borders::ALL).title("Inner block");
|
||||
let outer_block = Block::bordered().title("Outer block");
|
||||
let inner_block = Block::bordered().title("Inner block");
|
||||
let inner = outer_block.inner(area);
|
||||
frame.render_widget(outer_block, area);
|
||||
frame.render_widget(paragraph.clone().block(inner_block), inner);
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
#![allow(clippy::wildcard_imports)]
|
||||
|
||||
use std::{error::Error, io};
|
||||
|
||||
use crossterm::{
|
||||
@@ -166,15 +168,13 @@ fn make_dates(current_year: i32) -> CalendarEventStore {
|
||||
mod cals {
|
||||
use super::*;
|
||||
|
||||
pub(super) fn get_cal<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
|
||||
use Month::*;
|
||||
pub fn get_cal<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
|
||||
match m {
|
||||
May => example1(m, y, es),
|
||||
June => example2(m, y, es),
|
||||
July => example3(m, y, es),
|
||||
December => example3(m, y, es),
|
||||
February => example4(m, y, es),
|
||||
November => example5(m, y, es),
|
||||
Month::May => example1(m, y, es),
|
||||
Month::June => example2(m, y, es),
|
||||
Month::July | Month::December => example3(m, y, es),
|
||||
Month::February => example4(m, y, es),
|
||||
Month::November => example5(m, y, es),
|
||||
_ => default(m, y, es),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
#![allow(clippy::wildcard_imports)]
|
||||
|
||||
use std::{
|
||||
io::{self, stdout, Stdout},
|
||||
time::{Duration, Instant},
|
||||
@@ -44,8 +46,8 @@ struct App {
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn new() -> App {
|
||||
App {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
ball: Circle {
|
||||
@@ -64,7 +66,7 @@ impl App {
|
||||
|
||||
pub fn run() -> io::Result<()> {
|
||||
let mut terminal = init_terminal()?;
|
||||
let mut app = App::new();
|
||||
let mut app = Self::new();
|
||||
let mut last_tick = Instant::now();
|
||||
let tick_rate = Duration::from_millis(16);
|
||||
loop {
|
||||
@@ -106,13 +108,13 @@ impl App {
|
||||
// bounce the ball by flipping the velocity vector
|
||||
let ball = &self.ball;
|
||||
let playground = self.playground;
|
||||
if ball.x - ball.radius < playground.left() as f64
|
||||
|| ball.x + ball.radius > playground.right() as f64
|
||||
if ball.x - ball.radius < f64::from(playground.left())
|
||||
|| ball.x + ball.radius > f64::from(playground.right())
|
||||
{
|
||||
self.vx = -self.vx;
|
||||
}
|
||||
if ball.y - ball.radius < playground.top() as f64
|
||||
|| ball.y + ball.radius > playground.bottom() as f64
|
||||
if ball.y - ball.radius < f64::from(playground.top())
|
||||
|| ball.y + ball.radius > f64::from(playground.bottom())
|
||||
{
|
||||
self.vy = -self.vy;
|
||||
}
|
||||
@@ -135,7 +137,7 @@ impl App {
|
||||
|
||||
fn map_canvas(&self) -> impl Widget + '_ {
|
||||
Canvas::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("World"))
|
||||
.block(Block::bordered().title("World"))
|
||||
.marker(self.marker)
|
||||
.paint(|ctx| {
|
||||
ctx.draw(&Map {
|
||||
@@ -150,7 +152,7 @@ impl App {
|
||||
|
||||
fn pong_canvas(&self) -> impl Widget + '_ {
|
||||
Canvas::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("Pong"))
|
||||
.block(Block::bordered().title("Pong"))
|
||||
.marker(self.marker)
|
||||
.paint(|ctx| {
|
||||
ctx.draw(&self.ball);
|
||||
@@ -160,36 +162,38 @@ impl App {
|
||||
}
|
||||
|
||||
fn boxes_canvas(&self, area: Rect) -> impl Widget {
|
||||
let (left, right, bottom, top) =
|
||||
(0.0, area.width as f64, 0.0, area.height as f64 * 2.0 - 4.0);
|
||||
let left = 0.0;
|
||||
let right = f64::from(area.width);
|
||||
let bottom = 0.0;
|
||||
let top = f64::from(area.height).mul_add(2.0, -4.0);
|
||||
Canvas::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("Rects"))
|
||||
.block(Block::bordered().title("Rects"))
|
||||
.marker(self.marker)
|
||||
.x_bounds([left, right])
|
||||
.y_bounds([bottom, top])
|
||||
.paint(|ctx| {
|
||||
for i in 0..=11 {
|
||||
ctx.draw(&Rectangle {
|
||||
x: (i * i + 3 * i) as f64 / 2.0 + 2.0,
|
||||
x: f64::from(i * i + 3 * i) / 2.0 + 2.0,
|
||||
y: 2.0,
|
||||
width: i as f64,
|
||||
height: i as f64,
|
||||
width: f64::from(i),
|
||||
height: f64::from(i),
|
||||
color: Color::Red,
|
||||
});
|
||||
ctx.draw(&Rectangle {
|
||||
x: (i * i + 3 * i) as f64 / 2.0 + 2.0,
|
||||
x: f64::from(i * i + 3 * i) / 2.0 + 2.0,
|
||||
y: 21.0,
|
||||
width: i as f64,
|
||||
height: i as f64,
|
||||
width: f64::from(i),
|
||||
height: f64::from(i),
|
||||
color: Color::Blue,
|
||||
});
|
||||
}
|
||||
for i in 0..100 {
|
||||
if i % 10 != 0 {
|
||||
ctx.print(i as f64 + 1.0, 0.0, format!("{i}", i = i % 10));
|
||||
ctx.print(f64::from(i) + 1.0, 0.0, format!("{i}", i = i % 10));
|
||||
}
|
||||
if i % 2 == 0 && i % 10 != 0 {
|
||||
ctx.print(0.0, i as f64, format!("{i}", i = i % 10));
|
||||
ctx.print(0.0, f64::from(i), format!("{i}", i = i % 10));
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -26,11 +26,11 @@ use crossterm::{
|
||||
};
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{block::Title, *},
|
||||
widgets::{block::Title, Axis, Block, Chart, Dataset, GraphType, LegendPosition},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SinSignal {
|
||||
struct SinSignal {
|
||||
x: f64,
|
||||
interval: f64,
|
||||
period: f64,
|
||||
@@ -38,8 +38,8 @@ pub struct SinSignal {
|
||||
}
|
||||
|
||||
impl SinSignal {
|
||||
pub fn new(interval: f64, period: f64, scale: f64) -> SinSignal {
|
||||
SinSignal {
|
||||
const fn new(interval: f64, period: f64, scale: f64) -> Self {
|
||||
Self {
|
||||
x: 0.0,
|
||||
interval,
|
||||
period,
|
||||
@@ -66,12 +66,12 @@ struct App {
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn new() -> App {
|
||||
fn new() -> Self {
|
||||
let mut signal1 = SinSignal::new(0.2, 3.0, 18.0);
|
||||
let mut signal2 = SinSignal::new(0.1, 2.0, 10.0);
|
||||
let data1 = signal1.by_ref().take(200).collect::<Vec<(f64, f64)>>();
|
||||
let data2 = signal2.by_ref().take(200).collect::<Vec<(f64, f64)>>();
|
||||
App {
|
||||
Self {
|
||||
signal1,
|
||||
data1,
|
||||
signal2,
|
||||
@@ -81,14 +81,12 @@ impl App {
|
||||
}
|
||||
|
||||
fn on_tick(&mut self) {
|
||||
for _ in 0..5 {
|
||||
self.data1.remove(0);
|
||||
}
|
||||
self.data1.drain(0..5);
|
||||
self.data1.extend(self.signal1.by_ref().take(5));
|
||||
for _ in 0..10 {
|
||||
self.data2.remove(0);
|
||||
}
|
||||
|
||||
self.data2.drain(0..10);
|
||||
self.data2.extend(self.signal2.by_ref().take(10));
|
||||
|
||||
self.window[0] += 1.0;
|
||||
self.window[1] += 1.0;
|
||||
}
|
||||
@@ -135,7 +133,7 @@ fn run_app<B: Backend>(
|
||||
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
if key.code == KeyCode::Char('q') {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
@@ -186,11 +184,7 @@ fn render_chart1(f: &mut Frame, area: Rect, app: &App) {
|
||||
];
|
||||
|
||||
let chart = Chart::new(datasets)
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Chart 1".cyan().bold())
|
||||
.borders(Borders::ALL),
|
||||
)
|
||||
.block(Block::bordered().title("Chart 1".cyan().bold()))
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
.title("X Axis")
|
||||
@@ -219,13 +213,11 @@ fn render_line_chart(f: &mut Frame, area: Rect) {
|
||||
|
||||
let chart = Chart::new(datasets)
|
||||
.block(
|
||||
Block::default()
|
||||
.title(
|
||||
Title::default()
|
||||
.content("Line chart".cyan().bold())
|
||||
.alignment(Alignment::Center),
|
||||
)
|
||||
.borders(Borders::ALL),
|
||||
Block::bordered().title(
|
||||
Title::default()
|
||||
.content("Line chart".cyan().bold())
|
||||
.alignment(Alignment::Center),
|
||||
),
|
||||
)
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
@@ -244,7 +236,7 @@ fn render_line_chart(f: &mut Frame, area: Rect) {
|
||||
.legend_position(Some(LegendPosition::TopLeft))
|
||||
.hidden_legend_constraints((Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)));
|
||||
|
||||
f.render_widget(chart, area)
|
||||
f.render_widget(chart, area);
|
||||
}
|
||||
|
||||
fn render_scatter(f: &mut Frame, area: Rect) {
|
||||
@@ -271,7 +263,7 @@ fn render_scatter(f: &mut Frame, area: Rect) {
|
||||
|
||||
let chart = Chart::new(datasets)
|
||||
.block(
|
||||
Block::new().borders(Borders::all()).title(
|
||||
Block::bordered().title(
|
||||
Title::default()
|
||||
.content("Scatter chart".cyan().bold())
|
||||
.alignment(Alignment::Center),
|
||||
@@ -312,7 +304,7 @@ const HEAVY_PAYLOAD_DATA: [(f64, f64); 9] = [
|
||||
const MEDIUM_PAYLOAD_DATA: [(f64, f64); 29] = [
|
||||
(1963., 29500.),
|
||||
(1964., 30600.),
|
||||
(1965., 177900.),
|
||||
(1965., 177_900.),
|
||||
(1965., 21000.),
|
||||
(1966., 17900.),
|
||||
(1966., 8400.),
|
||||
@@ -342,7 +334,7 @@ const MEDIUM_PAYLOAD_DATA: [(f64, f64); 29] = [
|
||||
];
|
||||
|
||||
const SMALL_PAYLOAD_DATA: [(f64, f64); 23] = [
|
||||
(1961., 118500.),
|
||||
(1961., 118_500.),
|
||||
(1962., 14900.),
|
||||
(1975., 21400.),
|
||||
(1980., 32800.),
|
||||
|
||||
@@ -13,8 +13,9 @@
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
/// This example shows all the colors supported by ratatui. It will render a grid of foreground
|
||||
/// and background colors with their names and indexes.
|
||||
// This example shows all the colors supported by ratatui. It will render a grid of foreground
|
||||
// and background colors with their names and indexes.
|
||||
|
||||
use std::{
|
||||
error::Error,
|
||||
io::{self, Stdout},
|
||||
@@ -28,7 +29,10 @@ use crossterm::{
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{Block, Borders, Paragraph},
|
||||
};
|
||||
|
||||
type Result<T> = result::Result<T, Box<dyn Error>>;
|
||||
|
||||
@@ -48,7 +52,7 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
|
||||
|
||||
if event::poll(Duration::from_millis(250))? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
if key.code == KeyCode::Char('q') {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
@@ -226,12 +230,12 @@ fn render_indexed_colors(frame: &mut Frame, area: Rect) {
|
||||
}
|
||||
|
||||
fn title_block(title: String) -> Block<'static> {
|
||||
Block::default()
|
||||
Block::new()
|
||||
.borders(Borders::TOP)
|
||||
.border_style(Style::new().dark_gray())
|
||||
.title(title)
|
||||
.title_alignment(Alignment::Center)
|
||||
.border_style(Style::new().dark_gray())
|
||||
.title_style(Style::new().reset())
|
||||
.title(title)
|
||||
}
|
||||
|
||||
fn render_indexed_grayscale(frame: &mut Frame, area: Rect) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! # [Ratatui] Colors_RGB example
|
||||
//! # [Ratatui] `Colors_RGB` example
|
||||
//!
|
||||
//! The latest version of this example is available in the [examples] folder in the repository.
|
||||
//!
|
||||
@@ -13,18 +13,19 @@
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
/// This example shows the full range of RGB colors that can be displayed in the terminal.
|
||||
///
|
||||
/// Requires a terminal that supports 24-bit color (true color) and unicode.
|
||||
///
|
||||
/// This example also demonstrates how implementing the Widget trait on a mutable reference
|
||||
/// allows the widget to update its state while it is being rendered. This allows the fps
|
||||
/// widget to update the fps calculation and the colors widget to update a cached version of
|
||||
/// the colors to render instead of recalculating them every frame.
|
||||
///
|
||||
/// This is an alternative to using the StatefulWidget trait and a separate state struct. It is
|
||||
/// useful when the state is only used by the widget and doesn't need to be shared with other
|
||||
/// widgets.
|
||||
// This example shows the full range of RGB colors that can be displayed in the terminal.
|
||||
//
|
||||
// Requires a terminal that supports 24-bit color (true color) and unicode.
|
||||
//
|
||||
// This example also demonstrates how implementing the Widget trait on a mutable reference
|
||||
// allows the widget to update its state while it is being rendered. This allows the fps
|
||||
// widget to update the fps calculation and the colors widget to update a cached version of
|
||||
// the colors to render instead of recalculating them every frame.
|
||||
//
|
||||
// This is an alternative to using the `StatefulWidget` trait and a separate state struct. It
|
||||
// is useful when the state is only used by the widget and doesn't need to be shared with
|
||||
// other widgets.
|
||||
|
||||
use std::{
|
||||
io::stdout,
|
||||
panic,
|
||||
@@ -110,7 +111,7 @@ impl App {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_running(&self) -> bool {
|
||||
const fn is_running(&self) -> bool {
|
||||
matches!(self.state, AppState::Running)
|
||||
}
|
||||
|
||||
@@ -140,6 +141,7 @@ impl App {
|
||||
/// to update the colors to render.
|
||||
impl Widget for &mut App {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
#[allow(clippy::enum_glob_use)]
|
||||
use Constraint::*;
|
||||
let [top, colors] = Layout::vertical([Length(1), Min(0)]).areas(area);
|
||||
let [title, fps] = Layout::horizontal([Min(0), Length(8)]).areas(top);
|
||||
@@ -151,9 +153,9 @@ impl Widget for &mut App {
|
||||
}
|
||||
}
|
||||
|
||||
/// Default impl for FpsWidget
|
||||
/// Default impl for `FpsWidget`
|
||||
///
|
||||
/// Manual impl is required because we need to initialize the last_instant field to the current
|
||||
/// Manual impl is required because we need to initialize the `last_instant` field to the current
|
||||
/// instant.
|
||||
impl Default for FpsWidget {
|
||||
fn default() -> Self {
|
||||
@@ -165,7 +167,7 @@ impl Default for FpsWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// Widget impl for FpsWidget
|
||||
/// Widget impl for `FpsWidget`
|
||||
///
|
||||
/// This is implemented on a mutable reference so that we can update the frame count and fps
|
||||
/// calculation while rendering.
|
||||
@@ -173,7 +175,7 @@ impl Widget for &mut FpsWidget {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.calculate_fps();
|
||||
if let Some(fps) = self.fps {
|
||||
let text = format!("{:.1} fps", fps);
|
||||
let text = format!("{fps:.1} fps");
|
||||
Text::from(text).render(area, buf);
|
||||
}
|
||||
}
|
||||
@@ -185,6 +187,7 @@ impl FpsWidget {
|
||||
/// This updates the fps once a second, but only if the widget has rendered at least 2 frames
|
||||
/// since the last calculation. This avoids noise in the fps calculation when rendering on slow
|
||||
/// machines that can't render at least 2 frames per second.
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
fn calculate_fps(&mut self) {
|
||||
self.frame_count += 1;
|
||||
let elapsed = self.last_instant.elapsed();
|
||||
@@ -196,7 +199,7 @@ impl FpsWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// Widget impl for ColorsWidget
|
||||
/// Widget impl for `ColorsWidget`
|
||||
///
|
||||
/// This is implemented on a mutable reference so that we can update the frame count and store a
|
||||
/// cached version of the colors to render instead of recalculating them every frame.
|
||||
@@ -226,6 +229,7 @@ impl ColorsWidget {
|
||||
///
|
||||
/// This is called once per frame to setup the colors to render. It caches the colors so that
|
||||
/// they don't need to be recalculated every frame.
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
fn setup_colors(&mut self, size: Rect) {
|
||||
let Rect { width, height, .. } = size;
|
||||
// double the height because each screen row has two rows of half block pixels
|
||||
@@ -253,7 +257,7 @@ impl ColorsWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// Install color_eyre panic and error hooks
|
||||
/// Install `color_eyre` panic and error hooks
|
||||
///
|
||||
/// The hooks restore the terminal to a usable state before printing the error message.
|
||||
fn install_error_hooks() -> Result<()> {
|
||||
@@ -266,7 +270,7 @@ fn install_error_hooks() -> Result<()> {
|
||||
}))?;
|
||||
panic::set_hook(Box::new(move |info| {
|
||||
let _ = restore_terminal();
|
||||
panic(info)
|
||||
panic(info);
|
||||
}));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
#![allow(clippy::enum_glob_use, clippy::wildcard_imports)]
|
||||
|
||||
use std::io::{self, stdout};
|
||||
|
||||
use color_eyre::{config::HookBuilder, Result};
|
||||
@@ -27,7 +29,7 @@ use ratatui::{
|
||||
prelude::*,
|
||||
style::palette::tailwind::*,
|
||||
symbols::line,
|
||||
widgets::*,
|
||||
widgets::{Block, Paragraph, Wrap},
|
||||
};
|
||||
use strum::{Display, EnumIter, FromRepr};
|
||||
|
||||
@@ -67,9 +69,9 @@ enum ConstraintName {
|
||||
/// └──────────────┘
|
||||
/// ```
|
||||
struct ConstraintBlock {
|
||||
selected: bool,
|
||||
legend: bool,
|
||||
constraint: Constraint,
|
||||
legend: bool,
|
||||
selected: bool,
|
||||
}
|
||||
|
||||
/// A widget that renders a spacer with a label indicating the width of the spacer. E.g.:
|
||||
@@ -146,32 +148,31 @@ impl App {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// select the next block with wrap around
|
||||
fn increment_value(&mut self) {
|
||||
if self.constraints.is_empty() {
|
||||
let Some(constraint) = self.constraints.get_mut(self.selected_index) else {
|
||||
return;
|
||||
}
|
||||
self.constraints[self.selected_index] = match self.constraints[self.selected_index] {
|
||||
Constraint::Length(v) => Constraint::Length(v.saturating_add(1)),
|
||||
Constraint::Min(v) => Constraint::Min(v.saturating_add(1)),
|
||||
Constraint::Max(v) => Constraint::Max(v.saturating_add(1)),
|
||||
Constraint::Fill(v) => Constraint::Fill(v.saturating_add(1)),
|
||||
Constraint::Percentage(v) => Constraint::Percentage(v.saturating_add(1)),
|
||||
Constraint::Ratio(n, d) => Constraint::Ratio(n, d.saturating_add(1)),
|
||||
};
|
||||
match constraint {
|
||||
Constraint::Length(v)
|
||||
| Constraint::Min(v)
|
||||
| Constraint::Max(v)
|
||||
| Constraint::Fill(v)
|
||||
| Constraint::Percentage(v) => *v = v.saturating_add(1),
|
||||
Constraint::Ratio(_n, d) => *d = d.saturating_add(1),
|
||||
};
|
||||
}
|
||||
|
||||
fn decrement_value(&mut self) {
|
||||
if self.constraints.is_empty() {
|
||||
let Some(constraint) = self.constraints.get_mut(self.selected_index) else {
|
||||
return;
|
||||
}
|
||||
self.constraints[self.selected_index] = match self.constraints[self.selected_index] {
|
||||
Constraint::Length(v) => Constraint::Length(v.saturating_sub(1)),
|
||||
Constraint::Min(v) => Constraint::Min(v.saturating_sub(1)),
|
||||
Constraint::Max(v) => Constraint::Max(v.saturating_sub(1)),
|
||||
Constraint::Fill(v) => Constraint::Fill(v.saturating_sub(1)),
|
||||
Constraint::Percentage(v) => Constraint::Percentage(v.saturating_sub(1)),
|
||||
Constraint::Ratio(n, d) => Constraint::Ratio(n, d.saturating_sub(1)),
|
||||
};
|
||||
match constraint {
|
||||
Constraint::Length(v)
|
||||
| Constraint::Min(v)
|
||||
| Constraint::Max(v)
|
||||
| Constraint::Fill(v)
|
||||
| Constraint::Percentage(v) => *v = v.saturating_sub(1),
|
||||
Constraint::Ratio(_n, d) => *d = d.saturating_sub(1),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -222,7 +223,7 @@ impl App {
|
||||
}
|
||||
|
||||
fn exit(&mut self) {
|
||||
self.mode = AppMode::Quit
|
||||
self.mode = AppMode::Quit;
|
||||
}
|
||||
|
||||
fn swap_constraint(&mut self, name: ConstraintName) {
|
||||
@@ -235,7 +236,7 @@ impl App {
|
||||
ConstraintName::Min => Min(self.value),
|
||||
ConstraintName::Max => Max(self.value),
|
||||
ConstraintName::Fill => Fill(self.value),
|
||||
ConstraintName::Ratio => Ratio(1, self.value as u32 / 4), // for balance
|
||||
ConstraintName::Ratio => Ratio(1, u32::from(self.value) / 4), // for balance
|
||||
};
|
||||
self.constraints[self.selected_index] = constraint;
|
||||
}
|
||||
@@ -243,14 +244,13 @@ impl App {
|
||||
|
||||
impl From<Constraint> for ConstraintName {
|
||||
fn from(constraint: Constraint) -> Self {
|
||||
use Constraint::*;
|
||||
match constraint {
|
||||
Length(_) => ConstraintName::Length,
|
||||
Percentage(_) => ConstraintName::Percentage,
|
||||
Ratio(_, _) => ConstraintName::Ratio,
|
||||
Min(_) => ConstraintName::Min,
|
||||
Max(_) => ConstraintName::Max,
|
||||
Fill(_) => ConstraintName::Fill,
|
||||
Length(_) => Self::Length,
|
||||
Percentage(_) => Self::Percentage,
|
||||
Ratio(_, _) => Self::Ratio,
|
||||
Min(_) => Self::Min,
|
||||
Max(_) => Self::Max,
|
||||
Fill(_) => Self::Fill,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -267,9 +267,9 @@ impl Widget for &App {
|
||||
])
|
||||
.areas(area);
|
||||
|
||||
self.header().render(header_area, buf);
|
||||
self.instructions().render(instructions_area, buf);
|
||||
self.swap_legend().render(swap_legend_area, buf);
|
||||
App::header().render(header_area, buf);
|
||||
App::instructions().render(instructions_area, buf);
|
||||
App::swap_legend().render(swap_legend_area, buf);
|
||||
self.render_layout_blocks(blocks_area, buf);
|
||||
}
|
||||
}
|
||||
@@ -280,12 +280,12 @@ impl App {
|
||||
const TEXT_COLOR: Color = SLATE.c400;
|
||||
const AXIS_COLOR: Color = SLATE.c500;
|
||||
|
||||
fn header(&self) -> impl Widget {
|
||||
fn header() -> impl Widget {
|
||||
let text = "Constraint Explorer";
|
||||
text.bold().fg(Self::HEADER_COLOR).to_centered_line()
|
||||
text.bold().fg(Self::HEADER_COLOR).into_centered_line()
|
||||
}
|
||||
|
||||
fn instructions(&self) -> impl Widget {
|
||||
fn instructions() -> impl Widget {
|
||||
let text = "◄ ►: select, ▲ ▼: edit, 1-6: swap, a: add, x: delete, q: quit, + -: spacing";
|
||||
Paragraph::new(text)
|
||||
.fg(Self::TEXT_COLOR)
|
||||
@@ -293,7 +293,7 @@ impl App {
|
||||
.wrap(Wrap { trim: false })
|
||||
}
|
||||
|
||||
fn swap_legend(&self) -> impl Widget {
|
||||
fn swap_legend() -> impl Widget {
|
||||
#[allow(unstable_name_collisions)]
|
||||
Paragraph::new(
|
||||
Line::from(
|
||||
@@ -327,7 +327,7 @@ impl App {
|
||||
let label = if self.spacing != 0 {
|
||||
format!("{} px (gap: {} px)", width, self.spacing)
|
||||
} else {
|
||||
format!("{} px", width)
|
||||
format!("{width} px")
|
||||
};
|
||||
let bar_width = width.saturating_sub(2) as usize; // we want to `<` and `>` at the ends
|
||||
let width_bar = format!("<{label:-^bar_width$}>");
|
||||
@@ -348,7 +348,7 @@ impl App {
|
||||
self.render_layout_block(Flex::Center, center, buf);
|
||||
self.render_layout_block(Flex::End, end, buf);
|
||||
self.render_layout_block(Flex::SpaceAround, space_around, buf);
|
||||
self.render_layout_block(Flex::SpaceBetween, space_between, buf)
|
||||
self.render_layout_block(Flex::SpaceBetween, space_between, buf);
|
||||
}
|
||||
|
||||
fn render_user_constraints_legend(&self, area: Rect, buf: &mut Buffer) {
|
||||
@@ -371,7 +371,7 @@ impl App {
|
||||
Layout::vertical([Length(1), Max(1), Length(4)]).areas(area);
|
||||
|
||||
if label_area.height > 0 {
|
||||
format!("Flex::{:?}", flex).bold().render(label_area, buf);
|
||||
format!("Flex::{flex:?}").bold().render(label_area, buf);
|
||||
}
|
||||
|
||||
self.axis(area.width).render(axis_area, buf);
|
||||
@@ -405,17 +405,17 @@ impl Widget for ConstraintBlock {
|
||||
impl ConstraintBlock {
|
||||
const TEXT_COLOR: Color = SLATE.c200;
|
||||
|
||||
fn new(constraint: Constraint, selected: bool, legend: bool) -> Self {
|
||||
const fn new(constraint: Constraint, selected: bool, legend: bool) -> Self {
|
||||
Self {
|
||||
constraint,
|
||||
selected,
|
||||
legend,
|
||||
selected,
|
||||
}
|
||||
}
|
||||
|
||||
fn label(&self, width: u16) -> String {
|
||||
let long_width = format!("{} px", width);
|
||||
let short_width = format!("{}", width);
|
||||
let long_width = format!("{width} px");
|
||||
let short_width = format!("{width}");
|
||||
// border takes up 2 columns
|
||||
let available_space = width.saturating_sub(2) as usize;
|
||||
let width_label = if long_width.len() < available_space {
|
||||
@@ -423,7 +423,7 @@ impl ConstraintBlock {
|
||||
} else if short_width.len() < available_space {
|
||||
short_width
|
||||
} else {
|
||||
"".to_string()
|
||||
String::new()
|
||||
};
|
||||
format!("{}\n{}", self.constraint, width_label)
|
||||
}
|
||||
@@ -436,7 +436,7 @@ impl ConstraintBlock {
|
||||
} else {
|
||||
main_color
|
||||
};
|
||||
Block::default()
|
||||
Block::new()
|
||||
.fg(Self::TEXT_COLOR)
|
||||
.bg(selected_color)
|
||||
.render(area, buf);
|
||||
@@ -499,9 +499,9 @@ impl Widget for SpacerBlock {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
match area.height {
|
||||
1 => (),
|
||||
2 => self.render_2px(area, buf),
|
||||
3 => self.render_3px(area, buf),
|
||||
_ => self.render_4px(area, buf),
|
||||
2 => Self::render_2px(area, buf),
|
||||
3 => Self::render_3px(area, buf),
|
||||
_ => Self::render_4px(area, buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -541,7 +541,7 @@ impl SpacerBlock {
|
||||
/// A label that says "Spacer" if there is enough space
|
||||
fn spacer_label(width: u16) -> impl Widget {
|
||||
let label = if width >= 6 { "Spacer" } else { "" };
|
||||
label.fg(SpacerBlock::TEXT_COLOR).to_centered_line()
|
||||
label.fg(Self::TEXT_COLOR).into_centered_line()
|
||||
}
|
||||
|
||||
/// A label that says "8 px" if there is enough space
|
||||
@@ -553,12 +553,12 @@ impl SpacerBlock {
|
||||
} else if short_label.len() < width as usize {
|
||||
short_label
|
||||
} else {
|
||||
"".to_string()
|
||||
String::new()
|
||||
};
|
||||
Line::styled(label, Self::TEXT_COLOR).centered()
|
||||
}
|
||||
|
||||
fn render_2px(&self, area: Rect, buf: &mut Buffer) {
|
||||
fn render_2px(area: Rect, buf: &mut Buffer) {
|
||||
if area.width > 1 {
|
||||
Self::block().render(area, buf);
|
||||
} else {
|
||||
@@ -566,7 +566,7 @@ impl SpacerBlock {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_3px(&self, area: Rect, buf: &mut Buffer) {
|
||||
fn render_3px(area: Rect, buf: &mut Buffer) {
|
||||
if area.width > 1 {
|
||||
Self::block().render(area, buf);
|
||||
} else {
|
||||
@@ -577,7 +577,7 @@ impl SpacerBlock {
|
||||
Self::spacer_label(area.width).render(row, buf);
|
||||
}
|
||||
|
||||
fn render_4px(&self, area: Rect, buf: &mut Buffer) {
|
||||
fn render_4px(area: Rect, buf: &mut Buffer) {
|
||||
if area.width > 1 {
|
||||
Self::block().render(area, buf);
|
||||
} else {
|
||||
@@ -593,7 +593,7 @@ impl SpacerBlock {
|
||||
}
|
||||
|
||||
impl ConstraintName {
|
||||
fn color(&self) -> Color {
|
||||
const fn color(self) -> Color {
|
||||
match self {
|
||||
Self::Length => SLATE.c700,
|
||||
Self::Percentage => SLATE.c800,
|
||||
@@ -604,7 +604,7 @@ impl ConstraintName {
|
||||
}
|
||||
}
|
||||
|
||||
fn lighter_color(&self) -> Color {
|
||||
const fn lighter_color(self) -> Color {
|
||||
match self {
|
||||
Self::Length => STONE.c500,
|
||||
Self::Percentage => STONE.c600,
|
||||
@@ -626,7 +626,7 @@ fn init_error_hooks() -> Result<()> {
|
||||
}))?;
|
||||
std::panic::set_hook(Box::new(move |info| {
|
||||
let _ = restore_terminal();
|
||||
panic(info)
|
||||
panic(info);
|
||||
}));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -13,11 +13,13 @@
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
#![allow(clippy::enum_glob_use, clippy::wildcard_imports)]
|
||||
|
||||
use std::io::{self, stdout};
|
||||
|
||||
use color_eyre::{config::HookBuilder, Result};
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
event::{self, Event, KeyCode, KeyEventKind},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
};
|
||||
@@ -95,7 +97,7 @@ impl App {
|
||||
self.max_scroll_offset = (self.selected_tab.get_example_count() - 1) * EXAMPLE_HEIGHT;
|
||||
}
|
||||
|
||||
fn is_running(&self) -> bool {
|
||||
fn is_running(self) -> bool {
|
||||
self.state == AppState::Running
|
||||
}
|
||||
|
||||
@@ -107,6 +109,9 @@ impl App {
|
||||
fn handle_events(&mut self) -> Result<()> {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
use KeyCode::*;
|
||||
if key.kind != KeyEventKind::Press {
|
||||
return Ok(());
|
||||
}
|
||||
match key.code {
|
||||
Char('q') | Esc => self.quit(),
|
||||
Char('l') | Right => self.next(),
|
||||
@@ -138,14 +143,14 @@ impl App {
|
||||
}
|
||||
|
||||
fn up(&mut self) {
|
||||
self.scroll_offset = self.scroll_offset.saturating_sub(1)
|
||||
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)
|
||||
.min(self.max_scroll_offset);
|
||||
}
|
||||
|
||||
fn top(&mut self) {
|
||||
@@ -159,17 +164,16 @@ impl App {
|
||||
|
||||
impl Widget for App {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let [tabs, axis, demo] =
|
||||
Layout::vertical([Constraint::Length(3), Constraint::Length(3), Fill(0)]).areas(area);
|
||||
let [tabs, axis, demo] = Layout::vertical([Length(3), Length(3), Fill(0)]).areas(area);
|
||||
|
||||
self.render_tabs(tabs, buf);
|
||||
self.render_axis(axis, buf);
|
||||
Self::render_axis(axis, buf);
|
||||
self.render_demo(demo, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn render_tabs(&self, area: Rect, buf: &mut Buffer) {
|
||||
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())
|
||||
@@ -183,17 +187,17 @@ impl App {
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
fn render_axis(&self, area: Rect, buf: &mut Buffer) {
|
||||
fn render_axis(area: Rect, buf: &mut Buffer) {
|
||||
let width = area.width as usize;
|
||||
// a bar like `<----- 80 px ----->`
|
||||
let width_label = format!("{} px", width);
|
||||
let width_label = format!("{width} px");
|
||||
let width_bar = format!(
|
||||
"<{width_label:-^width$}>",
|
||||
width = width - width_label.len() / 2
|
||||
);
|
||||
Paragraph::new(width_bar.dark_gray())
|
||||
.centered()
|
||||
.block(Block::default().padding(Padding {
|
||||
.block(Block::new().padding(Padding {
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 1,
|
||||
@@ -206,7 +210,8 @@ impl App {
|
||||
///
|
||||
/// 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) {
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
fn render_demo(self, area: Rect, buf: &mut Buffer) {
|
||||
// render demo content into a separate buffer so all examples fit we add an extra
|
||||
// area.height to make sure the last example is fully visible even when the scroll offset is
|
||||
// at the max
|
||||
@@ -246,41 +251,40 @@ impl App {
|
||||
|
||||
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;
|
||||
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)
|
||||
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;
|
||||
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)
|
||||
Self::from_repr(next_index).unwrap_or(self)
|
||||
}
|
||||
|
||||
fn get_example_count(&self) -> u16 {
|
||||
use SelectedTab::*;
|
||||
const fn get_example_count(self) -> u16 {
|
||||
#[allow(clippy::match_same_arms)]
|
||||
match self {
|
||||
Length => 4,
|
||||
Percentage => 5,
|
||||
Ratio => 4,
|
||||
Fill => 2,
|
||||
Min => 5,
|
||||
Max => 5,
|
||||
Self::Length => 4,
|
||||
Self::Percentage => 5,
|
||||
Self::Ratio => 4,
|
||||
Self::Fill => 2,
|
||||
Self::Min => 5,
|
||||
Self::Max => 5,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_tab_title(value: SelectedTab) -> Line<'static> {
|
||||
use SelectedTab::*;
|
||||
fn to_tab_title(value: Self) -> Line<'static> {
|
||||
let text = format!(" {value} ");
|
||||
let color = match value {
|
||||
Length => LENGTH_COLOR,
|
||||
Percentage => PERCENTAGE_COLOR,
|
||||
Ratio => RATIO_COLOR,
|
||||
Fill => FILL_COLOR,
|
||||
Min => MIN_COLOR,
|
||||
Max => MAX_COLOR,
|
||||
Self::Length => LENGTH_COLOR,
|
||||
Self::Percentage => PERCENTAGE_COLOR,
|
||||
Self::Ratio => RATIO_COLOR,
|
||||
Self::Fill => FILL_COLOR,
|
||||
Self::Min => MIN_COLOR,
|
||||
Self::Max => MAX_COLOR,
|
||||
};
|
||||
text.fg(tailwind::SLATE.c200).bg(color).into()
|
||||
}
|
||||
@@ -289,18 +293,18 @@ impl SelectedTab {
|
||||
impl Widget for SelectedTab {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
match self {
|
||||
SelectedTab::Length => self.render_length_example(area, buf),
|
||||
SelectedTab::Percentage => self.render_percentage_example(area, buf),
|
||||
SelectedTab::Ratio => self.render_ratio_example(area, buf),
|
||||
SelectedTab::Fill => self.render_fill_example(area, buf),
|
||||
SelectedTab::Min => self.render_min_example(area, buf),
|
||||
SelectedTab::Max => self.render_max_example(area, buf),
|
||||
Self::Length => Self::render_length_example(area, buf),
|
||||
Self::Percentage => Self::render_percentage_example(area, buf),
|
||||
Self::Ratio => Self::render_ratio_example(area, buf),
|
||||
Self::Fill => Self::render_fill_example(area, buf),
|
||||
Self::Min => Self::render_min_example(area, buf),
|
||||
Self::Max => Self::render_max_example(area, buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SelectedTab {
|
||||
fn render_length_example(&self, area: Rect, buf: &mut Buffer) {
|
||||
fn render_length_example(area: Rect, buf: &mut Buffer) {
|
||||
let [example1, example2, example3, _] =
|
||||
Layout::vertical([Length(EXAMPLE_HEIGHT); 4]).areas(area);
|
||||
|
||||
@@ -309,7 +313,7 @@ impl SelectedTab {
|
||||
Example::new(&[Length(20), Max(20)]).render(example3, buf);
|
||||
}
|
||||
|
||||
fn render_percentage_example(&self, area: Rect, buf: &mut Buffer) {
|
||||
fn render_percentage_example(area: Rect, buf: &mut Buffer) {
|
||||
let [example1, example2, example3, example4, example5, _] =
|
||||
Layout::vertical([Length(EXAMPLE_HEIGHT); 6]).areas(area);
|
||||
|
||||
@@ -320,7 +324,7 @@ impl SelectedTab {
|
||||
Example::new(&[Percentage(0), Fill(0)]).render(example5, buf);
|
||||
}
|
||||
|
||||
fn render_ratio_example(&self, area: Rect, buf: &mut Buffer) {
|
||||
fn render_ratio_example(area: Rect, buf: &mut Buffer) {
|
||||
let [example1, example2, example3, example4, _] =
|
||||
Layout::vertical([Length(EXAMPLE_HEIGHT); 5]).areas(area);
|
||||
|
||||
@@ -330,14 +334,14 @@ impl SelectedTab {
|
||||
Example::new(&[Ratio(1, 2), Percentage(25), Length(10)]).render(example4, buf);
|
||||
}
|
||||
|
||||
fn render_fill_example(&self, area: Rect, buf: &mut Buffer) {
|
||||
fn render_fill_example(area: Rect, buf: &mut Buffer) {
|
||||
let [example1, example2, _] = Layout::vertical([Length(EXAMPLE_HEIGHT); 3]).areas(area);
|
||||
|
||||
Example::new(&[Fill(1), Fill(2), Fill(3)]).render(example1, buf);
|
||||
Example::new(&[Fill(1), Percentage(50), Fill(1)]).render(example2, buf);
|
||||
}
|
||||
|
||||
fn render_min_example(&self, area: Rect, buf: &mut Buffer) {
|
||||
fn render_min_example(area: Rect, buf: &mut Buffer) {
|
||||
let [example1, example2, example3, example4, example5, _] =
|
||||
Layout::vertical([Length(EXAMPLE_HEIGHT); 6]).areas(area);
|
||||
|
||||
@@ -348,7 +352,7 @@ impl SelectedTab {
|
||||
Example::new(&[Percentage(100), Min(80)]).render(example5, buf);
|
||||
}
|
||||
|
||||
fn render_max_example(&self, area: Rect, buf: &mut Buffer) {
|
||||
fn render_max_example(area: Rect, buf: &mut Buffer) {
|
||||
let [example1, example2, example3, example4, example5, _] =
|
||||
Layout::vertical([Length(EXAMPLE_HEIGHT); 6]).areas(area);
|
||||
|
||||
@@ -379,14 +383,13 @@ impl Widget for Example {
|
||||
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);
|
||||
Self::illustration(*constraint, block.width).render(*block, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Example {
|
||||
fn illustration(&self, constraint: Constraint, width: u16) -> Paragraph {
|
||||
fn illustration(constraint: Constraint, width: u16) -> impl Widget {
|
||||
let color = match constraint {
|
||||
Constraint::Length(_) => LENGTH_COLOR,
|
||||
Constraint::Percentage(_) => PERCENTAGE_COLOR,
|
||||
@@ -417,7 +420,7 @@ fn init_error_hooks() -> Result<()> {
|
||||
}))?;
|
||||
std::panic::set_hook(Box::new(move |info| {
|
||||
let _ = restore_terminal();
|
||||
panic(info)
|
||||
panic(info);
|
||||
}));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ use crossterm::{
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
use ratatui::{prelude::*, widgets::Paragraph};
|
||||
|
||||
/// A custom widget that renders a button with a label, theme and state.
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -71,7 +71,7 @@ const GREEN: Theme = Theme {
|
||||
|
||||
/// A button with a label that can be themed.
|
||||
impl<'a> Button<'a> {
|
||||
pub fn new<T: Into<Line<'a>>>(label: T) -> Button<'a> {
|
||||
pub fn new<T: Into<Line<'a>>>(label: T) -> Self {
|
||||
Button {
|
||||
label: label.into(),
|
||||
theme: BLUE,
|
||||
@@ -79,18 +79,19 @@ impl<'a> Button<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn theme(mut self, theme: Theme) -> Button<'a> {
|
||||
pub const fn theme(mut self, theme: Theme) -> Self {
|
||||
self.theme = theme;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn state(mut self, state: State) -> Button<'a> {
|
||||
pub const fn state(mut self, state: State) -> Self {
|
||||
self.state = state;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for Button<'a> {
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let (background, text, shadow, highlight) = self.colors();
|
||||
buf.set_style(area, Style::new().bg(background).fg(text));
|
||||
@@ -124,7 +125,7 @@ impl<'a> Widget for Button<'a> {
|
||||
}
|
||||
|
||||
impl Button<'_> {
|
||||
fn colors(&self) -> (Color, Color, Color, Color) {
|
||||
const fn colors(&self) -> (Color, Color, Color, Color) {
|
||||
let theme = self.theme;
|
||||
match self.state {
|
||||
State::Normal => (theme.background, theme.text, theme.shadow, theme.highlight),
|
||||
@@ -163,7 +164,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
|
||||
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
|
||||
let mut selected_button: usize = 0;
|
||||
let button_states = &mut [State::Selected, State::Normal, State::Normal];
|
||||
let mut button_states = [State::Selected, State::Normal, State::Normal];
|
||||
loop {
|
||||
terminal.draw(|frame| ui(frame, button_states))?;
|
||||
if !event::poll(Duration::from_millis(100))? {
|
||||
@@ -174,18 +175,20 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
|
||||
if key.kind != event::KeyEventKind::Press {
|
||||
continue;
|
||||
}
|
||||
if handle_key_event(key, button_states, &mut selected_button).is_break() {
|
||||
if handle_key_event(key, &mut button_states, &mut selected_button).is_break() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse) => handle_mouse_event(mouse, button_states, &mut selected_button),
|
||||
Event::Mouse(mouse) => {
|
||||
handle_mouse_event(mouse, &mut button_states, &mut selected_button);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ui(frame: &mut Frame, states: &[State; 3]) {
|
||||
fn ui(frame: &mut Frame, states: [State; 3]) {
|
||||
let vertical = Layout::vertical([
|
||||
Constraint::Length(1),
|
||||
Constraint::Max(3),
|
||||
@@ -202,7 +205,7 @@ fn ui(frame: &mut Frame, states: &[State; 3]) {
|
||||
frame.render_widget(Paragraph::new("←/→: select, Space: toggle, q: quit"), help);
|
||||
}
|
||||
|
||||
fn render_buttons(frame: &mut Frame<'_>, area: Rect, states: &[State; 3]) {
|
||||
fn render_buttons(frame: &mut Frame<'_>, area: Rect, states: [State; 3]) {
|
||||
let horizontal = Layout::horizontal([
|
||||
Constraint::Length(15),
|
||||
Constraint::Length(15),
|
||||
|
||||
@@ -2,7 +2,7 @@ use rand::{
|
||||
distributions::{Distribution, Uniform},
|
||||
rngs::ThreadRng,
|
||||
};
|
||||
use ratatui::widgets::*;
|
||||
use ratatui::widgets::ListState;
|
||||
|
||||
const TASKS: [&str; 24] = [
|
||||
"Item1", "Item2", "Item3", "Item4", "Item5", "Item6", "Item7", "Item8", "Item9", "Item10",
|
||||
@@ -73,8 +73,8 @@ pub struct RandomSignal {
|
||||
}
|
||||
|
||||
impl RandomSignal {
|
||||
pub fn new(lower: u64, upper: u64) -> RandomSignal {
|
||||
RandomSignal {
|
||||
pub fn new(lower: u64, upper: u64) -> Self {
|
||||
Self {
|
||||
distribution: Uniform::new(lower, upper),
|
||||
rng: rand::thread_rng(),
|
||||
}
|
||||
@@ -97,8 +97,8 @@ pub struct SinSignal {
|
||||
}
|
||||
|
||||
impl SinSignal {
|
||||
pub fn new(interval: f64, period: f64, scale: f64) -> SinSignal {
|
||||
SinSignal {
|
||||
pub const fn new(interval: f64, period: f64, scale: f64) -> Self {
|
||||
Self {
|
||||
x: 0.0,
|
||||
interval,
|
||||
period,
|
||||
@@ -144,8 +144,8 @@ pub struct StatefulList<T> {
|
||||
}
|
||||
|
||||
impl<T> StatefulList<T> {
|
||||
pub fn with_items(items: Vec<T>) -> StatefulList<T> {
|
||||
StatefulList {
|
||||
pub fn with_items(items: Vec<T>) -> Self {
|
||||
Self {
|
||||
state: ListState::default(),
|
||||
items,
|
||||
}
|
||||
@@ -191,9 +191,7 @@ where
|
||||
S: Iterator,
|
||||
{
|
||||
fn on_tick(&mut self) {
|
||||
for _ in 0..self.tick_rate {
|
||||
self.points.remove(0);
|
||||
}
|
||||
self.points.drain(0..self.tick_rate);
|
||||
self.points
|
||||
.extend(self.source.by_ref().take(self.tick_rate));
|
||||
}
|
||||
@@ -237,7 +235,7 @@ pub struct App<'a> {
|
||||
}
|
||||
|
||||
impl<'a> App<'a> {
|
||||
pub fn new(title: &'a str, enhanced_graphics: bool) -> App<'a> {
|
||||
pub fn new(title: &'a str, enhanced_graphics: bool) -> Self {
|
||||
let mut rand_signal = RandomSignal::new(0, 100);
|
||||
let sparkline_points = rand_signal.by_ref().take(300).collect();
|
||||
let mut sin_signal = SinSignal::new(0.2, 3.0, 18.0);
|
||||
|
||||
@@ -4,7 +4,10 @@ use std::{
|
||||
};
|
||||
|
||||
use ratatui::prelude::*;
|
||||
use termwiz::{input::*, terminal::Terminal as TermwizTerminal};
|
||||
use termwiz::{
|
||||
input::{InputEvent, KeyCode},
|
||||
terminal::Terminal as TermwizTerminal,
|
||||
};
|
||||
|
||||
use crate::{app::App, ui};
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#[allow(clippy::wildcard_imports)]
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{canvas::*, *},
|
||||
@@ -13,7 +14,7 @@ pub fn draw(f: &mut Frame, app: &mut App) {
|
||||
.iter()
|
||||
.map(|t| text::Line::from(Span::styled(*t, Style::default().fg(Color::Green))))
|
||||
.collect::<Tabs>()
|
||||
.block(Block::default().borders(Borders::ALL).title(app.title))
|
||||
.block(Block::bordered().title(app.title))
|
||||
.highlight_style(Style::default().fg(Color::Yellow))
|
||||
.select(app.tabs.index);
|
||||
f.render_widget(tabs, chunks[0]);
|
||||
@@ -45,12 +46,12 @@ fn draw_gauges(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
])
|
||||
.margin(1)
|
||||
.split(area);
|
||||
let block = Block::default().borders(Borders::ALL).title("Graphs");
|
||||
let block = Block::bordered().title("Graphs");
|
||||
f.render_widget(block, area);
|
||||
|
||||
let label = format!("{:.2}%", app.progress * 100.0);
|
||||
let gauge = Gauge::default()
|
||||
.block(Block::default().title("Gauge:"))
|
||||
.block(Block::new().title("Gauge:"))
|
||||
.gauge_style(
|
||||
Style::default()
|
||||
.fg(Color::Magenta)
|
||||
@@ -63,7 +64,7 @@ fn draw_gauges(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
f.render_widget(gauge, chunks[0]);
|
||||
|
||||
let sparkline = Sparkline::default()
|
||||
.block(Block::default().title("Sparkline:"))
|
||||
.block(Block::new().title("Sparkline:"))
|
||||
.style(Style::default().fg(Color::Green))
|
||||
.data(&app.sparkline.points)
|
||||
.bar_set(if app.enhanced_graphics {
|
||||
@@ -74,7 +75,7 @@ fn draw_gauges(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
f.render_widget(sparkline, chunks[1]);
|
||||
|
||||
let line_gauge = LineGauge::default()
|
||||
.block(Block::default().title("LineGauge:"))
|
||||
.block(Block::new().title("LineGauge:"))
|
||||
.gauge_style(Style::default().fg(Color::Magenta))
|
||||
.line_set(if app.enhanced_graphics {
|
||||
symbols::line::THICK
|
||||
@@ -85,6 +86,7 @@ fn draw_gauges(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
f.render_widget(line_gauge, chunks[2]);
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn draw_charts(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
let constraints = if app.show_chart {
|
||||
vec![Constraint::Percentage(50), Constraint::Percentage(50)]
|
||||
@@ -108,7 +110,7 @@ fn draw_charts(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
.map(|i| ListItem::new(vec![text::Line::from(Span::raw(*i))]))
|
||||
.collect();
|
||||
let tasks = List::new(tasks)
|
||||
.block(Block::default().borders(Borders::ALL).title("List"))
|
||||
.block(Block::bordered().title("List"))
|
||||
.highlight_style(Style::default().add_modifier(Modifier::BOLD))
|
||||
.highlight_symbol("> ");
|
||||
f.render_stateful_widget(tasks, chunks[0], &mut app.tasks.state);
|
||||
@@ -136,12 +138,12 @@ fn draw_charts(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
ListItem::new(content)
|
||||
})
|
||||
.collect();
|
||||
let logs = List::new(logs).block(Block::default().borders(Borders::ALL).title("List"));
|
||||
let logs = List::new(logs).block(Block::bordered().title("List"));
|
||||
f.render_stateful_widget(logs, chunks[1], &mut app.logs.state);
|
||||
}
|
||||
|
||||
let barchart = BarChart::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("Bar chart"))
|
||||
.block(Block::bordered().title("Bar chart"))
|
||||
.data(&app.barchart)
|
||||
.bar_width(3)
|
||||
.bar_gap(2)
|
||||
@@ -193,14 +195,12 @@ fn draw_charts(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
];
|
||||
let chart = Chart::new(datasets)
|
||||
.block(
|
||||
Block::default()
|
||||
.title(Span::styled(
|
||||
"Chart",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
))
|
||||
.borders(Borders::ALL),
|
||||
Block::bordered().title(Span::styled(
|
||||
"Chart",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)),
|
||||
)
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
@@ -252,7 +252,7 @@ fn draw_text(f: &mut Frame, area: Rect) {
|
||||
"One more thing is that it should display unicode characters: 10€"
|
||||
),
|
||||
];
|
||||
let block = Block::default().borders(Borders::ALL).title(Span::styled(
|
||||
let block = Block::bordered().title(Span::styled(
|
||||
"Footer",
|
||||
Style::default()
|
||||
.fg(Color::Magenta)
|
||||
@@ -290,11 +290,11 @@ fn draw_second_tab(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.bottom_margin(1),
|
||||
)
|
||||
.block(Block::default().title("Servers").borders(Borders::ALL));
|
||||
.block(Block::bordered().title("Servers"));
|
||||
f.render_widget(table, chunks[0]);
|
||||
|
||||
let map = Canvas::default()
|
||||
.block(Block::default().title("World").borders(Borders::ALL))
|
||||
.block(Block::bordered().title("World"))
|
||||
.paint(|ctx| {
|
||||
ctx.draw(&Map {
|
||||
color: Color::White,
|
||||
@@ -388,6 +388,6 @@ fn draw_third_tab(f: &mut Frame, _app: &mut App, area: Rect) {
|
||||
Constraint::Ratio(1, 3),
|
||||
],
|
||||
)
|
||||
.block(Block::default().title("Colors").borders(Borders::ALL));
|
||||
.block(Block::bordered().title("Colors"));
|
||||
f.render_widget(table, chunks[0]);
|
||||
}
|
||||
|
||||
@@ -38,14 +38,10 @@ enum Tab {
|
||||
}
|
||||
|
||||
pub fn run(terminal: &mut Terminal<impl Backend>) -> Result<()> {
|
||||
App::new().run(terminal)
|
||||
App::default().run(terminal)
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Run the app until the user quits.
|
||||
pub fn run(&mut self, terminal: &mut Terminal<impl Backend>) -> Result<()> {
|
||||
while self.is_running() {
|
||||
@@ -80,11 +76,12 @@ impl App {
|
||||
let timeout = Duration::from_secs_f64(1.0 / 50.0);
|
||||
match term::next_event(timeout)? {
|
||||
Some(Event::Key(key)) if key.kind == KeyEventKind::Press => self.handle_key_press(key),
|
||||
_ => Ok(()),
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_key_press(&mut self, key: KeyEvent) -> Result<()> {
|
||||
fn handle_key_press(&mut self, key: KeyEvent) {
|
||||
use KeyCode::*;
|
||||
match key.code {
|
||||
Char('q') | Esc => self.mode = Mode::Quit,
|
||||
@@ -93,10 +90,8 @@ impl App {
|
||||
Char('k') | Up => self.prev(),
|
||||
Char('j') | Down => self.next(),
|
||||
Char('d') | Delete => self.destroy(),
|
||||
|
||||
_ => {}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prev(&mut self) {
|
||||
@@ -124,11 +119,11 @@ impl App {
|
||||
}
|
||||
|
||||
fn next_tab(&mut self) {
|
||||
self.tab = self.tab.next()
|
||||
self.tab = self.tab.next();
|
||||
}
|
||||
|
||||
fn destroy(&mut self) {
|
||||
self.mode = Mode::Destroy
|
||||
self.mode = Mode::Destroy;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,7 +142,7 @@ impl Widget for &App {
|
||||
Block::new().style(THEME.root).render(area, buf);
|
||||
self.render_title_bar(title_bar, buf);
|
||||
self.render_selected_tab(tab, buf);
|
||||
self.render_bottom_bar(bottom_bar, buf);
|
||||
App::render_bottom_bar(bottom_bar, buf);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,7 +152,7 @@ impl App {
|
||||
let [title, tabs] = layout.areas(area);
|
||||
|
||||
Span::styled("Ratatui", THEME.app_title).render(title, buf);
|
||||
let titles = Tab::iter().map(|tab| tab.title());
|
||||
let titles = Tab::iter().map(Tab::title);
|
||||
Tabs::new(titles)
|
||||
.style(THEME.tabs)
|
||||
.highlight_style(THEME.tabs_selected)
|
||||
@@ -177,7 +172,7 @@ impl App {
|
||||
};
|
||||
}
|
||||
|
||||
fn render_bottom_bar(&self, area: Rect, buf: &mut Buffer) {
|
||||
fn render_bottom_bar(area: Rect, buf: &mut Buffer) {
|
||||
let keys = [
|
||||
("H/←", "Left"),
|
||||
("L/→", "Right"),
|
||||
@@ -189,8 +184,8 @@ impl App {
|
||||
let spans = keys
|
||||
.iter()
|
||||
.flat_map(|(key, desc)| {
|
||||
let key = Span::styled(format!(" {} ", key), THEME.key_binding.key);
|
||||
let desc = Span::styled(format!(" {} ", desc), THEME.key_binding.description);
|
||||
let key = Span::styled(format!(" {key} "), THEME.key_binding.key);
|
||||
let desc = Span::styled(format!(" {desc} "), THEME.key_binding.description);
|
||||
[key, desc]
|
||||
})
|
||||
.collect_vec();
|
||||
@@ -202,22 +197,22 @@ impl App {
|
||||
}
|
||||
|
||||
impl Tab {
|
||||
fn next(&self) -> Self {
|
||||
let current_index = *self as usize;
|
||||
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)
|
||||
Self::from_repr(next_index).unwrap_or(self)
|
||||
}
|
||||
|
||||
fn prev(&self) -> Self {
|
||||
let current_index = *self as usize;
|
||||
fn prev(self) -> Self {
|
||||
let current_index = self as usize;
|
||||
let prev_index = current_index.saturating_sub(1);
|
||||
Self::from_repr(prev_index).unwrap_or(*self)
|
||||
Self::from_repr(prev_index).unwrap_or(self)
|
||||
}
|
||||
|
||||
fn title(&self) -> String {
|
||||
fn title(self) -> String {
|
||||
match self {
|
||||
Tab::About => "".to_string(),
|
||||
tab => format!(" {} ", tab),
|
||||
Self::About => String::new(),
|
||||
tab => format!(" {tab} "),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ use std::cmp::min;
|
||||
|
||||
use derive_builder::Builder;
|
||||
use font8x8::UnicodeFonts;
|
||||
use ratatui::{prelude::*, text::StyledGrapheme, widgets::Widget};
|
||||
use ratatui::{prelude::*, text::StyledGrapheme};
|
||||
|
||||
#[allow(unused)]
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Default)]
|
||||
@@ -129,24 +129,24 @@ pub struct BigText<'a> {
|
||||
|
||||
/// The size of single glyphs
|
||||
///
|
||||
/// Defaults to `BigTextSize::default()` (=> BigTextSize::Full)
|
||||
/// 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);
|
||||
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);
|
||||
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) {
|
||||
const fn cells_per_glyph(size: PixelSize) -> (u16, u16) {
|
||||
match size {
|
||||
PixelSize::Full => (8, 8),
|
||||
PixelSize::HalfHeight => (8, 4),
|
||||
@@ -160,7 +160,7 @@ fn cells_per_glyph(size: &PixelSize) -> (u16, u16) {
|
||||
/// The size of each cell depends on given font size
|
||||
fn layout(
|
||||
area: Rect,
|
||||
pixel_size: &PixelSize,
|
||||
pixel_size: PixelSize,
|
||||
) -> impl IntoIterator<Item = impl IntoIterator<Item = Rect>> {
|
||||
let (width, height) = cells_per_glyph(pixel_size);
|
||||
(area.top()..area.bottom())
|
||||
@@ -178,7 +178,7 @@ fn layout(
|
||||
|
||||
/// 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) {
|
||||
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) {
|
||||
@@ -187,7 +187,7 @@ fn render_symbol(grapheme: StyledGrapheme, area: Rect, buf: &mut Buffer, pixel_s
|
||||
}
|
||||
|
||||
/// Get the correct unicode symbol for two vertical "pixels"
|
||||
fn get_symbol_half_height(top: u8, bottom: u8) -> char {
|
||||
const fn get_symbol_half_height(top: u8, bottom: u8) -> char {
|
||||
match top {
|
||||
0 => match bottom {
|
||||
0 => ' ',
|
||||
@@ -201,7 +201,7 @@ fn get_symbol_half_height(top: u8, bottom: u8) -> char {
|
||||
}
|
||||
|
||||
/// Get the correct unicode symbol for two horizontal "pixels"
|
||||
fn get_symbol_half_width(left: u8, right: u8) -> char {
|
||||
const fn get_symbol_half_width(left: u8, right: u8) -> char {
|
||||
match left {
|
||||
0 => match right {
|
||||
0 => ' ',
|
||||
@@ -215,20 +215,26 @@ fn get_symbol_half_width(left: u8, right: u8) -> char {
|
||||
}
|
||||
|
||||
/// 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 fn get_symbol_half_size(
|
||||
top_left: u8,
|
||||
top_right: u8,
|
||||
bottom_left: u8,
|
||||
bottom_right: u8,
|
||||
) -> char {
|
||||
const QUADRANT_SYMBOLS: [char; 16] = [
|
||||
' ', '▘', '▝', '▀', '▖', '▌', '▞', '▛', '▗', '▚', '▐', '▜', '▄', '▙', '▟', '█',
|
||||
];
|
||||
QUADRANT_SYMBOLS[top_left + (top_right << 1) + (bottom_left << 2) + (bottom_right << 3)]
|
||||
|
||||
let top_left = if top_left > 0 { 1 } else { 0 };
|
||||
let top_right = if top_right > 0 { 1 << 1 } else { 0 };
|
||||
let bottom_left = if bottom_left > 0 { 1 << 2 } else { 0 };
|
||||
let bottom_right = if bottom_right > 0 { 1 << 3 } else { 0 };
|
||||
|
||||
QUADRANT_SYMBOLS[top_left + top_right + bottom_left + bottom_right]
|
||||
}
|
||||
|
||||
/// Render a single 8x8 glyph into a cell by setting the corresponding cells in the buffer.
|
||||
fn render_glyph(glyph: [u8; 8], area: Rect, buf: &mut Buffer, pixel_size: &PixelSize) {
|
||||
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);
|
||||
@@ -270,8 +276,6 @@ fn render_glyph(glyph: [u8; 8], area: Rect, buf: &mut Buffer, pixel_size: &Pixel
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ratatui::assert_buffer_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||
@@ -302,7 +306,7 @@ mod tests {
|
||||
.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![
|
||||
let expected = Buffer::with_lines([
|
||||
" ████ ██ ███ ████ ██ ",
|
||||
"██ ██ ██ ██ ",
|
||||
"███ ███ █████ ███ ██ ██ ████ ██ ███ █████ ████ ",
|
||||
@@ -312,7 +316,7 @@ mod tests {
|
||||
" ████ ████ ██ ██ ██ ████ ████ ███████ ████ ██ ██ ████ ",
|
||||
" █████ ",
|
||||
]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -323,7 +327,7 @@ mod tests {
|
||||
.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![
|
||||
let expected = Buffer::with_lines([
|
||||
"██████ █ ███",
|
||||
"█ ██ █ ██ ██",
|
||||
" ██ ██ ███ ██ ██ █████ ████ ████ █████ ████ ██",
|
||||
@@ -331,7 +335,7 @@ mod tests {
|
||||
" ██ ██ ██ ██ ██ ██ ██ ██ █████ ██ ██████ ██ ██",
|
||||
" ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ █ ██ ██ ██",
|
||||
]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -342,7 +346,7 @@ mod tests {
|
||||
.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![
|
||||
let expected = Buffer::with_lines([
|
||||
"██ ██ ███ █ ██ ",
|
||||
"███ ███ ██ ██ ",
|
||||
"███████ ██ ██ ██ █████ ███ ",
|
||||
@@ -360,7 +364,7 @@ mod tests {
|
||||
"███████ ████ ██ ██ ████ █████ ",
|
||||
" ",
|
||||
]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -372,18 +376,17 @@ mod tests {
|
||||
.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![
|
||||
" ████ █ ███ ███ ",
|
||||
"██ ██ ██ ██ ██ ",
|
||||
"███ █████ ██ ██ ██ ████ ██ ",
|
||||
" ███ ██ ██ ██ ██ ██ ██ █████ ",
|
||||
" ███ ██ ██ ██ ██ ██████ ██ ██ ",
|
||||
"██ ██ ██ █ █████ ██ ██ ██ ██ ",
|
||||
" ████ ██ ██ ████ ████ ███ ██ ",
|
||||
" █████ ",
|
||||
let expected = Buffer::with_lines([
|
||||
" ████ █ ███ ███ ".bold(),
|
||||
"██ ██ ██ ██ ██ ".bold(),
|
||||
"███ █████ ██ ██ ██ ████ ██ ".bold(),
|
||||
" ███ ██ ██ ██ ██ ██ ██ █████ ".bold(),
|
||||
" ███ ██ ██ ██ ██ ██████ ██ ██ ".bold(),
|
||||
"██ ██ ██ █ █████ ██ ██ ██ ██ ".bold(),
|
||||
" ████ ██ ██ ████ ████ ███ ██ ".bold(),
|
||||
" █████ ".bold(),
|
||||
]);
|
||||
expected.set_style(Rect::new(0, 0, 48, 8), Style::new().bold());
|
||||
assert_buffer_eq!(buf, expected);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -398,7 +401,7 @@ mod tests {
|
||||
.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![
|
||||
let mut expected = Buffer::with_lines([
|
||||
"██████ ███ ",
|
||||
" ██ ██ ██ ",
|
||||
" ██ ██ ████ ██ ",
|
||||
@@ -427,7 +430,7 @@ mod tests {
|
||||
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);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -439,13 +442,13 @@ mod tests {
|
||||
.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![
|
||||
let expected = Buffer::with_lines([
|
||||
"▄█▀▀█▄ ▀▀ ▀██ ▀██▀ ▀▀ ",
|
||||
"▀██▄ ▀██ ██▀▀█▄ ▄█▀▀▄█▀ ██ ▄█▀▀█▄ ██ ▀██ ██▀▀█▄ ▄█▀▀█▄ ",
|
||||
"▄▄ ▀██ ██ ██ ██ ▀█▄▄██ ██ ██▀▀▀▀ ██ ▄█ ██ ██ ██ ██▀▀▀▀ ",
|
||||
" ▀▀▀▀ ▀▀▀▀ ▀▀ ▀▀ ▄▄▄▄█▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀▀▀▀ ▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀ ",
|
||||
]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -457,12 +460,12 @@ mod tests {
|
||||
.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![
|
||||
let expected = Buffer::with_lines([
|
||||
"█▀██▀█ ▄█ ▀██",
|
||||
" ██ ▀█▄█▀█▄ ██ ██ ██▀▀█▄ ▄█▀▀█▄ ▀▀▀█▄ ▀██▀▀ ▄█▀▀█▄ ▄▄▄██",
|
||||
" ██ ██ ▀▀ ██ ██ ██ ██ ██ ▄▄ ▄█▀▀██ ██ ▄ ██▀▀▀▀ ██ ██",
|
||||
]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -474,7 +477,7 @@ mod tests {
|
||||
.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![
|
||||
let expected = Buffer::with_lines([
|
||||
"██▄ ▄██ ▀██ ▄█ ▀▀ ",
|
||||
"███████ ██ ██ ██ ▀██▀▀ ▀██ ",
|
||||
"██ ▀ ██ ██ ██ ██ ██ ▄ ██ ",
|
||||
@@ -484,7 +487,7 @@ mod tests {
|
||||
" ██ ▄█ ██ ██ ██ ██▀▀▀▀ ▀▀▀█▄ ",
|
||||
"▀▀▀▀▀▀▀ ▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀ ▀▀▀▀▀ ",
|
||||
]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -497,14 +500,13 @@ mod tests {
|
||||
.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![
|
||||
"▄█▀▀█▄ ▄█ ▀██ ▀██ ",
|
||||
"▀██▄ ▀██▀▀ ██ ██ ██ ▄█▀▀█▄ ▄▄▄██ ",
|
||||
"▄▄ ▀██ ██ ▄ ▀█▄▄██ ██ ██▀▀▀▀ ██ ██ ",
|
||||
" ▀▀▀▀ ▀▀ ▄▄▄▄█▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀ ▀▀ ",
|
||||
let expected = Buffer::with_lines([
|
||||
"▄█▀▀█▄ ▄█ ▀██ ▀██ ".bold(),
|
||||
"▀██▄ ▀██▀▀ ██ ██ ██ ▄█▀▀█▄ ▄▄▄██ ".bold(),
|
||||
"▄▄ ▀██ ██ ▄ ▀█▄▄██ ██ ██▀▀▀▀ ██ ██ ".bold(),
|
||||
" ▀▀▀▀ ▀▀ ▄▄▄▄█▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀ ▀▀ ".bold(),
|
||||
]);
|
||||
expected.set_style(Rect::new(0, 0, 48, 4), Style::new().bold());
|
||||
assert_buffer_eq!(buf, expected);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -520,7 +522,7 @@ mod tests {
|
||||
.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![
|
||||
let mut expected = Buffer::with_lines([
|
||||
"▀██▀▀█▄ ▀██ ",
|
||||
" ██▄▄█▀ ▄█▀▀█▄ ▄▄▄██ ",
|
||||
" ██ ▀█▄ ██▀▀▀▀ ██ ██ ",
|
||||
@@ -537,7 +539,7 @@ mod tests {
|
||||
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);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -549,7 +551,7 @@ mod tests {
|
||||
.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![
|
||||
let expected = Buffer::with_lines([
|
||||
"▐█▌ █ ▐█ ██ █ ",
|
||||
"█ █ █ ▐▌ ",
|
||||
"█▌ ▐█ ██▌ ▐█▐▌ █ ▐█▌ ▐▌ ▐█ ██▌ ▐█▌ ",
|
||||
@@ -559,7 +561,7 @@ mod tests {
|
||||
"▐█▌ ▐█▌ █ █ █ ▐█▌ ▐█▌ ███▌▐█▌ █ █ ▐█▌ ",
|
||||
" ██▌ ",
|
||||
]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -571,7 +573,7 @@ mod tests {
|
||||
.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![
|
||||
let expected = Buffer::with_lines([
|
||||
"███ ▐ ▐█",
|
||||
"▌█▐ █ █",
|
||||
" █ █▐█ █ █ ██▌ ▐█▌ ▐█▌ ▐██ ▐█▌ █",
|
||||
@@ -579,7 +581,7 @@ mod tests {
|
||||
" █ ▐▌▐▌█ █ █ █ █ ▐██ █ ███ █ █",
|
||||
" █ ▐▌ █ █ █ █ █ █ █ █ █▐ █ █ █",
|
||||
]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -591,7 +593,7 @@ mod tests {
|
||||
.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![
|
||||
let expected = Buffer::with_lines([
|
||||
"█ ▐▌ ▐█ ▐ █ ",
|
||||
"█▌█▌ █ █ ",
|
||||
"███▌█ █ █ ▐██ ▐█ ",
|
||||
@@ -609,7 +611,7 @@ mod tests {
|
||||
"███▌▐█▌ █ █ ▐█▌ ██▌ ",
|
||||
" ",
|
||||
]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -622,18 +624,17 @@ mod tests {
|
||||
.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![
|
||||
"▐█▌ ▐ ▐█ ▐█ ",
|
||||
"█ █ █ █ █ ",
|
||||
"█▌ ▐██ █ █ █ ▐█▌ █ ",
|
||||
"▐█ █ █ █ █ █ █ ▐██ ",
|
||||
" ▐█ █ █ █ █ ███ █ █ ",
|
||||
"█ █ █▐ ▐██ █ █ █ █ ",
|
||||
"▐█▌ ▐▌ █ ▐█▌ ▐█▌ ▐█▐▌",
|
||||
" ██▌ ",
|
||||
let expected = Buffer::with_lines([
|
||||
"▐█▌ ▐ ▐█ ▐█ ".bold(),
|
||||
"█ █ █ █ █ ".bold(),
|
||||
"█▌ ▐██ █ █ █ ▐█▌ █ ".bold(),
|
||||
"▐█ █ █ █ █ █ █ ▐██ ".bold(),
|
||||
" ▐█ █ █ █ █ ███ █ █ ".bold(),
|
||||
"█ █ █▐ ▐██ █ █ █ █ ".bold(),
|
||||
"▐█▌ ▐▌ █ ▐█▌ ▐█▌ ▐█▐▌".bold(),
|
||||
" ██▌ ".bold(),
|
||||
]);
|
||||
expected.set_style(Rect::new(0, 0, 24, 8), Style::new().bold());
|
||||
assert_buffer_eq!(buf, expected);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -649,7 +650,7 @@ mod tests {
|
||||
.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![
|
||||
let mut expected = Buffer::with_lines([
|
||||
"███ ▐█ ",
|
||||
"▐▌▐▌ █ ",
|
||||
"▐▌▐▌▐█▌ █ ",
|
||||
@@ -678,7 +679,7 @@ mod tests {
|
||||
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);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -711,13 +712,13 @@ mod tests {
|
||||
.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![
|
||||
let expected = Buffer::with_lines([
|
||||
"▟▀▙ ▀ ▝█ ▜▛ ▀ ",
|
||||
"▜▙ ▝█ █▀▙ ▟▀▟▘ █ ▟▀▙ ▐▌ ▝█ █▀▙ ▟▀▙ ",
|
||||
"▄▝█ █ █ █ ▜▄█ █ █▀▀ ▐▌▗▌ █ █ █ █▀▀ ",
|
||||
"▝▀▘ ▝▀▘ ▀ ▀ ▄▄▛ ▝▀▘ ▝▀▘ ▀▀▀▘▝▀▘ ▀ ▀ ▝▀▘ ",
|
||||
]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -729,12 +730,12 @@ mod tests {
|
||||
.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![
|
||||
let expected = Buffer::with_lines([
|
||||
"▛█▜ ▟ ▝█",
|
||||
" █ ▜▟▜▖█ █ █▀▙ ▟▀▙ ▝▀▙ ▝█▀ ▟▀▙ ▗▄█",
|
||||
" █ ▐▌▝▘█ █ █ █ █ ▄ ▟▀█ █▗ █▀▀ █ █",
|
||||
]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -746,7 +747,7 @@ mod tests {
|
||||
.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![
|
||||
let expected = Buffer::with_lines([
|
||||
"█▖▟▌ ▝█ ▟ ▀ ",
|
||||
"███▌█ █ █ ▝█▀ ▝█ ",
|
||||
"█▝▐▌█ █ █ █▗ █ ",
|
||||
@@ -756,7 +757,7 @@ mod tests {
|
||||
"▐▌▗▌ █ █ █ █▀▀ ▝▀▙ ",
|
||||
"▀▀▀▘▝▀▘ ▀ ▀ ▝▀▘ ▀▀▘ ",
|
||||
]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -769,14 +770,13 @@ mod tests {
|
||||
.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![
|
||||
"▟▀▙ ▟ ▝█ ▝█ ",
|
||||
"▜▙ ▝█▀ █ █ █ ▟▀▙ ▗▄█ ",
|
||||
"▄▝█ █▗ ▜▄█ █ █▀▀ █ █ ",
|
||||
"▝▀▘ ▝▘ ▄▄▛ ▝▀▘ ▝▀▘ ▝▀▝▘",
|
||||
let expected = Buffer::with_lines([
|
||||
"▟▀▙ ▟ ▝█ ▝█ ".bold(),
|
||||
"▜▙ ▝█▀ █ █ █ ▟▀▙ ▗▄█ ".bold(),
|
||||
"▄▝█ █▗ ▜▄█ █ █▀▀ █ █ ".bold(),
|
||||
"▝▀▘ ▝▘ ▄▄▛ ▝▀▘ ▝▀▘ ▝▀▝▘".bold(),
|
||||
]);
|
||||
expected.set_style(Rect::new(0, 0, 24, 4), Style::new().bold());
|
||||
assert_buffer_eq!(buf, expected);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -792,7 +792,7 @@ mod tests {
|
||||
.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![
|
||||
let mut expected = Buffer::with_lines([
|
||||
"▜▛▜▖ ▝█ ",
|
||||
"▐▙▟▘▟▀▙ ▗▄█ ",
|
||||
"▐▌▜▖█▀▀ █ █ ",
|
||||
@@ -809,7 +809,7 @@ mod tests {
|
||||
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);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,13 +9,14 @@ use ratatui::prelude::*;
|
||||
pub struct RgbSwatch;
|
||||
|
||||
impl Widget for RgbSwatch {
|
||||
#[allow(clippy::cast_precision_loss, clippy::similar_names)]
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
for (yi, y) in (area.top()..area.bottom()).enumerate() {
|
||||
let value = area.height as f32 - yi as f32;
|
||||
let value_fg = value / (area.height as f32);
|
||||
let value_bg = (value - 0.5) / (area.height as f32);
|
||||
let value = f32::from(area.height) - yi as f32;
|
||||
let value_fg = value / f32::from(area.height);
|
||||
let value_bg = (value - 0.5) / f32::from(area.height);
|
||||
for (xi, x) in (area.left()..area.right()).enumerate() {
|
||||
let hue = xi as f32 * 360.0 / area.width as f32;
|
||||
let hue = xi as f32 * 360.0 / f32::from(area.width);
|
||||
let fg = color_from_oklab(hue, Okhsv::max_saturation(), value_fg);
|
||||
let bg = color_from_oklab(hue, Okhsv::max_saturation(), value_bg);
|
||||
buf.get_mut(x, y).set_char('▀').set_fg(fg).set_bg(bg);
|
||||
@@ -24,7 +25,7 @@ impl Widget for RgbSwatch {
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a hue and value into an RGB color via the OkLab color space.
|
||||
/// Convert a hue and value into an RGB color via the Oklab color space.
|
||||
///
|
||||
/// See <https://bottosson.github.io/posts/oklab/> for more details.
|
||||
pub fn color_from_oklab(hue: f32, saturation: f32, value: f32) -> Color {
|
||||
|
||||
@@ -30,11 +30,16 @@ pub fn destroy(frame: &mut Frame<'_>) {
|
||||
///
|
||||
/// Each pick some random pixels and move them each down one row. This is a very inefficient way to
|
||||
/// do this, but it works well enough for this demo.
|
||||
#[allow(
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::cast_precision_loss,
|
||||
clippy::cast_sign_loss
|
||||
)]
|
||||
fn drip(frame_count: usize, area: Rect, buf: &mut Buffer) {
|
||||
// a seeded rng as we have to move the same random pixels each frame
|
||||
let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(10);
|
||||
let ramp_frames = 450;
|
||||
let fractional_speed = frame_count as f64 / ramp_frames as f64;
|
||||
let fractional_speed = frame_count as f64 / f64::from(ramp_frames);
|
||||
let variable_speed = DRIP_SPEED as f64 * fractional_speed * fractional_speed * fractional_speed;
|
||||
let pixel_count = (frame_count as f64 * variable_speed).floor() as usize;
|
||||
for _ in 0..pixel_count {
|
||||
@@ -68,6 +73,7 @@ fn drip(frame_count: usize, area: Rect, buf: &mut Buffer) {
|
||||
}
|
||||
|
||||
/// draw some text fading in and out from black to red and back
|
||||
#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
|
||||
fn text(frame_count: usize, area: Rect, buf: &mut Buffer) {
|
||||
let sub_frame = frame_count.saturating_sub(TEXT_DELAY);
|
||||
if sub_frame == 0 {
|
||||
@@ -114,10 +120,13 @@ fn blend(mask_color: Color, cell_color: Color, percentage: f64) -> Color {
|
||||
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);
|
||||
let remain = 1.0 - percentage;
|
||||
|
||||
let red = f64::from(mask_red).mul_add(percentage, f64::from(cell_red) * remain);
|
||||
let green = f64::from(mask_green).mul_add(percentage, f64::from(cell_green) * remain);
|
||||
let blue = f64::from(mask_blue).mul_add(percentage, f64::from(cell_blue) * remain);
|
||||
|
||||
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
|
||||
Color::Rgb(red as u8, green as u8, blue as u8)
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ pub fn init_hooks() -> Result<()> {
|
||||
}))?;
|
||||
std::panic::set_hook(Box::new(move |info| {
|
||||
let _ = term::restore();
|
||||
panic(info)
|
||||
panic(info);
|
||||
}));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -13,6 +13,14 @@
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
#![allow(
|
||||
clippy::enum_glob_use,
|
||||
clippy::missing_errors_doc,
|
||||
clippy::module_name_repetitions,
|
||||
clippy::must_use_candidate,
|
||||
clippy::wildcard_imports
|
||||
)]
|
||||
|
||||
mod app;
|
||||
mod big_text;
|
||||
mod colors;
|
||||
@@ -31,7 +39,7 @@ pub use theme::*;
|
||||
fn main() -> Result<()> {
|
||||
errors::init_hooks()?;
|
||||
let terminal = &mut term::init()?;
|
||||
App::new().run(terminal)?;
|
||||
App::default().run(terminal)?;
|
||||
term::restore()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -97,10 +97,11 @@ fn render_crate_description(area: Rect, buf: &mut Buffer) {
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
/// Use half block characters to render a logo based on the RATATUI_LOGO const.
|
||||
/// Use half block characters to render a logo based on the `RATATUI_LOGO` const.
|
||||
///
|
||||
/// The logo is rendered in three colors, one for the rat, one for the terminal, and one for the
|
||||
/// rat's eye. The eye color alternates between two colors based on the selected row.
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
pub fn render_logo(selected_row: usize, area: Rect, buf: &mut Buffer) {
|
||||
let eye_color = if selected_row % 2 == 0 {
|
||||
THEME.logo.rat_eye
|
||||
|
||||
@@ -10,6 +10,7 @@ struct Ingredient {
|
||||
}
|
||||
|
||||
impl Ingredient {
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
fn height(&self) -> u16 {
|
||||
self.name.lines().count() as u16
|
||||
}
|
||||
@@ -148,7 +149,7 @@ fn render_recipe(area: Rect, buf: &mut Buffer) {
|
||||
|
||||
fn render_ingredients(selected_row: usize, area: Rect, buf: &mut Buffer) {
|
||||
let mut state = TableState::default().with_selected(Some(selected_row));
|
||||
let rows = INGREDIENTS.iter().cloned();
|
||||
let rows = INGREDIENTS.iter().copied();
|
||||
let theme = THEME.recipe;
|
||||
StatefulWidget::render(
|
||||
Table::new(rows, [Constraint::Length(7), Constraint::Length(30)])
|
||||
@@ -171,5 +172,5 @@ fn render_scrollbar(position: usize, area: Rect, buf: &mut Buffer) {
|
||||
.end_symbol(None)
|
||||
.track_symbol(None)
|
||||
.thumb_symbol("▐")
|
||||
.render(area, buf, &mut state)
|
||||
.render(area, buf, &mut state);
|
||||
}
|
||||
|
||||
@@ -49,10 +49,10 @@ fn render_hops(selected_row: usize, area: Rect, buf: &mut Buffer) {
|
||||
.iter()
|
||||
.map(|hop| Row::new(vec![hop.host, hop.address]))
|
||||
.collect_vec();
|
||||
let block = Block::default()
|
||||
.title("Traceroute bad.horse".bold().white())
|
||||
let block = Block::new()
|
||||
.padding(Padding::new(1, 1, 1, 1))
|
||||
.title_alignment(Alignment::Center)
|
||||
.padding(Padding::new(1, 1, 1, 1));
|
||||
.title("Traceroute bad.horse".bold().white());
|
||||
StatefulWidget::render(
|
||||
Table::new(rows, [Constraint::Max(100), Constraint::Length(15)])
|
||||
.header(Row::new(vec!["Host", "Address"]).set_style(THEME.traceroute.header))
|
||||
|
||||
@@ -84,7 +84,7 @@ fn render_simple_barchart(area: Rect, buf: &mut Buffer) {
|
||||
// This doesn't actually render correctly as the text is too wide for the bar
|
||||
// See https://github.com/ratatui-org/ratatui/issues/513 for more info
|
||||
// (the demo GIFs hack around this by hacking the calculation in bars.rs)
|
||||
.text_value(format!("{}°", value))
|
||||
.text_value(format!("{value}°"))
|
||||
.style(if value > 70 {
|
||||
Style::new().fg(Color::Red)
|
||||
} else {
|
||||
@@ -128,12 +128,14 @@ fn render_horizontal_barchart(area: Rect, buf: &mut Buffer) {
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
pub fn render_gauge(progress: usize, area: Rect, buf: &mut Buffer) {
|
||||
let percent = (progress * 3).min(100) as f64;
|
||||
|
||||
render_line_gauge(percent, area, buf);
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
fn render_line_gauge(percent: f64, area: Rect, buf: &mut Buffer) {
|
||||
// cycle color hue based on the percent for a neat effect yellow -> red
|
||||
let hue = 90.0 - (percent as f32 * 0.6);
|
||||
@@ -141,7 +143,7 @@ fn render_line_gauge(percent: f64, area: Rect, buf: &mut Buffer) {
|
||||
let fg = color_from_oklab(hue, Okhsv::max_saturation(), value);
|
||||
let bg = color_from_oklab(hue, Okhsv::max_saturation(), value * 0.5);
|
||||
let label = if percent < 100.0 {
|
||||
format!("Downloading: {}%", percent)
|
||||
format!("Downloading: {percent}%")
|
||||
} else {
|
||||
"Download Complete!".into()
|
||||
};
|
||||
|
||||
@@ -19,9 +19,12 @@ use crossterm::{
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{Block, Borders, Paragraph},
|
||||
};
|
||||
|
||||
/// Example code for libr.rs
|
||||
/// Example code for lib.rs
|
||||
///
|
||||
/// When cargo-rdme supports doc comments that import from code, this will be imported
|
||||
/// rather than copied to the lib.rs file.
|
||||
@@ -34,7 +37,6 @@ fn main() -> io::Result<()> {
|
||||
let mut should_quit = false;
|
||||
while !should_quit {
|
||||
terminal.draw(match arg.as_str() {
|
||||
"hello_world" => hello_world,
|
||||
"layout" => layout,
|
||||
"styling" => styling,
|
||||
_ => hello_world,
|
||||
@@ -49,8 +51,7 @@ fn main() -> io::Result<()> {
|
||||
|
||||
fn hello_world(frame: &mut Frame) {
|
||||
frame.render_widget(
|
||||
Paragraph::new("Hello World!")
|
||||
.block(Block::default().title("Greeting").borders(Borders::ALL)),
|
||||
Paragraph::new("Hello World!").block(Block::bordered().title("Greeting")),
|
||||
frame.size(),
|
||||
);
|
||||
}
|
||||
@@ -85,8 +86,8 @@ fn layout(frame: &mut Frame) {
|
||||
Block::new().borders(Borders::TOP).title("Status Bar"),
|
||||
status_bar,
|
||||
);
|
||||
frame.render_widget(Block::default().borders(Borders::ALL).title("Left"), left);
|
||||
frame.render_widget(Block::default().borders(Borders::ALL).title("Right"), right);
|
||||
frame.render_widget(Block::bordered().title("Left"), left);
|
||||
frame.render_widget(Block::bordered().title("Right"), right);
|
||||
}
|
||||
|
||||
fn styling(frame: &mut Frame) {
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
#![allow(clippy::enum_glob_use, clippy::wildcard_imports)]
|
||||
|
||||
use std::io::{self, stdout};
|
||||
|
||||
use color_eyre::{config::HookBuilder, Result};
|
||||
@@ -165,7 +167,7 @@ impl App {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_running(&self) -> bool {
|
||||
fn is_running(self) -> bool {
|
||||
self.state == AppState::Running
|
||||
}
|
||||
|
||||
@@ -203,14 +205,14 @@ impl App {
|
||||
}
|
||||
|
||||
fn up(&mut self) {
|
||||
self.scroll_offset = self.scroll_offset.saturating_sub(1)
|
||||
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())
|
||||
.min(max_scroll_offset());
|
||||
}
|
||||
|
||||
fn top(&mut self) {
|
||||
@@ -239,8 +241,7 @@ fn max_scroll_offset() -> u16 {
|
||||
example_height()
|
||||
- EXAMPLE_DATA
|
||||
.last()
|
||||
.map(|(desc, _)| get_description_height(desc) + 4)
|
||||
.unwrap_or(0)
|
||||
.map_or(0, |(desc, _)| get_description_height(desc) + 4)
|
||||
}
|
||||
|
||||
/// The height of all examples combined
|
||||
@@ -264,12 +265,12 @@ impl Widget for App {
|
||||
} else {
|
||||
axis.width
|
||||
};
|
||||
self.axis(axis_width, self.spacing).render(axis, buf);
|
||||
Self::axis(axis_width, self.spacing).render(axis, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn tabs(&self) -> impl Widget {
|
||||
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()))
|
||||
@@ -283,13 +284,13 @@ impl App {
|
||||
}
|
||||
|
||||
/// a bar like `<----- 80 px (gap: 2 px)? ----->`
|
||||
fn axis(&self, width: u16, spacing: u16) -> impl Widget {
|
||||
fn axis(width: u16, spacing: u16) -> impl Widget {
|
||||
let width = width as usize;
|
||||
// only show gap when spacing is not zero
|
||||
let label = if spacing != 0 {
|
||||
format!("{} px (gap: {} px)", width, spacing)
|
||||
format!("{width} px (gap: {spacing} px)")
|
||||
} else {
|
||||
format!("{} px", width)
|
||||
format!("{width} px")
|
||||
};
|
||||
let bar_width = width.saturating_sub(2); // we want to `<` and `>` at the ends
|
||||
let width_bar = format!("<{label:-^bar_width$}>");
|
||||
@@ -302,6 +303,7 @@ impl App {
|
||||
/// into the main buffer. This is done to make it possible to handle scrolling easily.
|
||||
///
|
||||
/// Returns bool indicating whether scroll was needed
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
fn render_demo(self, area: Rect, buf: &mut Buffer) -> bool {
|
||||
// render demo content into a separate buffer so all examples fit we add an extra
|
||||
// area.height to make sure the last example is fully visible even when the scroll offset is
|
||||
@@ -347,31 +349,30 @@ impl App {
|
||||
|
||||
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;
|
||||
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)
|
||||
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;
|
||||
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)
|
||||
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> {
|
||||
fn to_tab_title(value: Self) -> Line<'static> {
|
||||
use tailwind::*;
|
||||
use SelectedTab::*;
|
||||
let text = value.to_string();
|
||||
let color = match value {
|
||||
Legacy => ORANGE.c400,
|
||||
Start => SKY.c400,
|
||||
Center => SKY.c300,
|
||||
End => SKY.c200,
|
||||
SpaceAround => INDIGO.c400,
|
||||
SpaceBetween => INDIGO.c300,
|
||||
Self::Legacy => ORANGE.c400,
|
||||
Self::Start => SKY.c400,
|
||||
Self::Center => SKY.c300,
|
||||
Self::End => SKY.c200,
|
||||
Self::SpaceAround => INDIGO.c400,
|
||||
Self::SpaceBetween => INDIGO.c300,
|
||||
};
|
||||
format!(" {text} ").fg(color).bg(Color::Black).into()
|
||||
}
|
||||
@@ -382,20 +383,18 @@ impl StatefulWidget for SelectedTab {
|
||||
fn render(self, area: Rect, buf: &mut Buffer, spacing: &mut Self::State) {
|
||||
let spacing = *spacing;
|
||||
match self {
|
||||
SelectedTab::Legacy => self.render_examples(area, buf, Flex::Legacy, spacing),
|
||||
SelectedTab::Start => self.render_examples(area, buf, Flex::Start, spacing),
|
||||
SelectedTab::Center => self.render_examples(area, buf, Flex::Center, spacing),
|
||||
SelectedTab::End => self.render_examples(area, buf, Flex::End, spacing),
|
||||
SelectedTab::SpaceAround => self.render_examples(area, buf, Flex::SpaceAround, spacing),
|
||||
SelectedTab::SpaceBetween => {
|
||||
self.render_examples(area, buf, Flex::SpaceBetween, spacing)
|
||||
}
|
||||
Self::Legacy => Self::render_examples(area, buf, Flex::Legacy, spacing),
|
||||
Self::Start => Self::render_examples(area, buf, Flex::Start, spacing),
|
||||
Self::Center => Self::render_examples(area, buf, Flex::Center, spacing),
|
||||
Self::End => Self::render_examples(area, buf, Flex::End, spacing),
|
||||
Self::SpaceAround => Self::render_examples(area, buf, Flex::SpaceAround, spacing),
|
||||
Self::SpaceBetween => Self::render_examples(area, buf, Flex::SpaceBetween, spacing),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SelectedTab {
|
||||
fn render_examples(&self, area: Rect, buf: &mut Buffer, flex: Flex, spacing: u16) {
|
||||
fn render_examples(area: Rect, buf: &mut Buffer, flex: Flex, spacing: u16) {
|
||||
let heights = EXAMPLE_DATA
|
||||
.iter()
|
||||
.map(|(desc, _)| get_description_height(desc) + 4);
|
||||
@@ -432,7 +431,7 @@ impl Widget for Example {
|
||||
Paragraph::new(
|
||||
self.description
|
||||
.split('\n')
|
||||
.map(|s| format!("// {}", s).italic().fg(tailwind::SLATE.c400))
|
||||
.map(|s| format!("// {s}").italic().fg(tailwind::SLATE.c400))
|
||||
.map(Line::from)
|
||||
.collect::<Vec<Line>>(),
|
||||
)
|
||||
@@ -440,18 +439,17 @@ impl Widget for Example {
|
||||
}
|
||||
|
||||
for (block, constraint) in blocks.iter().zip(&self.constraints) {
|
||||
self.illustration(*constraint, block.width)
|
||||
.render(*block, buf);
|
||||
Self::illustration(*constraint, block.width).render(*block, buf);
|
||||
}
|
||||
|
||||
for spacer in spacers.iter() {
|
||||
self.render_spacer(*spacer, buf);
|
||||
Self::render_spacer(*spacer, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Example {
|
||||
fn render_spacer(&self, spacer: Rect, buf: &mut Buffer) {
|
||||
fn render_spacer(spacer: Rect, buf: &mut Buffer) {
|
||||
if spacer.width > 1 {
|
||||
let corners_only = symbols::border::Set {
|
||||
top_left: line::NORMAL.top_left,
|
||||
@@ -483,7 +481,7 @@ impl Example {
|
||||
} else if width > 2 {
|
||||
format!("{width}")
|
||||
} else {
|
||||
"".to_string()
|
||||
String::new()
|
||||
};
|
||||
let text = Text::from(vec![
|
||||
Line::raw(""),
|
||||
@@ -496,7 +494,7 @@ impl Example {
|
||||
.render(spacer, buf);
|
||||
}
|
||||
|
||||
fn illustration(&self, constraint: Constraint, width: u16) -> Paragraph {
|
||||
fn illustration(constraint: Constraint, width: u16) -> impl Widget {
|
||||
let main_color = color_for_constraint(constraint);
|
||||
let fg_color = Color::White;
|
||||
let title = format!("{constraint}");
|
||||
@@ -510,7 +508,7 @@ impl Example {
|
||||
}
|
||||
}
|
||||
|
||||
fn color_for_constraint(constraint: Constraint) -> Color {
|
||||
const fn color_for_constraint(constraint: Constraint) -> Color {
|
||||
use tailwind::*;
|
||||
match constraint {
|
||||
Constraint::Min(_) => BLUE.c900,
|
||||
@@ -532,7 +530,7 @@ fn init_error_hooks() -> Result<()> {
|
||||
}))?;
|
||||
std::panic::set_hook(Box::new(move |info| {
|
||||
let _ = restore_terminal();
|
||||
panic(info)
|
||||
panic(info);
|
||||
}));
|
||||
Ok(())
|
||||
}
|
||||
@@ -551,6 +549,7 @@ fn restore_terminal() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
fn get_description_height(s: &str) -> u16 {
|
||||
if s.is_empty() {
|
||||
0
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
#![allow(clippy::enum_glob_use)]
|
||||
|
||||
use std::{io::stdout, time::Duration};
|
||||
|
||||
use color_eyre::{config::HookBuilder, Result};
|
||||
@@ -24,7 +26,7 @@ use crossterm::{
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
style::palette::tailwind,
|
||||
widgets::{block::Title, *},
|
||||
widgets::{block::Title, Block, Borders, Gauge, Padding, Paragraph},
|
||||
};
|
||||
|
||||
const GAUGE1_COLOR: Color = tailwind::RED.c800;
|
||||
@@ -84,7 +86,7 @@ impl App {
|
||||
// difference between how a continuous gauge acts for floor and rounded values.
|
||||
self.progress_columns = (self.progress_columns + 1).clamp(0, terminal_width);
|
||||
self.progress1 = self.progress_columns * 100 / terminal_width;
|
||||
self.progress2 = self.progress_columns as f64 * 100.0 / terminal_width as f64;
|
||||
self.progress2 = f64::from(self.progress_columns) * 100.0 / f64::from(terminal_width);
|
||||
|
||||
// progress3 and progress4 similarly show the difference between unicode and non-unicode
|
||||
// gauges measuring the same thing.
|
||||
@@ -119,6 +121,7 @@ impl App {
|
||||
}
|
||||
|
||||
impl Widget for &App {
|
||||
#[allow(clippy::similar_names)]
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
use Constraint::*;
|
||||
let layout = Layout::vertical([Length(2), Min(0), Length(1)]);
|
||||
@@ -127,8 +130,8 @@ impl Widget for &App {
|
||||
let layout = Layout::vertical([Ratio(1, 4); 4]);
|
||||
let [gauge1_area, gauge2_area, gauge3_area, gauge4_area] = layout.areas(gauge_area);
|
||||
|
||||
self.render_header(header_area, buf);
|
||||
self.render_footer(footer_area, buf);
|
||||
render_header(header_area, buf);
|
||||
render_footer(footer_area, buf);
|
||||
|
||||
self.render_gauge1(gauge1_area, buf);
|
||||
self.render_gauge2(gauge2_area, buf);
|
||||
@@ -137,23 +140,23 @@ impl Widget for &App {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_header(area: Rect, buf: &mut Buffer) {
|
||||
Paragraph::new("Ratatui Gauge Example")
|
||||
.bold()
|
||||
.alignment(Alignment::Center)
|
||||
.fg(CUSTOM_LABEL_COLOR)
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
fn render_footer(area: Rect, buf: &mut Buffer) {
|
||||
Paragraph::new("Press ENTER to start")
|
||||
.alignment(Alignment::Center)
|
||||
.fg(CUSTOM_LABEL_COLOR)
|
||||
.bold()
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn render_header(&self, area: Rect, buf: &mut Buffer) {
|
||||
Paragraph::new("Ratatui Gauge Example")
|
||||
.bold()
|
||||
.alignment(Alignment::Center)
|
||||
.fg(CUSTOM_LABEL_COLOR)
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
fn render_footer(&self, area: Rect, buf: &mut Buffer) {
|
||||
Paragraph::new("Press ENTER to start")
|
||||
.alignment(Alignment::Center)
|
||||
.fg(CUSTOM_LABEL_COLOR)
|
||||
.bold()
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
fn render_gauge1(&self, area: Rect, buf: &mut Buffer) {
|
||||
let title = title_block("Gauge with percentage");
|
||||
Gauge::default()
|
||||
@@ -203,11 +206,11 @@ impl App {
|
||||
|
||||
fn title_block(title: &str) -> Block {
|
||||
let title = Title::from(title).alignment(Alignment::Center);
|
||||
Block::default()
|
||||
.title(title)
|
||||
Block::new()
|
||||
.borders(Borders::NONE)
|
||||
.fg(CUSTOM_LABEL_COLOR)
|
||||
.padding(Padding::vertical(1))
|
||||
.title(title)
|
||||
.fg(CUSTOM_LABEL_COLOR)
|
||||
}
|
||||
|
||||
fn init_error_hooks() -> color_eyre::Result<()> {
|
||||
@@ -220,7 +223,7 @@ fn init_error_hooks() -> color_eyre::Result<()> {
|
||||
}))?;
|
||||
std::panic::set_hook(Box::new(move |info| {
|
||||
let _ = restore_terminal();
|
||||
panic(info)
|
||||
panic(info);
|
||||
}));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ use crossterm::{
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
use ratatui::{prelude::*, widgets::Paragraph};
|
||||
|
||||
/// This is a bare minimum example. There are many approaches to running an application loop, so
|
||||
/// this is not meant to be prescriptive. It is only meant to demonstrate the basic setup and
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
#![allow(clippy::wildcard_imports)]
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, VecDeque},
|
||||
error::Error,
|
||||
@@ -129,6 +131,7 @@ fn input_handling(tx: mpsc::Sender<Event>) {
|
||||
});
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_precision_loss, clippy::needless_pass_by_value)]
|
||||
fn workers(tx: mpsc::Sender<Event>) -> Vec<Worker> {
|
||||
(0..4)
|
||||
.map(|id| {
|
||||
@@ -168,6 +171,7 @@ fn downloads() -> Downloads {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
workers: Vec<Worker>,
|
||||
@@ -194,7 +198,7 @@ fn run_app<B: Backend>(
|
||||
Event::DownloadUpdate(worker_id, _download_id, progress) => {
|
||||
let download = downloads.in_progress.get_mut(&worker_id).unwrap();
|
||||
download.progress = progress;
|
||||
redraw = false
|
||||
redraw = false;
|
||||
}
|
||||
Event::DownloadDone(worker_id, download_id) => {
|
||||
let download = downloads.in_progress.remove(&worker_id).unwrap();
|
||||
@@ -232,7 +236,7 @@ fn run_app<B: Backend>(
|
||||
fn ui(f: &mut Frame, downloads: &Downloads) {
|
||||
let area = f.size();
|
||||
|
||||
let block = Block::default().title(block::Title::from("Progress").alignment(Alignment::Center));
|
||||
let block = Block::new().title(block::Title::from("Progress").alignment(Alignment::Center));
|
||||
f.render_widget(block, area);
|
||||
|
||||
let vertical = Layout::vertical([Constraint::Length(2), Constraint::Length(4)]).margin(1);
|
||||
@@ -242,6 +246,7 @@ fn ui(f: &mut Frame, downloads: &Downloads) {
|
||||
|
||||
// total progress
|
||||
let done = NUM_DOWNLOADS - downloads.pending.len() - downloads.in_progress.len();
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
let progress = LineGauge::default()
|
||||
.gauge_style(Style::default().fg(Color::Blue))
|
||||
.label(format!("{done}/{NUM_DOWNLOADS}"))
|
||||
@@ -271,6 +276,7 @@ fn ui(f: &mut Frame, downloads: &Downloads) {
|
||||
let list = List::new(items);
|
||||
f.render_widget(list, list_area);
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
for (i, (_, download)) in downloads.in_progress.iter().enumerate() {
|
||||
let gauge = Gauge::default()
|
||||
.gauge_style(Style::default().fg(Color::Yellow))
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
#![allow(clippy::enum_glob_use)]
|
||||
|
||||
use std::{error::Error, io};
|
||||
|
||||
use crossterm::{
|
||||
@@ -21,7 +23,11 @@ use crossterm::{
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use ratatui::{layout::Constraint::*, prelude::*, widgets::*};
|
||||
use ratatui::{
|
||||
layout::Constraint::*,
|
||||
prelude::*,
|
||||
widgets::{Block, Paragraph},
|
||||
};
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
@@ -55,13 +61,14 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
|
||||
terminal.draw(ui)?;
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
if key.code == KeyCode::Char('q') {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn ui(frame: &mut Frame) {
|
||||
let vertical = Layout::vertical([
|
||||
Length(4), // text
|
||||
@@ -94,12 +101,12 @@ fn ui(frame: &mut Frame) {
|
||||
.iter()
|
||||
.flat_map(|area| {
|
||||
Layout::horizontal([
|
||||
Constraint::Length(14),
|
||||
Constraint::Length(14),
|
||||
Constraint::Length(14),
|
||||
Constraint::Length(14),
|
||||
Constraint::Length(14),
|
||||
Constraint::Min(0), // fills remaining space
|
||||
Length(14),
|
||||
Length(14),
|
||||
Length(14),
|
||||
Length(14),
|
||||
Length(14),
|
||||
Min(0), // fills remaining space
|
||||
])
|
||||
.split(*area)
|
||||
.iter()
|
||||
@@ -183,16 +190,15 @@ fn render_example_combination(
|
||||
title: &str,
|
||||
constraints: Vec<(Constraint, Constraint)>,
|
||||
) {
|
||||
let block = Block::default()
|
||||
let block = Block::bordered()
|
||||
.title(title.gray())
|
||||
.style(Style::reset())
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::DarkGray));
|
||||
let inner = block.inner(area);
|
||||
frame.render_widget(block, area);
|
||||
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)]);
|
||||
for (i, (a, b)) in constraints.into_iter().enumerate() {
|
||||
render_single_example(frame, layout[i], vec![a, b, Min(0)]);
|
||||
}
|
||||
// This is to make it easy to visually see the alignment of the examples
|
||||
// with the constraints.
|
||||
@@ -213,11 +219,11 @@ fn render_single_example(frame: &mut Frame, area: Rect, constraints: Vec<Constra
|
||||
|
||||
fn constraint_label(constraint: Constraint) -> String {
|
||||
match constraint {
|
||||
Length(n) => format!("{n}"),
|
||||
Min(n) => format!("{n}"),
|
||||
Max(n) => format!("{n}"),
|
||||
Percentage(n) => format!("{n}"),
|
||||
Fill(n) => format!("{n}"),
|
||||
Ratio(a, b) => format!("{a}:{b}"),
|
||||
Constraint::Ratio(a, b) => format!("{a}:{b}"),
|
||||
Constraint::Length(n)
|
||||
| Constraint::Min(n)
|
||||
| Constraint::Max(n)
|
||||
| Constraint::Percentage(n)
|
||||
| Constraint::Fill(n) => format!("{n}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
#![allow(clippy::enum_glob_use, clippy::wildcard_imports)]
|
||||
|
||||
use std::{error::Error, io, io::stdout};
|
||||
|
||||
use color_eyre::config::HookBuilder;
|
||||
@@ -81,7 +83,7 @@ fn init_error_hooks() -> color_eyre::Result<()> {
|
||||
}))?;
|
||||
std::panic::set_hook(Box::new(move |info| {
|
||||
let _ = restore_terminal();
|
||||
panic(info)
|
||||
panic(info);
|
||||
}));
|
||||
Ok(())
|
||||
}
|
||||
@@ -100,9 +102,9 @@ fn restore_terminal() -> color_eyre::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl App<'_> {
|
||||
fn new<'a>() -> App<'a> {
|
||||
App {
|
||||
impl<'a> App<'a> {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
items: StatefulList::with_items([
|
||||
("Rewrite everything with Rust!", "I can't hold my inner voice. He tells me to rewrite the complete universe with Rust", Status::Todo),
|
||||
("Rewrite all of your tui apps with Ratatui", "Yes, you heard that right. Go and replace your tui with Ratatui.", Status::Completed),
|
||||
@@ -125,11 +127,11 @@ impl App<'_> {
|
||||
}
|
||||
|
||||
fn go_top(&mut self) {
|
||||
self.items.state.select(Some(0))
|
||||
self.items.state.select(Some(0));
|
||||
}
|
||||
|
||||
fn go_bottom(&mut self) {
|
||||
self.items.state.select(Some(self.items.items.len() - 1))
|
||||
self.items.state.select(Some(self.items.items.len() - 1));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,30 +179,23 @@ impl Widget for &mut App<'_> {
|
||||
let vertical = Layout::vertical([Constraint::Percentage(50), Constraint::Percentage(50)]);
|
||||
let [upper_item_list_area, lower_item_list_area] = vertical.areas(rest_area);
|
||||
|
||||
self.render_title(header_area, buf);
|
||||
render_title(header_area, buf);
|
||||
self.render_todo(upper_item_list_area, buf);
|
||||
self.render_info(lower_item_list_area, buf);
|
||||
self.render_footer(footer_area, buf);
|
||||
render_footer(footer_area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl App<'_> {
|
||||
fn render_title(&self, area: Rect, buf: &mut Buffer) {
|
||||
Paragraph::new("Ratatui List Example")
|
||||
.bold()
|
||||
.centered()
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
fn render_todo(&mut self, area: Rect, buf: &mut Buffer) {
|
||||
// We create two blocks, one is for the header (outer) and the other is for list (inner).
|
||||
let outer_block = Block::default()
|
||||
let outer_block = Block::new()
|
||||
.borders(Borders::NONE)
|
||||
.fg(TEXT_COLOR)
|
||||
.bg(TODO_HEADER_BG)
|
||||
.title_alignment(Alignment::Center)
|
||||
.title("TODO List")
|
||||
.title_alignment(Alignment::Center);
|
||||
let inner_block = Block::default()
|
||||
.fg(TEXT_COLOR)
|
||||
.bg(TODO_HEADER_BG);
|
||||
let inner_block = Block::new()
|
||||
.borders(Borders::NONE)
|
||||
.fg(TEXT_COLOR)
|
||||
.bg(NORMAL_ROW_COLOR);
|
||||
@@ -251,16 +246,16 @@ impl App<'_> {
|
||||
};
|
||||
|
||||
// We show the list item's info under the list in this paragraph
|
||||
let outer_info_block = Block::default()
|
||||
let outer_info_block = Block::new()
|
||||
.borders(Borders::NONE)
|
||||
.fg(TEXT_COLOR)
|
||||
.bg(TODO_HEADER_BG)
|
||||
.title_alignment(Alignment::Center)
|
||||
.title("TODO Info")
|
||||
.title_alignment(Alignment::Center);
|
||||
let inner_info_block = Block::default()
|
||||
.fg(TEXT_COLOR)
|
||||
.bg(TODO_HEADER_BG);
|
||||
let inner_info_block = Block::new()
|
||||
.borders(Borders::NONE)
|
||||
.bg(NORMAL_ROW_COLOR)
|
||||
.padding(Padding::horizontal(1));
|
||||
.padding(Padding::horizontal(1))
|
||||
.bg(NORMAL_ROW_COLOR);
|
||||
|
||||
// This is a similar process to what we did for list. outer_info_area will be used for
|
||||
// header inner_info_area will be used for the list info.
|
||||
@@ -278,14 +273,19 @@ impl App<'_> {
|
||||
// We can now render the item info
|
||||
info_paragraph.render(inner_info_area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_footer(&self, area: Rect, buf: &mut Buffer) {
|
||||
Paragraph::new(
|
||||
"\nUse ↓↑ to move, ← to unselect, → to change status, g/G to go top/bottom.",
|
||||
)
|
||||
fn render_title(area: Rect, buf: &mut Buffer) {
|
||||
Paragraph::new("Ratatui List Example")
|
||||
.bold()
|
||||
.centered()
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
fn render_footer(area: Rect, buf: &mut Buffer) {
|
||||
Paragraph::new("\nUse ↓↑ to move, ← to unselect, → to change status, g/G to go top/bottom.")
|
||||
.centered()
|
||||
.render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl StatefulList<'_> {
|
||||
|
||||
@@ -13,9 +13,10 @@
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
/// This example is useful for testing how your terminal emulator handles different modifiers.
|
||||
/// It will render a grid of combinations of foreground and background colors with all
|
||||
/// modifiers applied to them.
|
||||
// This example is useful for testing how your terminal emulator handles different modifiers.
|
||||
// It will render a grid of combinations of foreground and background colors with all
|
||||
// modifiers applied to them.
|
||||
|
||||
use std::{
|
||||
error::Error,
|
||||
io::{self, Stdout},
|
||||
@@ -30,7 +31,7 @@ use crossterm::{
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
use ratatui::{prelude::*, widgets::Paragraph};
|
||||
|
||||
type Result<T> = result::Result<T, Box<dyn Error>>;
|
||||
|
||||
@@ -50,7 +51,7 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
|
||||
|
||||
if event::poll(Duration::from_millis(250))? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
if key.code == KeyCode::Char('q') {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
@@ -87,8 +88,8 @@ fn ui(frame: &mut Frame) {
|
||||
.chain(Modifier::all().iter())
|
||||
.collect_vec();
|
||||
let mut index = 0;
|
||||
for bg in colors.iter() {
|
||||
for fg in colors.iter() {
|
||||
for bg in &colors {
|
||||
for fg in &colors {
|
||||
for modifier in &all_modifiers {
|
||||
let modifier_name = format!("{modifier:11?}");
|
||||
let padding = (" ").repeat(12 - modifier_name.len());
|
||||
|
||||
@@ -35,7 +35,10 @@ use crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{Block, Paragraph},
|
||||
};
|
||||
|
||||
type Result<T> = std::result::Result<T, Box<dyn Error>>;
|
||||
|
||||
@@ -139,11 +142,9 @@ fn ui(f: &mut Frame, app: &App) {
|
||||
Line::from("try first without the panic handler to see the difference"),
|
||||
];
|
||||
|
||||
let b = Block::default()
|
||||
.title("Panic Handler Demo")
|
||||
.borders(Borders::ALL);
|
||||
let paragraph = Paragraph::new(text)
|
||||
.block(Block::bordered().title("Panic Handler Demo"))
|
||||
.centered();
|
||||
|
||||
let p = Paragraph::new(text).block(b).centered();
|
||||
|
||||
f.render_widget(p, f.size());
|
||||
f.render_widget(paragraph, f.size());
|
||||
}
|
||||
|
||||
@@ -24,15 +24,18 @@ use crossterm::{
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{Block, Paragraph, Wrap},
|
||||
};
|
||||
|
||||
struct App {
|
||||
scroll: u16,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn new() -> App {
|
||||
App { scroll: 0 }
|
||||
const fn new() -> Self {
|
||||
Self { scroll: 0 }
|
||||
}
|
||||
|
||||
fn on_tick(&mut self) {
|
||||
@@ -82,7 +85,7 @@ fn run_app<B: Backend>(
|
||||
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
if key.code == KeyCode::Char('q') {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
@@ -102,7 +105,7 @@ fn ui(f: &mut Frame, app: &App) {
|
||||
let mut long_line = s.repeat(usize::from(size.width) / s.len() + 4);
|
||||
long_line.push('\n');
|
||||
|
||||
let block = Block::default().black();
|
||||
let block = Block::new().black();
|
||||
f.render_widget(block, size);
|
||||
|
||||
let layout = Layout::vertical([Constraint::Ratio(1, 4); 4]).split(size);
|
||||
@@ -124,8 +127,7 @@ fn ui(f: &mut Frame, app: &App) {
|
||||
];
|
||||
|
||||
let create_block = |title| {
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
Block::bordered()
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.title(Span::styled(
|
||||
title,
|
||||
|
||||
@@ -13,8 +13,9 @@
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
/// See also https://github.com/joshka/tui-popup and
|
||||
/// https://github.com/sephiroth74/tui-confirm-dialog
|
||||
// See also https://github.com/joshka/tui-popup and
|
||||
// https://github.com/sephiroth74/tui-confirm-dialog
|
||||
|
||||
use std::{error::Error, io};
|
||||
|
||||
use crossterm::{
|
||||
@@ -22,15 +23,18 @@ use crossterm::{
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{Block, Clear, Paragraph, Wrap},
|
||||
};
|
||||
|
||||
struct App {
|
||||
show_popup: bool,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn new() -> App {
|
||||
App { show_popup: false }
|
||||
const fn new() -> Self {
|
||||
Self { show_popup: false }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,14 +98,11 @@ fn ui(f: &mut Frame, app: &App) {
|
||||
.wrap(Wrap { trim: true });
|
||||
f.render_widget(paragraph, instructions);
|
||||
|
||||
let block = Block::default()
|
||||
.title("Content")
|
||||
.borders(Borders::ALL)
|
||||
.on_blue();
|
||||
let block = Block::bordered().title("Content").on_blue();
|
||||
f.render_widget(block, content);
|
||||
|
||||
if app.show_popup {
|
||||
let block = Block::default().title("Popup").borders(Borders::ALL);
|
||||
let block = Block::bordered().title("Popup");
|
||||
let area = centered_rect(60, 20, area);
|
||||
f.render_widget(Clear, area); //this clears out the background
|
||||
f.render_widget(block, area);
|
||||
|
||||
@@ -22,49 +22,46 @@ use std::{
|
||||
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
||||
use indoc::indoc;
|
||||
use itertools::izip;
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
use ratatui::{prelude::*, widgets::Paragraph};
|
||||
|
||||
/// A fun example of using half block characters to draw a logo
|
||||
fn main() -> io::Result<()> {
|
||||
#[allow(clippy::many_single_char_names)]
|
||||
fn logo() -> String {
|
||||
let r = indoc! {"
|
||||
▄▄▄
|
||||
█▄▄▀
|
||||
█ █
|
||||
"}
|
||||
.lines();
|
||||
"};
|
||||
let a = indoc! {"
|
||||
▄▄
|
||||
█▄▄█
|
||||
█ █
|
||||
"}
|
||||
.lines();
|
||||
"};
|
||||
let t = indoc! {"
|
||||
▄▄▄
|
||||
█
|
||||
█
|
||||
"}
|
||||
.lines();
|
||||
"};
|
||||
let u = indoc! {"
|
||||
▄ ▄
|
||||
█ █
|
||||
▀▄▄▀
|
||||
"}
|
||||
.lines();
|
||||
"};
|
||||
let i = indoc! {"
|
||||
▄
|
||||
█
|
||||
█
|
||||
"}
|
||||
.lines();
|
||||
"};
|
||||
izip!(r.lines(), a.lines(), t.lines(), u.lines(), i.lines())
|
||||
.map(|(r, a, t, u, i)| format!("{r:5}{a:5}{t:4}{a:5}{t:4}{u:5}{i:5}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
fn main() -> io::Result<()> {
|
||||
let mut terminal = init()?;
|
||||
terminal.draw(|frame| {
|
||||
let logo = izip!(r, a.clone(), t.clone(), a, t, u, i)
|
||||
.map(|(r, a, t, a2, t2, u, i)| {
|
||||
format!("{:5}{:5}{:4}{:5}{:4}{:5}{:5}", r, a, t, a2, t2, u, i)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
frame.render_widget(Paragraph::new(logo), frame.size());
|
||||
frame.render_widget(Paragraph::new(logo()), frame.size());
|
||||
})?;
|
||||
sleep(Duration::from_secs(5));
|
||||
restore()?;
|
||||
@@ -72,7 +69,7 @@ fn main() -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn init() -> io::Result<Terminal<impl Backend>> {
|
||||
fn init() -> io::Result<Terminal<impl Backend>> {
|
||||
enable_raw_mode()?;
|
||||
let options = TerminalOptions {
|
||||
viewport: Viewport::Inline(3),
|
||||
@@ -80,7 +77,7 @@ pub fn init() -> io::Result<Terminal<impl Backend>> {
|
||||
Terminal::with_options(CrosstermBackend::new(stdout()), options)
|
||||
}
|
||||
|
||||
pub fn restore() -> io::Result<()> {
|
||||
fn restore() -> io::Result<()> {
|
||||
disable_raw_mode()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
#![warn(clippy::pedantic)]
|
||||
#![allow(clippy::wildcard_imports)]
|
||||
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
@@ -107,6 +110,7 @@ fn run_app<B: Backend>(
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines, clippy::cast_possible_truncation)]
|
||||
fn ui(f: &mut Frame, app: &mut App) {
|
||||
let size = f.size();
|
||||
|
||||
@@ -133,10 +137,7 @@ fn ui(f: &mut Frame, app: &mut App) {
|
||||
Line::from("This is a line".reset()),
|
||||
Line::from(vec![
|
||||
Span::raw("Masked text: "),
|
||||
Span::styled(
|
||||
Masked::new("password", '*'),
|
||||
Style::default().fg(Color::Red),
|
||||
),
|
||||
Span::styled(Masked::new("password", '*'), Style::new().fg(Color::Red)),
|
||||
]),
|
||||
Line::from("This is a line "),
|
||||
Line::from("This is a line ".red()),
|
||||
@@ -146,10 +147,7 @@ fn ui(f: &mut Frame, app: &mut App) {
|
||||
Line::from("This is a line".reset()),
|
||||
Line::from(vec![
|
||||
Span::raw("Masked text: "),
|
||||
Span::styled(
|
||||
Masked::new("password", '*'),
|
||||
Style::default().fg(Color::Red),
|
||||
),
|
||||
Span::styled(Masked::new("password", '*'), Style::new().fg(Color::Red)),
|
||||
]),
|
||||
];
|
||||
app.vertical_scroll_state = app.vertical_scroll_state.content_length(text.len());
|
||||
@@ -157,9 +155,9 @@ fn ui(f: &mut Frame, app: &mut App) {
|
||||
|
||||
let create_block = |title: &'static str| Block::bordered().gray().title(title.bold());
|
||||
|
||||
let title = Block::default()
|
||||
.title("Use h j k l or ◄ ▲ ▼ ► to scroll ".bold())
|
||||
.title_alignment(Alignment::Center);
|
||||
let title = Block::new()
|
||||
.title_alignment(Alignment::Center)
|
||||
.title("Use h j k l or ◄ ▲ ▼ ► to scroll ".bold());
|
||||
f.render_widget(title, chunks[0]);
|
||||
|
||||
let paragraph = Paragraph::new(text.clone())
|
||||
@@ -168,8 +166,7 @@ fn ui(f: &mut Frame, app: &mut App) {
|
||||
.scroll((app.vertical_scroll as u16, 0));
|
||||
f.render_widget(paragraph, chunks[1]);
|
||||
f.render_stateful_widget(
|
||||
Scrollbar::default()
|
||||
.orientation(ScrollbarOrientation::VerticalRight)
|
||||
Scrollbar::new(ScrollbarOrientation::VerticalRight)
|
||||
.begin_symbol(Some("↑"))
|
||||
.end_symbol(Some("↓")),
|
||||
chunks[1],
|
||||
@@ -184,8 +181,7 @@ fn ui(f: &mut Frame, app: &mut App) {
|
||||
.scroll((app.vertical_scroll as u16, 0));
|
||||
f.render_widget(paragraph, chunks[2]);
|
||||
f.render_stateful_widget(
|
||||
Scrollbar::default()
|
||||
.orientation(ScrollbarOrientation::VerticalLeft)
|
||||
Scrollbar::new(ScrollbarOrientation::VerticalLeft)
|
||||
.symbols(scrollbar::VERTICAL)
|
||||
.begin_symbol(None)
|
||||
.track_symbol(None)
|
||||
@@ -205,8 +201,7 @@ fn ui(f: &mut Frame, app: &mut App) {
|
||||
.scroll((0, app.horizontal_scroll as u16));
|
||||
f.render_widget(paragraph, chunks[3]);
|
||||
f.render_stateful_widget(
|
||||
Scrollbar::default()
|
||||
.orientation(ScrollbarOrientation::HorizontalBottom)
|
||||
Scrollbar::new(ScrollbarOrientation::HorizontalBottom)
|
||||
.thumb_symbol("🬋")
|
||||
.end_symbol(None),
|
||||
chunks[3].inner(&Margin {
|
||||
@@ -224,8 +219,7 @@ fn ui(f: &mut Frame, app: &mut App) {
|
||||
.scroll((0, app.horizontal_scroll as u16));
|
||||
f.render_widget(paragraph, chunks[4]);
|
||||
f.render_stateful_widget(
|
||||
Scrollbar::default()
|
||||
.orientation(ScrollbarOrientation::HorizontalBottom)
|
||||
Scrollbar::new(ScrollbarOrientation::HorizontalBottom)
|
||||
.thumb_symbol("░")
|
||||
.track_symbol(Some("─")),
|
||||
chunks[4].inner(&Margin {
|
||||
|
||||
@@ -28,17 +28,20 @@ use rand::{
|
||||
distributions::{Distribution, Uniform},
|
||||
rngs::ThreadRng,
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{Block, Borders, Sparkline},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RandomSignal {
|
||||
struct RandomSignal {
|
||||
distribution: Uniform<u64>,
|
||||
rng: ThreadRng,
|
||||
}
|
||||
|
||||
impl RandomSignal {
|
||||
pub fn new(lower: u64, upper: u64) -> RandomSignal {
|
||||
RandomSignal {
|
||||
fn new(lower: u64, upper: u64) -> Self {
|
||||
Self {
|
||||
distribution: Uniform::new(lower, upper),
|
||||
rng: rand::thread_rng(),
|
||||
}
|
||||
@@ -60,12 +63,12 @@ struct App {
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn new() -> App {
|
||||
fn new() -> Self {
|
||||
let mut signal = RandomSignal::new(0, 100);
|
||||
let data1 = signal.by_ref().take(200).collect::<Vec<u64>>();
|
||||
let data2 = signal.by_ref().take(200).collect::<Vec<u64>>();
|
||||
let data3 = signal.by_ref().take(200).collect::<Vec<u64>>();
|
||||
App {
|
||||
Self {
|
||||
signal,
|
||||
data1,
|
||||
data2,
|
||||
@@ -127,7 +130,7 @@ fn run_app<B: Backend>(
|
||||
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
if key.code == KeyCode::Char('q') {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
@@ -148,18 +151,18 @@ fn ui(f: &mut Frame, app: &App) {
|
||||
.split(f.size());
|
||||
let sparkline = Sparkline::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Data1")
|
||||
.borders(Borders::LEFT | Borders::RIGHT),
|
||||
Block::new()
|
||||
.borders(Borders::LEFT | Borders::RIGHT)
|
||||
.title("Data1"),
|
||||
)
|
||||
.data(&app.data1)
|
||||
.style(Style::default().fg(Color::Yellow));
|
||||
f.render_widget(sparkline, chunks[0]);
|
||||
let sparkline = Sparkline::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Data2")
|
||||
.borders(Borders::LEFT | Borders::RIGHT),
|
||||
Block::new()
|
||||
.borders(Borders::LEFT | Borders::RIGHT)
|
||||
.title("Data2"),
|
||||
)
|
||||
.data(&app.data2)
|
||||
.style(Style::default().bg(Color::Green));
|
||||
@@ -167,9 +170,9 @@ fn ui(f: &mut Frame, app: &App) {
|
||||
// Multiline
|
||||
let sparkline = Sparkline::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Data3")
|
||||
.borders(Borders::LEFT | Borders::RIGHT),
|
||||
Block::new()
|
||||
.borders(Borders::LEFT | Borders::RIGHT)
|
||||
.title("Data3"),
|
||||
)
|
||||
.data(&app.data3)
|
||||
.style(Style::default().fg(Color::Red));
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
#![allow(clippy::enum_glob_use, clippy::wildcard_imports)]
|
||||
|
||||
use std::{error::Error, io};
|
||||
|
||||
use crossterm::{
|
||||
@@ -48,7 +50,7 @@ struct TableColors {
|
||||
}
|
||||
|
||||
impl TableColors {
|
||||
fn new(color: &tailwind::Palette) -> Self {
|
||||
const fn new(color: &tailwind::Palette) -> Self {
|
||||
Self {
|
||||
buffer_bg: tailwind::SLATE.c950,
|
||||
header_bg: color.c900,
|
||||
@@ -69,7 +71,7 @@ struct Data {
|
||||
}
|
||||
|
||||
impl Data {
|
||||
fn ref_array(&self) -> [&String; 3] {
|
||||
const fn ref_array(&self) -> [&String; 3] {
|
||||
[&self.name, &self.address, &self.email]
|
||||
}
|
||||
|
||||
@@ -96,9 +98,9 @@ struct App {
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn new() -> App {
|
||||
fn new() -> Self {
|
||||
let data_vec = generate_fake_names();
|
||||
App {
|
||||
Self {
|
||||
state: TableState::default().with_selected(0),
|
||||
longest_item_lens: constraint_len_calculator(&data_vec),
|
||||
scroll_state: ScrollbarState::new((data_vec.len() - 1) * ITEM_HEIGHT),
|
||||
@@ -147,7 +149,7 @@ impl App {
|
||||
}
|
||||
|
||||
pub fn set_colors(&mut self) {
|
||||
self.colors = TableColors::new(&PALETTES[self.color_index])
|
||||
self.colors = TableColors::new(&PALETTES[self.color_index]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,8 +247,7 @@ fn render_table(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
.fg(app.colors.selected_style_fg);
|
||||
|
||||
let header = ["Name", "Address", "Email"]
|
||||
.iter()
|
||||
.cloned()
|
||||
.into_iter()
|
||||
.map(Cell::from)
|
||||
.collect::<Row>()
|
||||
.style(header_style)
|
||||
@@ -257,9 +258,8 @@ fn render_table(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
_ => app.colors.alt_row_color,
|
||||
};
|
||||
let item = data.ref_array();
|
||||
item.iter()
|
||||
.cloned()
|
||||
.map(|content| Cell::from(Text::from(format!("\n{}\n", content))))
|
||||
item.into_iter()
|
||||
.map(|content| Cell::from(Text::from(format!("\n{content}\n"))))
|
||||
.collect::<Row>()
|
||||
.style(Style::new().fg(app.colors.row_fg).bg(color))
|
||||
.height(4)
|
||||
@@ -308,6 +308,7 @@ fn constraint_len_calculator(items: &[Data]) -> (u16, u16, u16) {
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
(name_len as u16, address_len as u16, email_len as u16)
|
||||
}
|
||||
|
||||
@@ -325,15 +326,14 @@ fn render_scrollbar(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
);
|
||||
}
|
||||
|
||||
fn render_footer(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
fn render_footer(f: &mut Frame, app: &App, area: Rect) {
|
||||
let info_footer = Paragraph::new(Line::from(INFO_TEXT))
|
||||
.style(Style::new().fg(app.colors.row_fg).bg(app.colors.buffer_bg))
|
||||
.centered()
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::new().fg(app.colors.footer_border_color))
|
||||
.border_type(BorderType::Double),
|
||||
Block::bordered()
|
||||
.border_type(BorderType::Double)
|
||||
.border_style(Style::new().fg(app.colors.footer_border_color)),
|
||||
);
|
||||
f.render_widget(info_footer, area);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,9 @@
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
use std::{io, io::stdout};
|
||||
#![allow(clippy::wildcard_imports, clippy::enum_glob_use)]
|
||||
|
||||
use std::io::stdout;
|
||||
|
||||
use color_eyre::{config::HookBuilder, Result};
|
||||
use crossterm::{
|
||||
@@ -72,7 +74,7 @@ impl App {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_events(&mut self) -> Result<(), io::Error> {
|
||||
fn handle_events(&mut self) -> std::io::Result<()> {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
use KeyCode::*;
|
||||
@@ -102,17 +104,17 @@ impl App {
|
||||
|
||||
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;
|
||||
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)
|
||||
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;
|
||||
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)
|
||||
Self::from_repr(next_index).unwrap_or(self)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,20 +127,16 @@ impl Widget for &App {
|
||||
let horizontal = Layout::horizontal([Min(0), Length(20)]);
|
||||
let [tabs_area, title_area] = horizontal.areas(header_area);
|
||||
|
||||
self.render_title(title_area, buf);
|
||||
render_title(title_area, buf);
|
||||
self.render_tabs(tabs_area, buf);
|
||||
self.selected_tab.render(inner_area, buf);
|
||||
self.render_footer(footer_area, buf);
|
||||
render_footer(footer_area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn render_title(&self, area: Rect, buf: &mut Buffer) {
|
||||
"Ratatui Tabs Example".bold().render(area, buf);
|
||||
}
|
||||
|
||||
fn render_tabs(&self, area: Rect, buf: &mut Buffer) {
|
||||
let titles = SelectedTab::iter().map(|tab| tab.title());
|
||||
let titles = SelectedTab::iter().map(SelectedTab::title);
|
||||
let highlight_style = (Color::default(), self.selected_tab.palette().c700);
|
||||
let selected_tab_index = self.selected_tab as usize;
|
||||
Tabs::new(titles)
|
||||
@@ -148,74 +146,77 @@ impl App {
|
||||
.divider(" ")
|
||||
.render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_footer(&self, area: Rect, buf: &mut Buffer) {
|
||||
Line::raw("◄ ► to change tab | Press q to quit")
|
||||
.centered()
|
||||
.render(area, buf);
|
||||
}
|
||||
fn render_title(area: Rect, buf: &mut Buffer) {
|
||||
"Ratatui Tabs Example".bold().render(area, buf);
|
||||
}
|
||||
|
||||
fn render_footer(area: Rect, buf: &mut Buffer) {
|
||||
Line::raw("◄ ► to change tab | Press q to quit")
|
||||
.centered()
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
impl Widget for SelectedTab {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
// in a real app these might be separate widgets
|
||||
match self {
|
||||
SelectedTab::Tab1 => self.render_tab0(area, buf),
|
||||
SelectedTab::Tab2 => self.render_tab1(area, buf),
|
||||
SelectedTab::Tab3 => self.render_tab2(area, buf),
|
||||
SelectedTab::Tab4 => self.render_tab3(area, buf),
|
||||
Self::Tab1 => self.render_tab0(area, buf),
|
||||
Self::Tab2 => self.render_tab1(area, buf),
|
||||
Self::Tab3 => self.render_tab2(area, buf),
|
||||
Self::Tab4 => self.render_tab3(area, buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SelectedTab {
|
||||
/// Return tab's name as a styled `Line`
|
||||
fn title(&self) -> Line<'static> {
|
||||
fn title(self) -> Line<'static> {
|
||||
format!(" {self} ")
|
||||
.fg(tailwind::SLATE.c200)
|
||||
.bg(self.palette().c900)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn render_tab0(&self, area: Rect, buf: &mut Buffer) {
|
||||
fn render_tab0(self, area: Rect, buf: &mut Buffer) {
|
||||
Paragraph::new("Hello, World!")
|
||||
.block(self.block())
|
||||
.render(area, buf)
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
fn render_tab1(&self, area: Rect, buf: &mut Buffer) {
|
||||
fn render_tab1(self, area: Rect, buf: &mut Buffer) {
|
||||
Paragraph::new("Welcome to the Ratatui tabs example!")
|
||||
.block(self.block())
|
||||
.render(area, buf)
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
fn render_tab2(&self, area: Rect, buf: &mut Buffer) {
|
||||
fn render_tab2(self, area: Rect, buf: &mut Buffer) {
|
||||
Paragraph::new("Look! I'm different than others!")
|
||||
.block(self.block())
|
||||
.render(area, buf)
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
fn render_tab3(&self, area: Rect, buf: &mut Buffer) {
|
||||
fn render_tab3(self, area: Rect, buf: &mut Buffer) {
|
||||
Paragraph::new("I know, these are some basic changes. But I think you got the main idea.")
|
||||
.block(self.block())
|
||||
.render(area, buf)
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
/// A block surrounding the tab's content
|
||||
fn block(&self) -> Block<'static> {
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
fn block(self) -> Block<'static> {
|
||||
Block::bordered()
|
||||
.border_set(symbols::border::PROPORTIONAL_TALL)
|
||||
.padding(Padding::horizontal(1))
|
||||
.border_style(self.palette().c700)
|
||||
}
|
||||
|
||||
fn palette(&self) -> tailwind::Palette {
|
||||
const fn palette(self) -> tailwind::Palette {
|
||||
match self {
|
||||
SelectedTab::Tab1 => tailwind::BLUE,
|
||||
SelectedTab::Tab2 => tailwind::EMERALD,
|
||||
SelectedTab::Tab3 => tailwind::INDIGO,
|
||||
SelectedTab::Tab4 => tailwind::RED,
|
||||
Self::Tab1 => tailwind::BLUE,
|
||||
Self::Tab2 => tailwind::EMERALD,
|
||||
Self::Tab3 => tailwind::INDIGO,
|
||||
Self::Tab4 => tailwind::RED,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -230,7 +231,7 @@ fn init_error_hooks() -> color_eyre::Result<()> {
|
||||
}))?;
|
||||
std::panic::set_hook(Box::new(move |info| {
|
||||
let _ = restore_terminal();
|
||||
panic(info)
|
||||
panic(info);
|
||||
}));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -13,28 +13,31 @@
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
// A simple example demonstrating how to handle user input. This is a bit out of the scope of
|
||||
// the library as it does not provide any input handling out of the box. However, it may helps
|
||||
// some to get started.
|
||||
//
|
||||
// This is a very simple example:
|
||||
// * An input box always focused. Every character you type is registered here.
|
||||
// * An entered character is inserted at the cursor position.
|
||||
// * Pressing Backspace erases the left character before the cursor position
|
||||
// * Pressing Enter pushes the current input in the history of previous messages. **Note: ** as
|
||||
// this is a relatively simple example unicode characters are unsupported and their use will
|
||||
// result in undefined behaviour.
|
||||
//
|
||||
// See also https://github.com/rhysd/tui-textarea and https://github.com/sayanarijit/tui-input/
|
||||
|
||||
use std::{error::Error, io};
|
||||
|
||||
/// A simple example demonstrating how to handle user input. This is a bit out of the scope of
|
||||
/// the library as it does not provide any input handling out of the box. However, it may helps
|
||||
/// some to get started.
|
||||
///
|
||||
/// This is a very simple example:
|
||||
/// * An input box always focused. Every character you type is registered here.
|
||||
/// * An entered character is inserted at the cursor position.
|
||||
/// * Pressing Backspace erases the left character before the cursor position
|
||||
/// * Pressing Enter pushes the current input in the history of previous messages. **Note: **
|
||||
/// as
|
||||
/// this is a relatively simple example unicode characters are unsupported and their use will
|
||||
/// result in undefined behaviour.
|
||||
///
|
||||
/// See also https://github.com/rhysd/tui-textarea and https://github.com/sayanarijit/tui-input/
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{Block, List, ListItem, Paragraph},
|
||||
};
|
||||
|
||||
enum InputMode {
|
||||
Normal,
|
||||
@@ -46,49 +49,59 @@ struct App {
|
||||
/// Current value of the input box
|
||||
input: String,
|
||||
/// Position of cursor in the editor area.
|
||||
cursor_position: usize,
|
||||
character_index: usize,
|
||||
/// Current input mode
|
||||
input_mode: InputMode,
|
||||
/// History of recorded messages
|
||||
messages: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for App {
|
||||
fn default() -> App {
|
||||
App {
|
||||
impl App {
|
||||
const fn new() -> Self {
|
||||
Self {
|
||||
input: String::new(),
|
||||
input_mode: InputMode::Normal,
|
||||
messages: Vec::new(),
|
||||
cursor_position: 0,
|
||||
character_index: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn move_cursor_left(&mut self) {
|
||||
let cursor_moved_left = self.cursor_position.saturating_sub(1);
|
||||
self.cursor_position = self.clamp_cursor(cursor_moved_left);
|
||||
let cursor_moved_left = self.character_index.saturating_sub(1);
|
||||
self.character_index = self.clamp_cursor(cursor_moved_left);
|
||||
}
|
||||
|
||||
fn move_cursor_right(&mut self) {
|
||||
let cursor_moved_right = self.cursor_position.saturating_add(1);
|
||||
self.cursor_position = self.clamp_cursor(cursor_moved_right);
|
||||
let cursor_moved_right = self.character_index.saturating_add(1);
|
||||
self.character_index = self.clamp_cursor(cursor_moved_right);
|
||||
}
|
||||
|
||||
fn enter_char(&mut self, new_char: char) {
|
||||
self.input.insert(self.cursor_position, new_char);
|
||||
|
||||
let index = self.byte_index();
|
||||
self.input.insert(index, new_char);
|
||||
self.move_cursor_right();
|
||||
}
|
||||
|
||||
/// Returns the byte index based on the character position.
|
||||
///
|
||||
/// Since each character in a string can be contain multiple bytes, it's necessary to calculate
|
||||
/// the byte index based on the index of the character.
|
||||
fn byte_index(&mut self) -> usize {
|
||||
self.input
|
||||
.char_indices()
|
||||
.map(|(i, _)| i)
|
||||
.nth(self.character_index)
|
||||
.unwrap_or(self.input.len())
|
||||
}
|
||||
|
||||
fn delete_char(&mut self) {
|
||||
let is_not_cursor_leftmost = self.cursor_position != 0;
|
||||
let is_not_cursor_leftmost = self.character_index != 0;
|
||||
if is_not_cursor_leftmost {
|
||||
// Method "remove" is not used on the saved text for deleting the selected char.
|
||||
// Reason: Using remove on String works on bytes instead of the chars.
|
||||
// Using remove would require special care because of char boundaries.
|
||||
|
||||
let current_index = self.cursor_position;
|
||||
let current_index = self.character_index;
|
||||
let from_left_to_current_index = current_index - 1;
|
||||
|
||||
// Getting all characters before the selected character.
|
||||
@@ -104,11 +117,11 @@ impl App {
|
||||
}
|
||||
|
||||
fn clamp_cursor(&self, new_cursor_pos: usize) -> usize {
|
||||
new_cursor_pos.clamp(0, self.input.len())
|
||||
new_cursor_pos.clamp(0, self.input.chars().count())
|
||||
}
|
||||
|
||||
fn reset_cursor(&mut self) {
|
||||
self.cursor_position = 0;
|
||||
self.character_index = 0;
|
||||
}
|
||||
|
||||
fn submit_message(&mut self) {
|
||||
@@ -127,7 +140,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// create app and run it
|
||||
let app = App::default();
|
||||
let app = App::new();
|
||||
let res = run_app(&mut terminal, app);
|
||||
|
||||
// restore terminal
|
||||
@@ -180,7 +193,7 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
_ => {}
|
||||
InputMode::Editing => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -225,7 +238,7 @@ fn ui(f: &mut Frame, app: &App) {
|
||||
InputMode::Normal => Style::default(),
|
||||
InputMode::Editing => Style::default().fg(Color::Yellow),
|
||||
})
|
||||
.block(Block::default().borders(Borders::ALL).title("Input"));
|
||||
.block(Block::bordered().title("Input"));
|
||||
f.render_widget(input, input_area);
|
||||
match app.input_mode {
|
||||
InputMode::Normal =>
|
||||
@@ -235,13 +248,14 @@ fn ui(f: &mut Frame, app: &App) {
|
||||
InputMode::Editing => {
|
||||
// Make the cursor visible and ask ratatui to put it at the specified coordinates after
|
||||
// rendering
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
f.set_cursor(
|
||||
// Draw the cursor at the current position in the input field.
|
||||
// This position is can be controlled via the left and right arrow key
|
||||
input_area.x + app.cursor_position as u16 + 1,
|
||||
input_area.x + app.character_index as u16 + 1,
|
||||
// Move one line down, from the border to the input line
|
||||
input_area.y + 1,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,7 +268,6 @@ fn ui(f: &mut Frame, app: &App) {
|
||||
ListItem::new(content)
|
||||
})
|
||||
.collect();
|
||||
let messages =
|
||||
List::new(messages).block(Block::default().borders(Borders::ALL).title("Messages"));
|
||||
let messages = List::new(messages).block(Block::bordered().title("Messages"));
|
||||
f.render_widget(messages, messages_area);
|
||||
}
|
||||
|
||||
@@ -18,4 +18,4 @@ Space
|
||||
Left
|
||||
Space
|
||||
Left
|
||||
Space
|
||||
Space
|
||||
|
||||
@@ -15,4 +15,4 @@ Enter
|
||||
Sleep 2s
|
||||
Show
|
||||
Type "d"
|
||||
Sleep 30s
|
||||
Sleep 30s
|
||||
|
||||
@@ -40,4 +40,4 @@ Tab
|
||||
# Weather
|
||||
Set TypingSpeed 100ms
|
||||
Down 40
|
||||
Sleep 2s
|
||||
Sleep 2s
|
||||
|
||||
@@ -46,4 +46,4 @@ Tab
|
||||
Screenshot "target/demo2-weather.png"
|
||||
Set TypingSpeed 100ms
|
||||
Down 40
|
||||
Sleep 2s
|
||||
Sleep 2s
|
||||
|
||||
@@ -10,8 +10,8 @@ use crossterm::{
|
||||
cursor::{Hide, MoveTo, Show},
|
||||
execute, queue,
|
||||
style::{
|
||||
Attribute as CAttribute, Attributes as CAttributes, Color as CColor, ContentStyle, Print,
|
||||
SetAttribute, SetBackgroundColor, SetForegroundColor,
|
||||
Attribute as CAttribute, Attributes as CAttributes, Color as CColor, Colors, ContentStyle,
|
||||
Print, SetAttribute, SetBackgroundColor, SetColors, SetForegroundColor,
|
||||
},
|
||||
terminal::{self, Clear},
|
||||
};
|
||||
@@ -97,8 +97,8 @@ where
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let backend = CrosstermBackend::new(stdout());
|
||||
/// ```
|
||||
pub fn new(writer: W) -> CrosstermBackend<W> {
|
||||
CrosstermBackend { writer }
|
||||
pub const fn new(writer: W) -> Self {
|
||||
Self { writer }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,14 +145,12 @@ where
|
||||
diff.queue(&mut self.writer)?;
|
||||
modifier = cell.modifier;
|
||||
}
|
||||
if cell.fg != fg {
|
||||
let color = CColor::from(cell.fg);
|
||||
queue!(self.writer, SetForegroundColor(color))?;
|
||||
if cell.fg != fg || cell.bg != bg {
|
||||
queue!(
|
||||
self.writer,
|
||||
SetColors(Colors::new(cell.fg.into(), cell.bg.into()))
|
||||
)?;
|
||||
fg = cell.fg;
|
||||
}
|
||||
if cell.bg != bg {
|
||||
let color = CColor::from(cell.bg);
|
||||
queue!(self.writer, SetBackgroundColor(color))?;
|
||||
bg = cell.bg;
|
||||
}
|
||||
#[cfg(feature = "underline-color")]
|
||||
@@ -228,7 +226,7 @@ where
|
||||
Ok(Rect::new(0, 0, width, height))
|
||||
}
|
||||
|
||||
fn window_size(&mut self) -> Result<WindowSize, io::Error> {
|
||||
fn window_size(&mut self) -> io::Result<WindowSize> {
|
||||
let crossterm::terminal::WindowSize {
|
||||
columns,
|
||||
rows,
|
||||
@@ -252,25 +250,25 @@ where
|
||||
impl From<Color> for CColor {
|
||||
fn from(color: Color) -> Self {
|
||||
match color {
|
||||
Color::Reset => CColor::Reset,
|
||||
Color::Black => CColor::Black,
|
||||
Color::Red => CColor::DarkRed,
|
||||
Color::Green => CColor::DarkGreen,
|
||||
Color::Yellow => CColor::DarkYellow,
|
||||
Color::Blue => CColor::DarkBlue,
|
||||
Color::Magenta => CColor::DarkMagenta,
|
||||
Color::Cyan => CColor::DarkCyan,
|
||||
Color::Gray => CColor::Grey,
|
||||
Color::DarkGray => CColor::DarkGrey,
|
||||
Color::LightRed => CColor::Red,
|
||||
Color::LightGreen => CColor::Green,
|
||||
Color::LightBlue => CColor::Blue,
|
||||
Color::LightYellow => CColor::Yellow,
|
||||
Color::LightMagenta => CColor::Magenta,
|
||||
Color::LightCyan => CColor::Cyan,
|
||||
Color::White => CColor::White,
|
||||
Color::Indexed(i) => CColor::AnsiValue(i),
|
||||
Color::Rgb(r, g, b) => CColor::Rgb { r, g, b },
|
||||
Color::Reset => Self::Reset,
|
||||
Color::Black => Self::Black,
|
||||
Color::Red => Self::DarkRed,
|
||||
Color::Green => Self::DarkGreen,
|
||||
Color::Yellow => Self::DarkYellow,
|
||||
Color::Blue => Self::DarkBlue,
|
||||
Color::Magenta => Self::DarkMagenta,
|
||||
Color::Cyan => Self::DarkCyan,
|
||||
Color::Gray => Self::Grey,
|
||||
Color::DarkGray => Self::DarkGrey,
|
||||
Color::LightRed => Self::Red,
|
||||
Color::LightGreen => Self::Green,
|
||||
Color::LightBlue => Self::Blue,
|
||||
Color::LightYellow => Self::Yellow,
|
||||
Color::LightMagenta => Self::Magenta,
|
||||
Color::LightCyan => Self::Cyan,
|
||||
Color::White => Self::White,
|
||||
Color::Indexed(i) => Self::AnsiValue(i),
|
||||
Color::Rgb(r, g, b) => Self::Rgb { r, g, b },
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -304,14 +302,13 @@ impl From<CColor> for Color {
|
||||
/// The `ModifierDiff` struct is used to calculate the difference between two `Modifier`
|
||||
/// values. This is useful when updating the terminal display, as it allows for more
|
||||
/// efficient updates by only sending the necessary changes.
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
struct ModifierDiff {
|
||||
pub from: Modifier,
|
||||
pub to: Modifier,
|
||||
}
|
||||
|
||||
impl ModifierDiff {
|
||||
fn queue<W>(&self, mut w: W) -> io::Result<()>
|
||||
fn queue<W>(self, mut w: W) -> io::Result<()>
|
||||
where
|
||||
W: io::Write,
|
||||
{
|
||||
@@ -377,22 +374,22 @@ impl From<CAttribute> for Modifier {
|
||||
// `Attribute*s*` (note the *s*) contains multiple `Attribute`
|
||||
// We convert `Attribute` to `Attribute*s*` (containing only 1 value) to avoid implementing
|
||||
// the conversion again
|
||||
Modifier::from(CAttributes::from(value))
|
||||
Self::from(CAttributes::from(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CAttributes> for Modifier {
|
||||
fn from(value: CAttributes) -> Self {
|
||||
let mut res = Modifier::empty();
|
||||
let mut res = Self::empty();
|
||||
|
||||
if value.has(CAttribute::Bold) {
|
||||
res |= Modifier::BOLD;
|
||||
res |= Self::BOLD;
|
||||
}
|
||||
if value.has(CAttribute::Dim) {
|
||||
res |= Modifier::DIM;
|
||||
res |= Self::DIM;
|
||||
}
|
||||
if value.has(CAttribute::Italic) {
|
||||
res |= Modifier::ITALIC;
|
||||
res |= Self::ITALIC;
|
||||
}
|
||||
if value.has(CAttribute::Underlined)
|
||||
|| value.has(CAttribute::DoubleUnderlined)
|
||||
@@ -400,22 +397,22 @@ impl From<CAttributes> for Modifier {
|
||||
|| value.has(CAttribute::Underdotted)
|
||||
|| value.has(CAttribute::Underdashed)
|
||||
{
|
||||
res |= Modifier::UNDERLINED;
|
||||
res |= Self::UNDERLINED;
|
||||
}
|
||||
if value.has(CAttribute::SlowBlink) {
|
||||
res |= Modifier::SLOW_BLINK;
|
||||
res |= Self::SLOW_BLINK;
|
||||
}
|
||||
if value.has(CAttribute::RapidBlink) {
|
||||
res |= Modifier::RAPID_BLINK;
|
||||
res |= Self::RAPID_BLINK;
|
||||
}
|
||||
if value.has(CAttribute::Reverse) {
|
||||
res |= Modifier::REVERSED;
|
||||
res |= Self::REVERSED;
|
||||
}
|
||||
if value.has(CAttribute::Hidden) {
|
||||
res |= Modifier::HIDDEN;
|
||||
res |= Self::HIDDEN;
|
||||
}
|
||||
if value.has(CAttribute::CrossedOut) {
|
||||
res |= Modifier::CROSSED_OUT;
|
||||
res |= Self::CROSSED_OUT;
|
||||
}
|
||||
|
||||
res
|
||||
@@ -449,10 +446,10 @@ impl From<ContentStyle> for Style {
|
||||
}
|
||||
|
||||
Self {
|
||||
fg: value.foreground_color.map(|c| c.into()),
|
||||
bg: value.background_color.map(|c| c.into()),
|
||||
fg: value.foreground_color.map(Into::into),
|
||||
bg: value.background_color.map(Into::into),
|
||||
#[cfg(feature = "underline-color")]
|
||||
underline_color: value.underline_color.map(|c| c.into()),
|
||||
underline_color: value.underline_color.map(Into::into),
|
||||
add_modifier: value.attributes.into(),
|
||||
sub_modifier,
|
||||
}
|
||||
@@ -668,6 +665,6 @@ mod tests {
|
||||
..Default::default()
|
||||
}),
|
||||
Style::default().underline_color(Color::Red)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,8 +82,8 @@ where
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let backend = TermionBackend::new(stdout());
|
||||
/// ```
|
||||
pub fn new(writer: W) -> TermionBackend<W> {
|
||||
TermionBackend { writer }
|
||||
pub const fn new(writer: W) -> Self {
|
||||
Self { writer }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,7 +198,7 @@ where
|
||||
Ok(Rect::new(0, 0, terminal.0, terminal.1))
|
||||
}
|
||||
|
||||
fn window_size(&mut self) -> Result<WindowSize, io::Error> {
|
||||
fn window_size(&mut self) -> io::Result<WindowSize> {
|
||||
Ok(WindowSize {
|
||||
columns_rows: termion::terminal_size()?.into(),
|
||||
pixels: termion::terminal_size_pixels()?.into(),
|
||||
@@ -209,16 +209,13 @@ where
|
||||
self.writer.flush()
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
struct Fg(Color);
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
struct Bg(Color);
|
||||
|
||||
/// The `ModifierDiff` struct is used to calculate the difference between two `Modifier`
|
||||
/// values. This is useful when updating the terminal display, as it allows for more
|
||||
/// efficient updates by only sending the necessary changes.
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
struct ModifierDiff {
|
||||
from: Modifier,
|
||||
to: Modifier,
|
||||
@@ -319,37 +316,37 @@ from_termion_for_color!(LightWhite, White);
|
||||
|
||||
impl From<tcolor::AnsiValue> for Color {
|
||||
fn from(value: tcolor::AnsiValue) -> Self {
|
||||
Color::Indexed(value.0)
|
||||
Self::Indexed(value.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tcolor::Bg<tcolor::AnsiValue>> for Style {
|
||||
fn from(value: tcolor::Bg<tcolor::AnsiValue>) -> Self {
|
||||
Style::default().bg(Color::Indexed(value.0 .0))
|
||||
Self::default().bg(Color::Indexed(value.0 .0))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tcolor::Fg<tcolor::AnsiValue>> for Style {
|
||||
fn from(value: tcolor::Fg<tcolor::AnsiValue>) -> Self {
|
||||
Style::default().fg(Color::Indexed(value.0 .0))
|
||||
Self::default().fg(Color::Indexed(value.0 .0))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tcolor::Rgb> for Color {
|
||||
fn from(value: tcolor::Rgb) -> Self {
|
||||
Color::Rgb(value.0, value.1, value.2)
|
||||
Self::Rgb(value.0, value.1, value.2)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tcolor::Bg<tcolor::Rgb>> for Style {
|
||||
fn from(value: tcolor::Bg<tcolor::Rgb>) -> Self {
|
||||
Style::default().bg(Color::Rgb(value.0 .0, value.0 .1, value.0 .2))
|
||||
Self::default().bg(Color::Rgb(value.0 .0, value.0 .1, value.0 .2))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tcolor::Fg<tcolor::Rgb>> for Style {
|
||||
fn from(value: tcolor::Fg<tcolor::Rgb>) -> Self {
|
||||
Style::default().fg(Color::Rgb(value.0 .0, value.0 .1, value.0 .2))
|
||||
Self::default().fg(Color::Rgb(value.0 .0, value.0 .1, value.0 .2))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -438,7 +435,7 @@ from_termion_for_modifier!(Blink, SLOW_BLINK);
|
||||
|
||||
impl From<termion::style::Reset> for Modifier {
|
||||
fn from(_: termion::style::Reset) -> Self {
|
||||
Modifier::empty()
|
||||
Self::empty()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -84,23 +84,23 @@ impl TermwizBackend {
|
||||
/// let backend = TermwizBackend::new()?;
|
||||
/// # Ok::<(), Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
pub fn new() -> Result<TermwizBackend, Box<dyn Error>> {
|
||||
pub fn new() -> Result<Self, Box<dyn Error>> {
|
||||
let mut buffered_terminal =
|
||||
BufferedTerminal::new(SystemTerminal::new(Capabilities::new_from_env()?)?)?;
|
||||
buffered_terminal.terminal().set_raw_mode()?;
|
||||
buffered_terminal.terminal().enter_alternate_screen()?;
|
||||
Ok(TermwizBackend { buffered_terminal })
|
||||
Ok(Self { buffered_terminal })
|
||||
}
|
||||
|
||||
/// Creates a new Termwiz backend instance with the given buffered terminal.
|
||||
pub fn with_buffered_terminal(instance: BufferedTerminal<SystemTerminal>) -> TermwizBackend {
|
||||
TermwizBackend {
|
||||
pub const fn with_buffered_terminal(instance: BufferedTerminal<SystemTerminal>) -> Self {
|
||||
Self {
|
||||
buffered_terminal: instance,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a reference to the buffered terminal used by the backend.
|
||||
pub fn buffered_terminal(&self) -> &BufferedTerminal<SystemTerminal> {
|
||||
pub const fn buffered_terminal(&self) -> &BufferedTerminal<SystemTerminal> {
|
||||
&self.buffered_terminal
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ impl TermwizBackend {
|
||||
}
|
||||
|
||||
impl Backend for TermwizBackend {
|
||||
fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error>
|
||||
fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
|
||||
where
|
||||
I: Iterator<Item = (u16, u16, &'a Cell)>,
|
||||
{
|
||||
@@ -181,13 +181,13 @@ impl Backend for TermwizBackend {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn hide_cursor(&mut self) -> Result<(), io::Error> {
|
||||
fn hide_cursor(&mut self) -> io::Result<()> {
|
||||
self.buffered_terminal
|
||||
.add_change(Change::CursorVisibility(CursorVisibility::Hidden));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn show_cursor(&mut self) -> Result<(), io::Error> {
|
||||
fn show_cursor(&mut self) -> io::Result<()> {
|
||||
self.buffered_terminal
|
||||
.add_change(Change::CursorVisibility(CursorVisibility::Visible));
|
||||
Ok(())
|
||||
@@ -207,18 +207,18 @@ impl Backend for TermwizBackend {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear(&mut self) -> Result<(), io::Error> {
|
||||
fn clear(&mut self) -> io::Result<()> {
|
||||
self.buffered_terminal
|
||||
.add_change(Change::ClearScreen(termwiz::color::ColorAttribute::Default));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn size(&self) -> Result<Rect, io::Error> {
|
||||
fn size(&self) -> io::Result<Rect> {
|
||||
let (cols, rows) = self.buffered_terminal.dimensions();
|
||||
Ok(Rect::new(0, 0, u16_max(cols), u16_max(rows)))
|
||||
}
|
||||
|
||||
fn window_size(&mut self) -> Result<WindowSize, io::Error> {
|
||||
fn window_size(&mut self) -> io::Result<WindowSize> {
|
||||
let ScreenSize {
|
||||
cols,
|
||||
rows,
|
||||
@@ -241,7 +241,7 @@ impl Backend for TermwizBackend {
|
||||
})
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> Result<(), io::Error> {
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.buffered_terminal
|
||||
.flush()
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
|
||||
@@ -251,7 +251,7 @@ impl Backend for TermwizBackend {
|
||||
|
||||
impl From<CellAttributes> for Style {
|
||||
fn from(value: CellAttributes) -> Self {
|
||||
let mut style = Style::new()
|
||||
let mut style = Self::new()
|
||||
.add_modifier(value.intensity().into())
|
||||
.add_modifier(value.underline().into())
|
||||
.add_modifier(value.blink().into());
|
||||
@@ -271,7 +271,7 @@ impl From<CellAttributes> for Style {
|
||||
|
||||
style.fg = Some(value.foreground().into());
|
||||
style.bg = Some(value.background().into());
|
||||
#[cfg(feature = "underline_color")]
|
||||
#[cfg(feature = "underline-color")]
|
||||
{
|
||||
style.underline_color = Some(value.underline_color().into());
|
||||
}
|
||||
@@ -283,9 +283,9 @@ impl From<CellAttributes> for 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,
|
||||
Intensity::Normal => Self::empty(),
|
||||
Intensity::Bold => Self::BOLD,
|
||||
Intensity::Half => Self::DIM,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -293,8 +293,8 @@ impl From<Intensity> for Modifier {
|
||||
impl From<Underline> for Modifier {
|
||||
fn from(value: Underline) -> Self {
|
||||
match value {
|
||||
Underline::None => Modifier::empty(),
|
||||
_ => Modifier::UNDERLINED,
|
||||
Underline::None => Self::empty(),
|
||||
_ => Self::UNDERLINED,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -302,17 +302,17 @@ impl From<Underline> for Modifier {
|
||||
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,
|
||||
Blink::None => Self::empty(),
|
||||
Blink::Slow => Self::SLOW_BLINK,
|
||||
Blink::Rapid => Self::RAPID_BLINK,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color> for ColorAttribute {
|
||||
fn from(color: Color) -> ColorAttribute {
|
||||
fn from(color: Color) -> Self {
|
||||
match color {
|
||||
Color::Reset => ColorAttribute::Default,
|
||||
Color::Reset => Self::Default,
|
||||
Color::Black => AnsiColor::Black.into(),
|
||||
Color::DarkGray => AnsiColor::Grey.into(),
|
||||
Color::Gray => AnsiColor::Silver.into(),
|
||||
@@ -329,10 +329,8 @@ impl From<Color> for ColorAttribute {
|
||||
Color::White => AnsiColor::White.into(),
|
||||
Color::Blue => AnsiColor::Navy.into(),
|
||||
Color::LightBlue => AnsiColor::Blue.into(),
|
||||
Color::Indexed(i) => ColorAttribute::PaletteIndex(i),
|
||||
Color::Rgb(r, g, b) => {
|
||||
ColorAttribute::TrueColorWithDefaultFallback(SrgbaTuple::from((r, g, b)))
|
||||
}
|
||||
Color::Indexed(i) => Self::PaletteIndex(i),
|
||||
Color::Rgb(r, g, b) => Self::TrueColorWithDefaultFallback(SrgbaTuple::from((r, g, b))),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -340,22 +338,22 @@ 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,
|
||||
AnsiColor::Black => Self::Black,
|
||||
AnsiColor::Grey => Self::DarkGray,
|
||||
AnsiColor::Silver => Self::Gray,
|
||||
AnsiColor::Maroon => Self::Red,
|
||||
AnsiColor::Red => Self::LightRed,
|
||||
AnsiColor::Green => Self::Green,
|
||||
AnsiColor::Lime => Self::LightGreen,
|
||||
AnsiColor::Olive => Self::Yellow,
|
||||
AnsiColor::Yellow => Self::LightYellow,
|
||||
AnsiColor::Purple => Self::Magenta,
|
||||
AnsiColor::Fuchsia => Self::LightMagenta,
|
||||
AnsiColor::Teal => Self::Cyan,
|
||||
AnsiColor::Aqua => Self::LightCyan,
|
||||
AnsiColor::White => Self::White,
|
||||
AnsiColor::Navy => Self::Blue,
|
||||
AnsiColor::Blue => Self::LightBlue,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -365,8 +363,8 @@ impl From<ColorAttribute> for Color {
|
||||
match value {
|
||||
ColorAttribute::TrueColorWithDefaultFallback(srgba)
|
||||
| ColorAttribute::TrueColorWithPaletteFallback(srgba, _) => srgba.into(),
|
||||
ColorAttribute::PaletteIndex(i) => Color::Indexed(i),
|
||||
ColorAttribute::Default => Color::Reset,
|
||||
ColorAttribute::PaletteIndex(i) => Self::Indexed(i),
|
||||
ColorAttribute::Default => Self::Reset,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -374,8 +372,8 @@ impl From<ColorAttribute> for Color {
|
||||
impl From<ColorSpec> for Color {
|
||||
fn from(value: ColorSpec) -> Self {
|
||||
match value {
|
||||
ColorSpec::Default => Color::Reset,
|
||||
ColorSpec::PaletteIndex(i) => Color::Indexed(i),
|
||||
ColorSpec::Default => Self::Reset,
|
||||
ColorSpec::PaletteIndex(i) => Self::Indexed(i),
|
||||
ColorSpec::TrueColor(srgba) => srgba.into(),
|
||||
}
|
||||
}
|
||||
@@ -384,14 +382,14 @@ impl From<ColorSpec> for Color {
|
||||
impl From<SrgbaTuple> for Color {
|
||||
fn from(value: SrgbaTuple) -> Self {
|
||||
let (r, g, b, _) = value.to_srgb_u8();
|
||||
Color::Rgb(r, g, b)
|
||||
Self::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)
|
||||
Self::Rgb(r, g, b)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -409,7 +407,6 @@ fn u16_max(i: usize) -> u16 {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::style::Stylize;
|
||||
|
||||
mod into_color {
|
||||
use Color as C;
|
||||
@@ -578,11 +575,19 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn from_cell_attribute_for_style() {
|
||||
use crate::style::Stylize;
|
||||
|
||||
#[cfg(feature = "underline-color")]
|
||||
const STYLE: Style = Style::new()
|
||||
.underline_color(Color::Reset)
|
||||
.fg(Color::Reset)
|
||||
.bg(Color::Reset);
|
||||
#[cfg(not(feature = "underline-color"))]
|
||||
const STYLE: Style = Style::new().fg(Color::Reset).bg(Color::Reset);
|
||||
|
||||
// default
|
||||
assert_eq!(
|
||||
Style::from(CellAttributes::default()),
|
||||
Style::new().fg(Color::Reset).bg(Color::Reset)
|
||||
);
|
||||
assert_eq!(Style::from(CellAttributes::default()), STYLE);
|
||||
|
||||
// foreground color
|
||||
assert_eq!(
|
||||
Style::from(
|
||||
@@ -590,7 +595,7 @@ mod tests {
|
||||
.set_foreground(ColorAttribute::PaletteIndex(31))
|
||||
.to_owned()
|
||||
),
|
||||
Style::new().fg(Color::Indexed(31)).bg(Color::Reset)
|
||||
STYLE.fg(Color::Indexed(31))
|
||||
);
|
||||
// background color
|
||||
assert_eq!(
|
||||
@@ -599,21 +604,7 @@ mod tests {
|
||||
.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)
|
||||
STYLE.bg(Color::Indexed(31))
|
||||
);
|
||||
// underlined
|
||||
assert_eq!(
|
||||
@@ -622,12 +613,12 @@ mod tests {
|
||||
.set_underline(Underline::Single)
|
||||
.to_owned()
|
||||
),
|
||||
Style::new().fg(Color::Reset).bg(Color::Reset).underlined()
|
||||
STYLE.underlined()
|
||||
);
|
||||
// blink
|
||||
assert_eq!(
|
||||
Style::from(CellAttributes::default().set_blink(Blink::Slow).to_owned()),
|
||||
Style::new().fg(Color::Reset).bg(Color::Reset).slow_blink()
|
||||
STYLE.slow_blink()
|
||||
);
|
||||
// intensity
|
||||
assert_eq!(
|
||||
@@ -636,27 +627,38 @@ mod tests {
|
||||
.set_intensity(Intensity::Bold)
|
||||
.to_owned()
|
||||
),
|
||||
Style::new().fg(Color::Reset).bg(Color::Reset).bold()
|
||||
STYLE.bold()
|
||||
);
|
||||
// italic
|
||||
assert_eq!(
|
||||
Style::from(CellAttributes::default().set_italic(true).to_owned()),
|
||||
Style::new().fg(Color::Reset).bg(Color::Reset).italic()
|
||||
STYLE.italic()
|
||||
);
|
||||
// reversed
|
||||
assert_eq!(
|
||||
Style::from(CellAttributes::default().set_reverse(true).to_owned()),
|
||||
Style::new().fg(Color::Reset).bg(Color::Reset).reversed()
|
||||
STYLE.reversed()
|
||||
);
|
||||
// strikethrough
|
||||
assert_eq!(
|
||||
Style::from(CellAttributes::default().set_strikethrough(true).to_owned()),
|
||||
Style::new().fg(Color::Reset).bg(Color::Reset).crossed_out()
|
||||
STYLE.crossed_out()
|
||||
);
|
||||
// hidden
|
||||
assert_eq!(
|
||||
Style::from(CellAttributes::default().set_invisible(true).to_owned()),
|
||||
Style::new().fg(Color::Reset).bg(Color::Reset).hidden()
|
||||
STYLE.hidden()
|
||||
);
|
||||
|
||||
// underline color
|
||||
#[cfg(feature = "underline-color")]
|
||||
assert_eq!(
|
||||
Style::from(
|
||||
CellAttributes::default()
|
||||
.set_underline_color(AnsiColor::Red)
|
||||
.to_owned()
|
||||
),
|
||||
STYLE.underline_color(Color::Indexed(9))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//! It is used in the integration tests to verify the correctness of the library.
|
||||
|
||||
use std::{
|
||||
fmt::{Display, Write},
|
||||
fmt::{self, Write},
|
||||
io,
|
||||
};
|
||||
|
||||
@@ -14,8 +14,7 @@ use crate::{
|
||||
layout::{Rect, Size},
|
||||
};
|
||||
|
||||
/// A [`Backend`] implementation used for integration testing that that renders to an in memory
|
||||
/// buffer.
|
||||
/// A [`Backend`] implementation used for integration testing that renders to an memory buffer.
|
||||
///
|
||||
/// Note: that although many of the integration and unit tests in ratatui are written using this
|
||||
/// backend, it is preferable to write unit tests for widgets directly against the buffer rather
|
||||
@@ -29,7 +28,7 @@ use crate::{
|
||||
///
|
||||
/// let mut backend = TestBackend::new(10, 2);
|
||||
/// backend.clear()?;
|
||||
/// backend.assert_buffer(&Buffer::with_lines(vec![" "; 2]));
|
||||
/// backend.assert_buffer_lines([" "; 2]);
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
@@ -72,9 +71,9 @@ fn buffer_view(buffer: &Buffer) -> String {
|
||||
}
|
||||
|
||||
impl TestBackend {
|
||||
/// Creates a new TestBackend with the specified width and height.
|
||||
pub fn new(width: u16, height: u16) -> TestBackend {
|
||||
TestBackend {
|
||||
/// Creates a new `TestBackend` with the specified width and height.
|
||||
pub fn new(width: u16, height: u16) -> Self {
|
||||
Self {
|
||||
width,
|
||||
height,
|
||||
buffer: Buffer::empty(Rect::new(0, 0, width, height)),
|
||||
@@ -83,68 +82,59 @@ impl TestBackend {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a reference to the internal buffer of the TestBackend.
|
||||
pub fn buffer(&self) -> &Buffer {
|
||||
/// Returns a reference to the internal buffer of the `TestBackend`.
|
||||
pub const fn buffer(&self) -> &Buffer {
|
||||
&self.buffer
|
||||
}
|
||||
|
||||
/// Resizes the TestBackend to the specified width and height.
|
||||
/// Resizes the `TestBackend` to the specified width and height.
|
||||
pub fn resize(&mut self, width: u16, height: u16) {
|
||||
self.buffer.resize(Rect::new(0, 0, width, height));
|
||||
self.width = width;
|
||||
self.height = height;
|
||||
}
|
||||
|
||||
/// Asserts that the TestBackend's buffer is equal to the expected buffer.
|
||||
/// If the buffers are not equal, a panic occurs with a detailed error message
|
||||
/// showing the differences between the expected and actual buffers.
|
||||
/// Asserts that the `TestBackend`'s buffer is equal to the expected buffer.
|
||||
///
|
||||
/// This is a shortcut for `assert_eq!(self.buffer(), &expected)`.
|
||||
///
|
||||
/// # Panics
|
||||
/// When they are not equal, a panic occurs with a detailed error message showing the
|
||||
/// differences between the expected and actual buffers.
|
||||
#[allow(deprecated)]
|
||||
#[track_caller]
|
||||
pub fn assert_buffer(&self, expected: &Buffer) {
|
||||
assert_eq!(expected.area, self.buffer.area);
|
||||
let diff = expected.diff(&self.buffer);
|
||||
if diff.is_empty() {
|
||||
return;
|
||||
}
|
||||
// TODO: use assert_eq!()
|
||||
crate::assert_buffer_eq!(&self.buffer, expected);
|
||||
}
|
||||
|
||||
let mut debug_info = String::from("Buffers are not equal");
|
||||
debug_info.push('\n');
|
||||
debug_info.push_str("Expected:");
|
||||
debug_info.push('\n');
|
||||
let expected_view = buffer_view(expected);
|
||||
debug_info.push_str(&expected_view);
|
||||
debug_info.push('\n');
|
||||
debug_info.push_str("Got:");
|
||||
debug_info.push('\n');
|
||||
let view = buffer_view(&self.buffer);
|
||||
debug_info.push_str(&view);
|
||||
debug_info.push('\n');
|
||||
|
||||
debug_info.push_str("Diff:");
|
||||
debug_info.push('\n');
|
||||
let nice_diff = diff
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, (x, y, cell))| {
|
||||
let expected_cell = expected.get(*x, *y);
|
||||
format!("{i}: at ({x}, {y}) expected {expected_cell:?} got {cell:?}")
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n");
|
||||
debug_info.push_str(&nice_diff);
|
||||
panic!("{debug_info}");
|
||||
/// Asserts that the `TestBackend`'s buffer is equal to the expected lines.
|
||||
///
|
||||
/// This is a shortcut for `assert_eq!(self.buffer(), &Buffer::with_lines(expected))`.
|
||||
///
|
||||
/// # Panics
|
||||
/// When they are not equal, a panic occurs with a detailed error message showing the
|
||||
/// differences between the expected and actual buffers.
|
||||
#[track_caller]
|
||||
pub fn assert_buffer_lines<'line, Lines>(&self, expected: Lines)
|
||||
where
|
||||
Lines: IntoIterator,
|
||||
Lines::Item: Into<crate::text::Line<'line>>,
|
||||
{
|
||||
self.assert_buffer(&Buffer::with_lines(expected));
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for TestBackend {
|
||||
/// Formats the TestBackend for display by calling the buffer_view function
|
||||
impl fmt::Display for TestBackend {
|
||||
/// Formats the `TestBackend` for display by calling the `buffer_view` function
|
||||
/// on its internal buffer.
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", buffer_view(&self.buffer))
|
||||
}
|
||||
}
|
||||
|
||||
impl Backend for TestBackend {
|
||||
fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error>
|
||||
fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
|
||||
where
|
||||
I: Iterator<Item = (u16, u16, &'a Cell)>,
|
||||
{
|
||||
@@ -155,26 +145,26 @@ impl Backend for TestBackend {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn hide_cursor(&mut self) -> Result<(), io::Error> {
|
||||
fn hide_cursor(&mut self) -> io::Result<()> {
|
||||
self.cursor = false;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn show_cursor(&mut self) -> Result<(), io::Error> {
|
||||
fn show_cursor(&mut self) -> io::Result<()> {
|
||||
self.cursor = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_cursor(&mut self) -> Result<(u16, u16), io::Error> {
|
||||
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
|
||||
Ok(self.pos)
|
||||
}
|
||||
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> Result<(), io::Error> {
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
||||
self.pos = (x, y);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear(&mut self) -> Result<(), io::Error> {
|
||||
fn clear(&mut self) -> io::Result<()> {
|
||||
self.buffer.reset();
|
||||
Ok(())
|
||||
}
|
||||
@@ -244,11 +234,11 @@ impl Backend for TestBackend {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn size(&self) -> Result<Rect, io::Error> {
|
||||
fn size(&self) -> io::Result<Rect> {
|
||||
Ok(Rect::new(0, 0, self.width, self.height))
|
||||
}
|
||||
|
||||
fn window_size(&mut self) -> Result<WindowSize, io::Error> {
|
||||
fn window_size(&mut self) -> io::Result<WindowSize> {
|
||||
// Some arbitrary window pixel size, probably doesn't need much testing.
|
||||
static WINDOW_PIXEL_SIZE: Size = Size {
|
||||
width: 640,
|
||||
@@ -260,7 +250,7 @@ impl Backend for TestBackend {
|
||||
})
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> Result<(), io::Error> {
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -276,7 +266,7 @@ mod tests {
|
||||
TestBackend {
|
||||
width: 10,
|
||||
height: 2,
|
||||
buffer: Buffer::with_lines(vec![" "; 2]),
|
||||
buffer: Buffer::with_lines([" "; 2]),
|
||||
cursor: false,
|
||||
pos: (0, 0),
|
||||
}
|
||||
@@ -284,20 +274,19 @@ mod tests {
|
||||
}
|
||||
#[test]
|
||||
fn test_buffer_view() {
|
||||
let buffer = Buffer::with_lines(vec!["aaaa"; 2]);
|
||||
let buffer = Buffer::with_lines(["aaaa"; 2]);
|
||||
assert_eq!(buffer_view(&buffer), "\"aaaa\"\n\"aaaa\"\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_view_with_overwrites() {
|
||||
let multi_byte_char = "👨👩👧👦"; // renders 8 wide
|
||||
let buffer = Buffer::with_lines(vec![multi_byte_char]);
|
||||
let buffer = Buffer::with_lines([multi_byte_char]);
|
||||
assert_eq!(
|
||||
buffer_view(&buffer),
|
||||
format!(
|
||||
r#""{multi_byte_char}" Hidden by multi-width symbols: [(1, " "), (2, " "), (3, " "), (4, " "), (5, " "), (6, " "), (7, " ")]
|
||||
"#,
|
||||
multi_byte_char = multi_byte_char
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -305,35 +294,33 @@ mod tests {
|
||||
#[test]
|
||||
fn buffer() {
|
||||
let backend = TestBackend::new(10, 2);
|
||||
assert_eq!(backend.buffer(), &Buffer::with_lines(vec![" "; 2]));
|
||||
backend.assert_buffer_lines([" "; 2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resize() {
|
||||
let mut backend = TestBackend::new(10, 2);
|
||||
backend.resize(5, 5);
|
||||
assert_eq!(backend.buffer(), &Buffer::with_lines(vec![" "; 5]));
|
||||
backend.assert_buffer_lines([" "; 5]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn assert_buffer() {
|
||||
let backend = TestBackend::new(10, 2);
|
||||
let buffer = Buffer::with_lines(vec![" "; 2]);
|
||||
backend.assert_buffer(&buffer);
|
||||
backend.assert_buffer_lines([" "; 2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
#[should_panic = "buffer contents not equal"]
|
||||
fn assert_buffer_panics() {
|
||||
let backend = TestBackend::new(10, 2);
|
||||
let buffer = Buffer::with_lines(vec!["aaaaaaaaaa"; 2]);
|
||||
backend.assert_buffer(&buffer);
|
||||
backend.assert_buffer_lines(["aaaaaaaaaa"; 2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display() {
|
||||
let backend = TestBackend::new(10, 2);
|
||||
assert_eq!(format!("{}", backend), "\" \"\n\" \"\n");
|
||||
assert_eq!(format!("{backend}"), "\" \"\n\" \"\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -343,7 +330,7 @@ mod tests {
|
||||
cell.set_symbol("a");
|
||||
backend.draw([(0, 0, &cell)].into_iter()).unwrap();
|
||||
backend.draw([(0, 1, &cell)].into_iter()).unwrap();
|
||||
backend.assert_buffer(&Buffer::with_lines(vec!["a "; 2]));
|
||||
backend.assert_buffer_lines(["a "; 2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -375,24 +362,19 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn clear() {
|
||||
let mut backend = TestBackend::new(10, 4);
|
||||
let mut backend = TestBackend::new(4, 2);
|
||||
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![
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
]));
|
||||
backend.assert_buffer_lines([" ", " "]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_region_all() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines(vec![
|
||||
backend.buffer = Buffer::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
@@ -401,19 +383,19 @@ mod tests {
|
||||
]);
|
||||
|
||||
backend.clear_region(ClearType::All).unwrap();
|
||||
backend.assert_buffer(&Buffer::with_lines(vec![
|
||||
backend.assert_buffer_lines([
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
]));
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_region_after_cursor() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines(vec![
|
||||
backend.buffer = Buffer::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
@@ -423,19 +405,19 @@ mod tests {
|
||||
|
||||
backend.set_cursor(3, 2).unwrap();
|
||||
backend.clear_region(ClearType::AfterCursor).unwrap();
|
||||
backend.assert_buffer(&Buffer::with_lines(vec![
|
||||
backend.assert_buffer_lines([
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaa ",
|
||||
" ",
|
||||
" ",
|
||||
]));
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_region_before_cursor() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines(vec![
|
||||
backend.buffer = Buffer::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
@@ -445,19 +427,19 @@ mod tests {
|
||||
|
||||
backend.set_cursor(5, 3).unwrap();
|
||||
backend.clear_region(ClearType::BeforeCursor).unwrap();
|
||||
backend.assert_buffer(&Buffer::with_lines(vec![
|
||||
backend.assert_buffer_lines([
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" aaaaa",
|
||||
"aaaaaaaaaa",
|
||||
]));
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_region_current_line() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines(vec![
|
||||
backend.buffer = Buffer::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
@@ -467,19 +449,19 @@ mod tests {
|
||||
|
||||
backend.set_cursor(3, 1).unwrap();
|
||||
backend.clear_region(ClearType::CurrentLine).unwrap();
|
||||
backend.assert_buffer(&Buffer::with_lines(vec![
|
||||
backend.assert_buffer_lines([
|
||||
"aaaaaaaaaa",
|
||||
" ",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
]));
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_region_until_new_line() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines(vec![
|
||||
backend.buffer = Buffer::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
@@ -489,19 +471,19 @@ mod tests {
|
||||
|
||||
backend.set_cursor(3, 0).unwrap();
|
||||
backend.clear_region(ClearType::UntilNewLine).unwrap();
|
||||
backend.assert_buffer(&Buffer::with_lines(vec![
|
||||
backend.assert_buffer_lines([
|
||||
"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![
|
||||
backend.buffer = Buffer::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
@@ -527,19 +509,19 @@ mod tests {
|
||||
assert_eq!(backend.get_cursor().unwrap(), (4, 4));
|
||||
|
||||
// As such the buffer should remain unchanged
|
||||
backend.assert_buffer(&Buffer::with_lines(vec![
|
||||
backend.assert_buffer_lines([
|
||||
"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![
|
||||
backend.buffer = Buffer::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
@@ -553,7 +535,7 @@ mod tests {
|
||||
|
||||
backend.append_lines(1).unwrap();
|
||||
|
||||
backend.buffer = Buffer::with_lines(vec![
|
||||
backend.buffer = Buffer::with_lines([
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
"dddddddddd",
|
||||
@@ -569,7 +551,7 @@ mod tests {
|
||||
#[test]
|
||||
fn append_multiple_lines_not_at_last_line() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines(vec![
|
||||
backend.buffer = Buffer::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
@@ -586,19 +568,19 @@ mod tests {
|
||||
assert_eq!(backend.get_cursor().unwrap(), (1, 4));
|
||||
|
||||
// As such the buffer should remain unchanged
|
||||
backend.assert_buffer(&Buffer::with_lines(vec![
|
||||
backend.assert_buffer_lines([
|
||||
"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![
|
||||
backend.buffer = Buffer::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
@@ -611,19 +593,19 @@ mod tests {
|
||||
backend.append_lines(3).unwrap();
|
||||
assert_eq!(backend.get_cursor().unwrap(), (1, 4));
|
||||
|
||||
backend.assert_buffer(&Buffer::with_lines(vec![
|
||||
backend.assert_buffer_lines([
|
||||
"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![
|
||||
backend.buffer = Buffer::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
@@ -636,19 +618,19 @@ mod tests {
|
||||
backend.append_lines(5).unwrap();
|
||||
assert_eq!(backend.get_cursor().unwrap(), (1, 4));
|
||||
|
||||
backend.assert_buffer(&Buffer::with_lines(vec![
|
||||
backend.assert_buffer_lines([
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
]));
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_multiple_lines_where_cursor_appends_height_lines() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines(vec![
|
||||
backend.buffer = Buffer::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
@@ -661,13 +643,13 @@ mod tests {
|
||||
backend.append_lines(5).unwrap();
|
||||
assert_eq!(backend.get_cursor().unwrap(), (1, 4));
|
||||
|
||||
backend.assert_buffer(&Buffer::with_lines(vec![
|
||||
backend.assert_buffer_lines([
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
"dddddddddd",
|
||||
"eeeeeeeeee",
|
||||
" ",
|
||||
]));
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,57 +1,44 @@
|
||||
/// 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.
|
||||
/// # Panics
|
||||
/// When the buffers differ this method panics and displays the differences similar to
|
||||
/// `assert_eq!()`.
|
||||
#[deprecated = "use assert_eq!(&actual, &expected)"]
|
||||
#[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
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
actual.area == expected.area,
|
||||
"buffer areas not equal\nexpected: {expected:?}\nactual: {actual:?}",
|
||||
);
|
||||
let nice_diff = expected
|
||||
.diff(actual)
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, (x, y, cell))| {
|
||||
let expected_cell = expected.get(x, y);
|
||||
format!("{i}: at ({x}, {y})\n expected: {expected_cell:?}\n actual: {cell:?}")
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n");
|
||||
assert!(
|
||||
nice_diff.is_empty(),
|
||||
"buffer contents not equal\nexpected: {expected:?}\nactual: {actual:?}\ndiff:\n{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");
|
||||
assert_eq!(
|
||||
actual, expected,
|
||||
"buffers are not equal in an unexpected way. Please open an issue about this."
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::prelude::*;
|
||||
@@ -63,7 +50,7 @@ mod tests {
|
||||
assert_buffer_eq!(buffer, other_buffer);
|
||||
}
|
||||
|
||||
#[should_panic]
|
||||
#[should_panic = "buffer areas not equal"]
|
||||
#[test]
|
||||
fn assert_buffer_eq_panics_on_unequal_area() {
|
||||
let buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
|
||||
@@ -71,7 +58,7 @@ mod tests {
|
||||
assert_buffer_eq!(buffer, other_buffer);
|
||||
}
|
||||
|
||||
#[should_panic]
|
||||
#[should_panic = "buffer contents not equal"]
|
||||
#[test]
|
||||
fn assert_buffer_eq_panics_on_unequal_style() {
|
||||
let buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
use std::{
|
||||
cmp::min,
|
||||
fmt::{Debug, Formatter, Result},
|
||||
};
|
||||
use std::fmt;
|
||||
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
@@ -55,23 +52,22 @@ pub struct Buffer {
|
||||
|
||||
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)
|
||||
#[must_use]
|
||||
pub fn empty(area: Rect) -> Self {
|
||||
Self::filled(area, &Cell::default())
|
||||
}
|
||||
|
||||
/// Returns a Buffer with all cells initialized with the attributes of the given Cell
|
||||
pub fn filled(area: Rect, cell: &Cell) -> Buffer {
|
||||
#[must_use]
|
||||
pub fn filled(area: Rect, cell: &Cell) -> Self {
|
||||
let size = area.area() as usize;
|
||||
let mut content = Vec::with_capacity(size);
|
||||
for _ in 0..size {
|
||||
content.push(cell.clone());
|
||||
}
|
||||
Buffer { area, content }
|
||||
let content = vec![cell.clone(); size];
|
||||
Self { area, content }
|
||||
}
|
||||
|
||||
/// Returns a Buffer containing the given lines
|
||||
pub fn with_lines<'a, Iter>(lines: Iter) -> Buffer
|
||||
#[must_use]
|
||||
pub fn with_lines<'a, Iter>(lines: Iter) -> Self
|
||||
where
|
||||
Iter: IntoIterator,
|
||||
Iter::Item: Into<Line<'a>>,
|
||||
@@ -79,7 +75,7 @@ impl Buffer {
|
||||
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));
|
||||
let mut buffer = Self::empty(Rect::new(0, 0, width, height));
|
||||
for (y, line) in lines.iter().enumerate() {
|
||||
buffer.set_line(0, y as u16, line, width);
|
||||
}
|
||||
@@ -92,17 +88,19 @@ impl Buffer {
|
||||
}
|
||||
|
||||
/// Returns the area covered by this buffer
|
||||
pub fn area(&self) -> &Rect {
|
||||
pub const fn area(&self) -> &Rect {
|
||||
&self.area
|
||||
}
|
||||
|
||||
/// Returns a reference to Cell at the given coordinates
|
||||
#[track_caller]
|
||||
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
|
||||
#[track_caller]
|
||||
pub fn get_mut(&mut self, x: u16, y: u16) -> &mut Cell {
|
||||
let i = self.index_of(x, y);
|
||||
&mut self.content[i]
|
||||
@@ -134,6 +132,7 @@ impl Buffer {
|
||||
/// // starts at (200, 100).
|
||||
/// buffer.index_of(0, 0); // Panics
|
||||
/// ```
|
||||
#[track_caller]
|
||||
pub fn index_of(&self, x: u16, y: u16) -> usize {
|
||||
debug_assert!(
|
||||
x >= self.area.left()
|
||||
@@ -184,65 +183,56 @@ impl Buffer {
|
||||
}
|
||||
|
||||
/// 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());
|
||||
self.set_stringn(x, y, string, usize::MAX, style);
|
||||
}
|
||||
|
||||
/// Print at most the first n characters of a string if enough space is available
|
||||
/// until the end of the line
|
||||
/// 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>`]).
|
||||
/// Use [`Buffer::set_string`] when the maximum amount of characters can be printed.
|
||||
pub fn set_stringn<T, S>(
|
||||
&mut self,
|
||||
x: u16,
|
||||
mut x: u16,
|
||||
y: u16,
|
||||
string: T,
|
||||
width: usize,
|
||||
max_width: usize,
|
||||
style: S,
|
||||
) -> (u16, u16)
|
||||
where
|
||||
T: AsRef<str>,
|
||||
S: Into<Style>,
|
||||
{
|
||||
let max_width = max_width.try_into().unwrap_or(u16::MAX);
|
||||
let mut remaining_width = self.area.right().saturating_sub(x).min(max_width);
|
||||
let graphemes = UnicodeSegmentation::graphemes(string.as_ref(), true)
|
||||
.map(|symbol| (symbol, symbol.width() as u16))
|
||||
.filter(|(_symbol, width)| *width > 0)
|
||||
.map_while(|(symbol, width)| {
|
||||
remaining_width = remaining_width.checked_sub(width)?;
|
||||
Some((symbol, width))
|
||||
});
|
||||
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);
|
||||
for (symbol, width) in graphemes {
|
||||
self.get_mut(x, y).set_symbol(symbol).set_style(style);
|
||||
let next_symbol = x + width;
|
||||
x += 1;
|
||||
// Reset following cells if multi-width (they would be hidden by the grapheme),
|
||||
for i in index + 1..index + width {
|
||||
self.content[i].reset();
|
||||
while x < next_symbol {
|
||||
self.get_mut(x, y).reset();
|
||||
x += 1;
|
||||
}
|
||||
index += width;
|
||||
x_offset += width;
|
||||
}
|
||||
(x_offset as u16, y)
|
||||
(x, 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;
|
||||
pub fn set_line(&mut self, x: u16, y: u16, line: &Line<'_>, max_width: u16) -> (u16, u16) {
|
||||
let mut remaining_width = max_width;
|
||||
let mut x = x;
|
||||
for span in line {
|
||||
if remaining_width == 0 {
|
||||
@@ -253,7 +243,7 @@ impl Buffer {
|
||||
y,
|
||||
span.content.as_ref(),
|
||||
remaining_width as usize,
|
||||
span.style,
|
||||
line.style.patch(span.style),
|
||||
);
|
||||
let w = pos.0.saturating_sub(x);
|
||||
x = pos.0;
|
||||
@@ -263,8 +253,8 @@ impl Buffer {
|
||||
}
|
||||
|
||||
/// 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)
|
||||
pub fn set_span(&mut self, x: u16, y: u16, span: &Span<'_>, max_width: u16) -> (u16, u16) {
|
||||
self.set_stringn(x, y, &span.content, max_width as usize, span.style)
|
||||
}
|
||||
|
||||
/// Set the style of all cells in the given area.
|
||||
@@ -301,10 +291,9 @@ impl Buffer {
|
||||
}
|
||||
|
||||
/// Merge an other buffer into this one
|
||||
pub fn merge(&mut self, other: &Buffer) {
|
||||
pub fn merge(&mut self, other: &Self) {
|
||||
let area = self.area.union(other.area);
|
||||
let cell = Cell::default();
|
||||
self.content.resize(area.area() as usize, cell.clone());
|
||||
self.content.resize(area.area() as usize, Cell::default());
|
||||
|
||||
// Move original content to the appropriate space
|
||||
let size = self.area.area() as usize;
|
||||
@@ -314,7 +303,7 @@ impl Buffer {
|
||||
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();
|
||||
self.content[i] = Cell::default();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -358,7 +347,7 @@ impl Buffer {
|
||||
/// 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)> {
|
||||
pub fn diff<'a>(&self, other: &'a Self) -> Vec<(u16, u16, &'a Cell)> {
|
||||
let previous_buffer = &self.content;
|
||||
let next_buffer = &other.content;
|
||||
|
||||
@@ -383,7 +372,7 @@ impl Buffer {
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Buffer {
|
||||
impl fmt::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:
|
||||
@@ -391,11 +380,14 @@ impl Debug for Buffer {
|
||||
/// * `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
|
||||
))?;
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_fmt(format_args!("Buffer {{\n area: {:?}", &self.area))?;
|
||||
|
||||
if self.area.is_empty() {
|
||||
return f.write_str("\n}");
|
||||
}
|
||||
|
||||
f.write_str(",\n content: [\n")?;
|
||||
let mut last_style = None;
|
||||
let mut styles = vec![];
|
||||
for (y, line) in self.content.chunks(self.area.width as usize).enumerate() {
|
||||
@@ -426,12 +418,13 @@ impl Debug for Buffer {
|
||||
}
|
||||
}
|
||||
}
|
||||
f.write_str("\",")?;
|
||||
if !overwritten.is_empty() {
|
||||
f.write_fmt(format_args!(
|
||||
"// hidden by multi-width symbols: {overwritten:?}"
|
||||
" // hidden by multi-width symbols: {overwritten:?}"
|
||||
))?;
|
||||
}
|
||||
f.write_str("\",\n")?;
|
||||
f.write_str("\n")?;
|
||||
}
|
||||
f.write_str(" ],\n styles: [\n")?;
|
||||
for s in styles {
|
||||
@@ -453,8 +446,12 @@ impl Debug for Buffer {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::iter;
|
||||
|
||||
use itertools::Itertools;
|
||||
use rstest::{fixture, rstest};
|
||||
|
||||
use super::*;
|
||||
use crate::assert_buffer_eq;
|
||||
|
||||
fn cell(s: &str) -> Cell {
|
||||
let mut cell = Cell::default();
|
||||
@@ -463,10 +460,40 @@ mod tests {
|
||||
}
|
||||
|
||||
#[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(
|
||||
fn debug_empty_buffer() {
|
||||
let buffer = Buffer::empty(Rect::ZERO);
|
||||
let result = format!("{buffer:?}");
|
||||
println!("{result}");
|
||||
let expected = "Buffer {\n area: Rect { x: 0, y: 0, width: 0, height: 0 }\n}";
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
#[cfg(feature = "underline-color")]
|
||||
#[test]
|
||||
fn debug_grapheme_override() {
|
||||
let buffer = Buffer::with_lines(["a🦀b"]);
|
||||
let result = format!("{buffer:?}");
|
||||
println!("{result}");
|
||||
let expected = indoc::indoc!(
|
||||
r#"
|
||||
Buffer {
|
||||
area: Rect { x: 0, y: 0, width: 4, height: 1 },
|
||||
content: [
|
||||
"a🦀b", // hidden by multi-width symbols: [(2, " ")]
|
||||
],
|
||||
styles: [
|
||||
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
]
|
||||
}"#
|
||||
);
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn debug_some_example() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 12, 2));
|
||||
buffer.set_string(0, 0, "Hello World!", Style::default());
|
||||
buffer.set_string(
|
||||
0,
|
||||
1,
|
||||
"G'day World!",
|
||||
@@ -475,42 +502,40 @@ mod tests {
|
||||
.bg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
);
|
||||
let result = format!("{buffer:?}");
|
||||
println!("{result}");
|
||||
#[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,
|
||||
]
|
||||
}"
|
||||
)
|
||||
let expected = indoc::indoc!(
|
||||
r#"
|
||||
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,
|
||||
]
|
||||
}"
|
||||
)
|
||||
let expected = indoc::indoc!(
|
||||
r#"
|
||||
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,
|
||||
]
|
||||
}"#
|
||||
);
|
||||
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -554,27 +579,27 @@ mod tests {
|
||||
|
||||
// Zero-width
|
||||
buffer.set_stringn(0, 0, "aaa", 0, Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" "]));
|
||||
assert_eq!(buffer, Buffer::with_lines([" "]));
|
||||
|
||||
buffer.set_string(0, 0, "aaa", Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["aaa "]));
|
||||
assert_eq!(buffer, Buffer::with_lines(["aaa "]));
|
||||
|
||||
// Width limit:
|
||||
buffer.set_stringn(0, 0, "bbbbbbbbbbbbbb", 4, Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["bbbb "]));
|
||||
assert_eq!(buffer, Buffer::with_lines(["bbbb "]));
|
||||
|
||||
buffer.set_string(0, 0, "12345", Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["12345"]));
|
||||
assert_eq!(buffer, Buffer::with_lines(["12345"]));
|
||||
|
||||
// Width truncation:
|
||||
buffer.set_string(0, 0, "123456", Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["12345"]));
|
||||
assert_eq!(buffer, Buffer::with_lines(["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"]));
|
||||
assert_eq!(buffer, Buffer::with_lines(["12345", "67890"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -585,7 +610,7 @@ mod tests {
|
||||
// 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"]));
|
||||
assert_eq!(buffer, Buffer::with_lines(["称号a"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -596,12 +621,12 @@ mod tests {
|
||||
// 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"]));
|
||||
assert_eq!(buffer, Buffer::with_lines(["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"]));
|
||||
assert_eq!(buffer, Buffer::with_lines(["a"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -609,37 +634,109 @@ mod tests {
|
||||
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!["コン "]));
|
||||
assert_eq!(buffer, Buffer::with_lines(["コン "]));
|
||||
|
||||
// Only 1 space left.
|
||||
buffer.set_string(0, 0, "コンピ", Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["コン "]));
|
||||
assert_eq!(buffer, Buffer::with_lines(["コン "]));
|
||||
}
|
||||
|
||||
#[fixture]
|
||||
fn small_one_line_buffer() -> Buffer {
|
||||
Buffer::empty(Rect::new(0, 0, 5, 1))
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::empty("", " ")]
|
||||
#[case::one("1", "1 ")]
|
||||
#[case::full("12345", "12345")]
|
||||
#[case::overflow("123456", "12345")]
|
||||
fn set_line_raw(
|
||||
mut small_one_line_buffer: Buffer,
|
||||
#[case] content: &str,
|
||||
#[case] expected: &str,
|
||||
) {
|
||||
let line = Line::raw(content);
|
||||
small_one_line_buffer.set_line(0, 0, &line, 5);
|
||||
|
||||
// note: testing with empty / set_string here instead of with_lines because with_lines calls
|
||||
// set_line
|
||||
let mut expected_buffer = Buffer::empty(small_one_line_buffer.area);
|
||||
expected_buffer.set_string(0, 0, expected, Style::default());
|
||||
assert_eq!(small_one_line_buffer, expected_buffer);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::empty("", " ")]
|
||||
#[case::one("1", "1 ")]
|
||||
#[case::full("12345", "12345")]
|
||||
#[case::overflow("123456", "12345")]
|
||||
fn set_line_styled(
|
||||
mut small_one_line_buffer: Buffer,
|
||||
#[case] content: &str,
|
||||
#[case] expected: &str,
|
||||
) {
|
||||
let color = Color::Blue;
|
||||
let line = Line::styled(content, color);
|
||||
small_one_line_buffer.set_line(0, 0, &line, 5);
|
||||
|
||||
// note: manually testing the contents here as the Buffer::with_lines calls set_line
|
||||
let actual_contents = small_one_line_buffer
|
||||
.content
|
||||
.iter()
|
||||
.map(Cell::symbol)
|
||||
.join("");
|
||||
let actual_styles = small_one_line_buffer
|
||||
.content
|
||||
.iter()
|
||||
.map(|c| c.fg)
|
||||
.collect_vec();
|
||||
|
||||
// set_line only sets the style for non-empty cells (unlike Line::render which sets the
|
||||
// style for all cells)
|
||||
let expected_styles = iter::repeat(color)
|
||||
.take(content.len().min(5))
|
||||
.chain(iter::repeat(Color::default()).take(5_usize.saturating_sub(content.len())))
|
||||
.collect_vec();
|
||||
assert_eq!(actual_contents, expected);
|
||||
assert_eq!(actual_styles, expected_styles);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_style() {
|
||||
let mut buffer = Buffer::with_lines(vec!["aaaaa", "bbbbb", "ccccc"]);
|
||||
let mut buffer = Buffer::with_lines(["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(),])
|
||||
);
|
||||
#[rustfmt::skip]
|
||||
let expected = Buffer::with_lines([
|
||||
"aaaaa".into(),
|
||||
"bbbbb".red(),
|
||||
"ccccc".into(),
|
||||
]);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_style_does_not_panic_when_out_of_area() {
|
||||
let mut buffer = Buffer::with_lines(vec!["aaaaa", "bbbbb", "ccccc"]);
|
||||
let mut buffer = Buffer::with_lines(["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(),])
|
||||
);
|
||||
#[rustfmt::skip]
|
||||
let expected = Buffer::with_lines([
|
||||
"aaaaa".into(),
|
||||
"bbbbb".red(),
|
||||
"ccccc".red(),
|
||||
]);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_lines() {
|
||||
let buffer =
|
||||
Buffer::with_lines(vec!["┌────────┐", "│コンピュ│", "│ーa 上で│", "└────────┘"]);
|
||||
#[rustfmt::skip]
|
||||
let buffer = Buffer::with_lines([
|
||||
"┌────────┐",
|
||||
"│コンピュ│",
|
||||
"│ーa 上で│",
|
||||
"└────────┘",
|
||||
]);
|
||||
assert_eq!(buffer.area.x, 0);
|
||||
assert_eq!(buffer.area.y, 0);
|
||||
assert_eq!(buffer.area.width, 10);
|
||||
@@ -675,14 +772,14 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn diff_single_width() {
|
||||
let prev = Buffer::with_lines(vec![
|
||||
let prev = Buffer::with_lines([
|
||||
" ",
|
||||
"┌Title─┐ ",
|
||||
"│ │ ",
|
||||
"│ │ ",
|
||||
"└──────┘ ",
|
||||
]);
|
||||
let next = Buffer::with_lines(vec![
|
||||
let next = Buffer::with_lines([
|
||||
" ",
|
||||
"┌TITLE─┐ ",
|
||||
"│ │ ",
|
||||
@@ -704,11 +801,11 @@ mod tests {
|
||||
#[test]
|
||||
#[rustfmt::skip]
|
||||
fn diff_multi_width() {
|
||||
let prev = Buffer::with_lines(vec![
|
||||
let prev = Buffer::with_lines([
|
||||
"┌Title─┐ ",
|
||||
"└──────┘ ",
|
||||
]);
|
||||
let next = Buffer::with_lines(vec![
|
||||
let next = Buffer::with_lines([
|
||||
"┌称号──┐ ",
|
||||
"└──────┘ ",
|
||||
]);
|
||||
@@ -727,8 +824,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn diff_multi_width_offset() {
|
||||
let prev = Buffer::with_lines(vec!["┌称号──┐"]);
|
||||
let next = Buffer::with_lines(vec!["┌─称号─┐"]);
|
||||
let prev = Buffer::with_lines(["┌称号──┐"]);
|
||||
let next = Buffer::with_lines(["┌─称号─┐"]);
|
||||
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(
|
||||
@@ -739,8 +836,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn diff_skip() {
|
||||
let prev = Buffer::with_lines(vec!["123"]);
|
||||
let mut next = Buffer::with_lines(vec!["456"]);
|
||||
let prev = Buffer::with_lines(["123"]);
|
||||
let mut next = Buffer::with_lines(["456"]);
|
||||
for i in 1..3 {
|
||||
next.content[i].set_skip(true);
|
||||
}
|
||||
@@ -770,7 +867,7 @@ mod tests {
|
||||
Cell::default().set_symbol("2"),
|
||||
);
|
||||
one.merge(&two);
|
||||
assert_buffer_eq!(one, Buffer::with_lines(vec!["11", "11", "22", "22"]));
|
||||
assert_eq!(one, Buffer::with_lines(["11", "11", "22", "22"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -794,10 +891,7 @@ mod tests {
|
||||
Cell::default().set_symbol("2"),
|
||||
);
|
||||
one.merge(&two);
|
||||
assert_buffer_eq!(
|
||||
one,
|
||||
Buffer::with_lines(vec!["22 ", "22 ", " 11", " 11"])
|
||||
);
|
||||
assert_eq!(one, Buffer::with_lines(["22 ", "22 ", " 11", " 11"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -821,14 +915,14 @@ mod tests {
|
||||
Cell::default().set_symbol("2"),
|
||||
);
|
||||
one.merge(&two);
|
||||
let mut merged = Buffer::with_lines(vec!["222 ", "222 ", "2221", "2221"]);
|
||||
let mut merged = Buffer::with_lines(["222 ", "222 ", "2221", "2221"]);
|
||||
merged.area = Rect {
|
||||
x: 1,
|
||||
y: 1,
|
||||
width: 4,
|
||||
height: 4,
|
||||
};
|
||||
assert_buffer_eq!(one, merged);
|
||||
assert_eq!(one, merged);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -887,6 +981,6 @@ mod tests {
|
||||
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()]));
|
||||
assert_eq!(buf, Buffer::with_lines(["foo".red(), "bar".blue()]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use std::fmt::Debug;
|
||||
|
||||
use compact_str::CompactString;
|
||||
|
||||
use crate::prelude::*;
|
||||
@@ -42,26 +40,26 @@ impl Cell {
|
||||
}
|
||||
|
||||
/// Sets the symbol of the cell.
|
||||
pub fn set_symbol(&mut self, symbol: &str) -> &mut Cell {
|
||||
pub fn set_symbol(&mut self, symbol: &str) -> &mut Self {
|
||||
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 {
|
||||
pub fn set_char(&mut self, ch: char) -> &mut Self {
|
||||
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 {
|
||||
pub fn set_fg(&mut self, color: Color) -> &mut Self {
|
||||
self.fg = color;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the background color of the cell.
|
||||
pub fn set_bg(&mut self, color: Color) -> &mut Cell {
|
||||
pub fn set_bg(&mut self, color: Color) -> &mut Self {
|
||||
self.bg = color;
|
||||
self
|
||||
}
|
||||
@@ -70,7 +68,7 @@ impl 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 {
|
||||
pub fn set_style<S: Into<Style>>(&mut self, style: S) -> &mut Self {
|
||||
let style = style.into();
|
||||
if let Some(c) = style.fg {
|
||||
self.fg = c;
|
||||
@@ -107,14 +105,14 @@ impl Cell {
|
||||
///
|
||||
/// 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 {
|
||||
pub fn set_skip(&mut self, skip: bool) -> &mut Self {
|
||||
self.skip = skip;
|
||||
self
|
||||
}
|
||||
|
||||
/// Resets the cell to the default state.
|
||||
pub fn reset(&mut self) {
|
||||
self.symbol = CompactString::new(" ");
|
||||
self.symbol = CompactString::new_inline(" ");
|
||||
self.fg = Color::Reset;
|
||||
self.bg = Color::Reset;
|
||||
#[cfg(feature = "underline-color")]
|
||||
@@ -127,9 +125,9 @@ impl Cell {
|
||||
}
|
||||
|
||||
impl Default for Cell {
|
||||
fn default() -> Cell {
|
||||
Cell {
|
||||
symbol: CompactString::new(" "),
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
symbol: CompactString::new_inline(" "),
|
||||
fg: Color::Reset,
|
||||
bg: Color::Reset,
|
||||
#[cfg(feature = "underline-color")]
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![warn(clippy::missing_const_for_fn)]
|
||||
|
||||
mod alignment;
|
||||
mod constraint;
|
||||
mod corner;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::fmt::{self, Display};
|
||||
use std::fmt;
|
||||
|
||||
use itertools::Itertools;
|
||||
use strum::EnumIs;
|
||||
@@ -197,22 +197,21 @@ impl Constraint {
|
||||
)]
|
||||
pub fn apply(&self, length: u16) -> u16 {
|
||||
match *self {
|
||||
Constraint::Percentage(p) => {
|
||||
let p = p as f32 / 100.0;
|
||||
let length = length as f32;
|
||||
Self::Percentage(p) => {
|
||||
let p = f32::from(p) / 100.0;
|
||||
let length = f32::from(length);
|
||||
(p * length).min(length) as u16
|
||||
}
|
||||
Constraint::Ratio(numerator, denominator) => {
|
||||
Self::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;
|
||||
let length = f32::from(length);
|
||||
(percentage * length).min(length) as u16
|
||||
}
|
||||
Constraint::Length(l) => length.min(l),
|
||||
Constraint::Fill(l) => length.min(l),
|
||||
Constraint::Max(m) => length.min(m),
|
||||
Constraint::Min(m) => length.max(m),
|
||||
Self::Length(l) | Self::Fill(l) => length.min(l),
|
||||
Self::Max(m) => length.min(m),
|
||||
Self::Min(m) => length.max(m),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,11 +225,11 @@ impl Constraint {
|
||||
/// 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>
|
||||
pub fn from_lengths<T>(lengths: T) -> Vec<Self>
|
||||
where
|
||||
T: IntoIterator<Item = u16>,
|
||||
{
|
||||
lengths.into_iter().map(Constraint::Length).collect_vec()
|
||||
lengths.into_iter().map(Self::Length).collect_vec()
|
||||
}
|
||||
|
||||
/// Convert an iterator of ratios into a vector of constraints
|
||||
@@ -243,13 +242,13 @@ impl Constraint {
|
||||
/// 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>
|
||||
pub fn from_ratios<T>(ratios: T) -> Vec<Self>
|
||||
where
|
||||
T: IntoIterator<Item = (u32, u32)>,
|
||||
{
|
||||
ratios
|
||||
.into_iter()
|
||||
.map(|(n, d)| Constraint::Ratio(n, d))
|
||||
.map(|(n, d)| Self::Ratio(n, d))
|
||||
.collect_vec()
|
||||
}
|
||||
|
||||
@@ -263,14 +262,11 @@ impl Constraint {
|
||||
/// 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>
|
||||
pub fn from_percentages<T>(percentages: T) -> Vec<Self>
|
||||
where
|
||||
T: IntoIterator<Item = u16>,
|
||||
{
|
||||
percentages
|
||||
.into_iter()
|
||||
.map(Constraint::Percentage)
|
||||
.collect_vec()
|
||||
percentages.into_iter().map(Self::Percentage).collect_vec()
|
||||
}
|
||||
|
||||
/// Convert an iterator of maxes into a vector of constraints
|
||||
@@ -283,11 +279,11 @@ impl Constraint {
|
||||
/// 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>
|
||||
pub fn from_maxes<T>(maxes: T) -> Vec<Self>
|
||||
where
|
||||
T: IntoIterator<Item = u16>,
|
||||
{
|
||||
maxes.into_iter().map(Constraint::Max).collect_vec()
|
||||
maxes.into_iter().map(Self::Max).collect_vec()
|
||||
}
|
||||
|
||||
/// Convert an iterator of mins into a vector of constraints
|
||||
@@ -300,11 +296,11 @@ impl Constraint {
|
||||
/// 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>
|
||||
pub fn from_mins<T>(mins: T) -> Vec<Self>
|
||||
where
|
||||
T: IntoIterator<Item = u16>,
|
||||
{
|
||||
mins.into_iter().map(Constraint::Min).collect_vec()
|
||||
mins.into_iter().map(Self::Min).collect_vec()
|
||||
}
|
||||
|
||||
/// Convert an iterator of proportional factors into a vector of constraints
|
||||
@@ -317,22 +313,22 @@ impl Constraint {
|
||||
/// let constraints = Constraint::from_mins([1, 2, 3]);
|
||||
/// let layout = Layout::default().constraints(constraints).split(area);
|
||||
/// ```
|
||||
pub fn from_fills<T>(proportional_factors: T) -> Vec<Constraint>
|
||||
pub fn from_fills<T>(proportional_factors: T) -> Vec<Self>
|
||||
where
|
||||
T: IntoIterator<Item = u16>,
|
||||
{
|
||||
proportional_factors
|
||||
.into_iter()
|
||||
.map(Constraint::Fill)
|
||||
.map(Self::Fill)
|
||||
.collect_vec()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u16> for Constraint {
|
||||
/// Convert a u16 into a [Constraint::Length]
|
||||
/// 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.
|
||||
/// explicitly create a [`Constraint::Length`] yourself.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
@@ -343,38 +339,38 @@ impl From<u16> for Constraint {
|
||||
/// 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)
|
||||
fn from(length: u16) -> Self {
|
||||
Self::Length(length)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Constraint> for Constraint {
|
||||
fn from(constraint: &Constraint) -> Self {
|
||||
impl From<&Self> for Constraint {
|
||||
fn from(constraint: &Self) -> Self {
|
||||
*constraint
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<Constraint> for Constraint {
|
||||
fn as_ref(&self) -> &Constraint {
|
||||
impl AsRef<Self> for Constraint {
|
||||
fn as_ref(&self) -> &Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Constraint {
|
||||
fn default() -> Self {
|
||||
Constraint::Percentage(100)
|
||||
Self::Percentage(100)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Constraint {
|
||||
impl fmt::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::Fill(l) => write!(f, "Fill({})", l),
|
||||
Constraint::Max(m) => write!(f, "Max({})", m),
|
||||
Constraint::Min(m) => write!(f, "Min({})", m),
|
||||
Self::Percentage(p) => write!(f, "Percentage({p})"),
|
||||
Self::Ratio(n, d) => write!(f, "Ratio({n}, {d})"),
|
||||
Self::Length(l) => write!(f, "Length({l})"),
|
||||
Self::Fill(l) => write!(f, "Fill({l})"),
|
||||
Self::Max(m) => write!(f, "Max({m})"),
|
||||
Self::Min(m) => write!(f, "Min({m})"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ type Cache = LruCache<(Rect, Layout), (Segments, Spacers)>;
|
||||
const FLOAT_PRECISION_MULTIPLIER: f64 = 100.0;
|
||||
|
||||
thread_local! {
|
||||
static LAYOUT_CACHE: OnceLock<RefCell<Cache>> = OnceLock::new();
|
||||
static LAYOUT_CACHE: OnceLock<RefCell<Cache>> = const { OnceLock::new() };
|
||||
}
|
||||
|
||||
/// A layout is a set of constraints that can be applied to a given area to split it into smaller
|
||||
@@ -58,7 +58,7 @@ thread_local! {
|
||||
/// many of the constraints in order of their priorities.
|
||||
///
|
||||
/// When the layout is computed, the result is cached in a thread-local cache, so that subsequent
|
||||
/// calls with the same parameters are faster. The cache is a LruCache, and the size of the cache
|
||||
/// calls with the same parameters are faster. The cache is a `LruCache`, and the size of the cache
|
||||
/// can be configured using [`Layout::init_cache()`].
|
||||
///
|
||||
/// # Constructors
|
||||
@@ -127,13 +127,13 @@ impl Layout {
|
||||
///
|
||||
/// The `constraints` parameter accepts any type that implements `IntoIterator<Item =
|
||||
/// Into<Constraint>>`. This includes arrays, slices, vectors, iterators. `Into<Constraint>` is
|
||||
/// implemented on u16, so you can pass an array, vec, etc. of u16 to this function to create a
|
||||
/// layout with fixed size chunks.
|
||||
/// implemented on `u16`, so you can pass an array, `Vec`, etc. of `u16` to this function to
|
||||
/// create a layout with fixed size chunks.
|
||||
///
|
||||
/// Default values for the other fields are:
|
||||
///
|
||||
/// - `margin`: 0, 0
|
||||
/// - `flex`: Flex::Start
|
||||
/// - `flex`: [`Flex::Start`]
|
||||
/// - `spacing`: 0
|
||||
///
|
||||
/// # Examples
|
||||
@@ -152,15 +152,15 @@ impl Layout {
|
||||
///
|
||||
/// Layout::new(Direction::Horizontal, vec![1, 2]);
|
||||
/// ```
|
||||
pub fn new<I>(direction: Direction, constraints: I) -> Layout
|
||||
pub fn new<I>(direction: Direction, constraints: I) -> Self
|
||||
where
|
||||
I: IntoIterator,
|
||||
I::Item: Into<Constraint>,
|
||||
{
|
||||
Layout {
|
||||
Self {
|
||||
direction,
|
||||
constraints: constraints.into_iter().map(Into::into).collect(),
|
||||
..Layout::default()
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,12 +175,12 @@ impl Layout {
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let layout = Layout::vertical([Constraint::Length(5), Constraint::Min(0)]);
|
||||
/// ```
|
||||
pub fn vertical<I>(constraints: I) -> Layout
|
||||
pub fn vertical<I>(constraints: I) -> Self
|
||||
where
|
||||
I: IntoIterator,
|
||||
I::Item: Into<Constraint>,
|
||||
{
|
||||
Layout::new(Direction::Vertical, constraints.into_iter().map(Into::into))
|
||||
Self::new(Direction::Vertical, constraints.into_iter().map(Into::into))
|
||||
}
|
||||
|
||||
/// Creates a new horizontal layout with default values.
|
||||
@@ -194,19 +194,19 @@ impl Layout {
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let layout = Layout::horizontal([Constraint::Length(5), Constraint::Min(0)]);
|
||||
/// ```
|
||||
pub fn horizontal<I>(constraints: I) -> Layout
|
||||
pub fn horizontal<I>(constraints: I) -> Self
|
||||
where
|
||||
I: IntoIterator,
|
||||
I::Item: Into<Constraint>,
|
||||
{
|
||||
Layout::new(
|
||||
Self::new(
|
||||
Direction::Horizontal,
|
||||
constraints.into_iter().map(Into::into),
|
||||
)
|
||||
}
|
||||
|
||||
/// Initialize an empty cache with a custom size. The cache is keyed on the layout and area, so
|
||||
/// that subsequent calls with the same parameters are faster. The cache is a LruCache, and
|
||||
/// that subsequent calls with the same parameters are faster. The cache is a `LruCache`, and
|
||||
/// grows until `cache_size` is reached.
|
||||
///
|
||||
/// Returns true if the cell's value was set by this call.
|
||||
@@ -214,7 +214,7 @@ impl Layout {
|
||||
/// has set this value or that the cache size is already initialized.
|
||||
///
|
||||
/// Note that a custom cache size will be set only if this function:
|
||||
/// * is called before [Layout::split()] otherwise, the cache size is
|
||||
/// * is called before [`Layout::split()`] otherwise, the cache size is
|
||||
/// [`Self::DEFAULT_CACHE_SIZE`].
|
||||
/// * is called for the first time, subsequent calls do not modify the cache size.
|
||||
pub fn init_cache(cache_size: usize) -> bool {
|
||||
@@ -246,7 +246,7 @@ impl Layout {
|
||||
/// assert_eq!(layout[..], [Rect::new(0, 0, 10, 5), Rect::new(0, 5, 10, 5)]);
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub const fn direction(mut self, direction: Direction) -> Layout {
|
||||
pub const fn direction(mut self, direction: Direction) -> Self {
|
||||
self.direction = direction;
|
||||
self
|
||||
}
|
||||
@@ -296,7 +296,7 @@ impl Layout {
|
||||
/// Layout::default().constraints(vec![1, 2, 3]);
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn constraints<I>(mut self, constraints: I) -> Layout
|
||||
pub fn constraints<I>(mut self, constraints: I) -> Self
|
||||
where
|
||||
I: IntoIterator,
|
||||
I::Item: Into<Constraint>,
|
||||
@@ -318,7 +318,7 @@ impl Layout {
|
||||
/// assert_eq!(layout[..], [Rect::new(2, 2, 6, 6)]);
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub const fn margin(mut self, margin: u16) -> Layout {
|
||||
pub const fn margin(mut self, margin: u16) -> Self {
|
||||
self.margin = Margin {
|
||||
horizontal: margin,
|
||||
vertical: margin,
|
||||
@@ -339,7 +339,7 @@ impl Layout {
|
||||
/// assert_eq!(layout[..], [Rect::new(2, 0, 6, 10)]);
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub const fn horizontal_margin(mut self, horizontal: u16) -> Layout {
|
||||
pub const fn horizontal_margin(mut self, horizontal: u16) -> Self {
|
||||
self.margin.horizontal = horizontal;
|
||||
self
|
||||
}
|
||||
@@ -357,7 +357,7 @@ impl Layout {
|
||||
/// assert_eq!(layout[..], [Rect::new(0, 2, 10, 6)]);
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub const fn vertical_margin(mut self, vertical: u16) -> Layout {
|
||||
pub const fn vertical_margin(mut self, vertical: u16) -> Self {
|
||||
self.margin.vertical = vertical;
|
||||
self
|
||||
}
|
||||
@@ -366,8 +366,8 @@ impl Layout {
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `flex`: A `Flex` enum value that represents the flex behavior of the layout. It can be one
|
||||
/// of the following:
|
||||
/// * `flex`: A [`Flex`] enum value that represents the flex behavior of the layout. It can be
|
||||
/// one of the following:
|
||||
/// - [`Flex::Legacy`]: The last item is stretched to fill the excess space.
|
||||
/// - [`Flex::Start`]: The items are aligned to the start of the layout.
|
||||
/// - [`Flex::Center`]: The items are aligned to the center of the layout.
|
||||
@@ -391,7 +391,8 @@ impl Layout {
|
||||
/// # use ratatui::layout::{Flex, Layout, Constraint::*};
|
||||
/// let layout = Layout::horizontal([Length(20), Length(20), Length(20)]).flex(Flex::Legacy);
|
||||
/// ```
|
||||
pub const fn flex(mut self, flex: Flex) -> Layout {
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub const fn flex(mut self, flex: Flex) -> Self {
|
||||
self.flex = flex;
|
||||
self
|
||||
}
|
||||
@@ -414,13 +415,14 @@ impl Layout {
|
||||
/// # Notes
|
||||
///
|
||||
/// - If the layout has only one item, the spacing will not be applied.
|
||||
/// - Spacing will not be applied for `Flex::SpaceAround` and `Flex::SpaceBetween`
|
||||
pub const fn spacing(mut self, spacing: u16) -> Layout {
|
||||
/// - Spacing will not be applied for [`Flex::SpaceAround`] and [`Flex::SpaceBetween`]
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub const fn spacing(mut self, spacing: u16) -> Self {
|
||||
self.spacing = spacing;
|
||||
self
|
||||
}
|
||||
|
||||
/// Split the rect into a number of sub-rects according to the given [`Layout`]`.
|
||||
/// 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]>`.
|
||||
@@ -446,10 +448,10 @@ impl Layout {
|
||||
/// # }
|
||||
pub fn areas<const N: usize>(&self, area: Rect) -> [Rect; N] {
|
||||
let (areas, _) = self.split_with_spacers(area);
|
||||
areas.to_vec().try_into().expect("invalid number of rects")
|
||||
areas.as_ref().try_into().expect("invalid number of rects")
|
||||
}
|
||||
|
||||
/// Split the rect into a number of sub-rects according to the given [`Layout`]` and return just
|
||||
/// Split the rect into a number of sub-rects according to the given [`Layout`] and return just
|
||||
/// the spacers between the areas.
|
||||
///
|
||||
/// This method requires the number of constraints to be known at compile time. If you don't
|
||||
@@ -480,7 +482,7 @@ impl Layout {
|
||||
pub fn spacers<const N: usize>(&self, area: Rect) -> [Rect; N] {
|
||||
let (_, spacers) = self.split_with_spacers(area);
|
||||
spacers
|
||||
.to_vec()
|
||||
.as_ref()
|
||||
.try_into()
|
||||
.expect("invalid number of rects")
|
||||
}
|
||||
@@ -495,8 +497,8 @@ impl Layout {
|
||||
///
|
||||
/// This method stores the result of the computation in a thread-local cache keyed on the layout
|
||||
/// and area, so that subsequent calls with the same parameters are faster. The cache is a
|
||||
/// LruCache, and grows until [`Self::DEFAULT_CACHE_SIZE`] is reached by default, if the cache
|
||||
/// is initialized with the [Layout::init_cache()] grows until the initialized cache size.
|
||||
/// `LruCache`, and grows until [`Self::DEFAULT_CACHE_SIZE`] is reached by default, if the cache
|
||||
/// is initialized with the [`Layout::init_cache()`] grows until the initialized cache size.
|
||||
///
|
||||
/// There is a helper method that can be used to split the whole area into smaller ones based on
|
||||
/// the layout: [`Layout::areas()`]. That method is a shortcut for calling this method. It
|
||||
@@ -532,8 +534,8 @@ impl Layout {
|
||||
///
|
||||
/// This method stores the result of the computation in a thread-local cache keyed on the layout
|
||||
/// and area, so that subsequent calls with the same parameters are faster. The cache is a
|
||||
/// LruCache, and grows until [`Self::DEFAULT_CACHE_SIZE`] is reached by default, if the cache
|
||||
/// is initialized with the [Layout::init_cache()] grows until the initialized cache size.
|
||||
/// `LruCache`, and grows until [`Self::DEFAULT_CACHE_SIZE`] is reached by default, if the cache
|
||||
/// is initialized with the [`Layout::init_cache()`] grows until the initialized cache size.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
@@ -698,7 +700,7 @@ fn configure_variable_constraints(
|
||||
area: Element,
|
||||
) -> Result<(), AddConstraintError> {
|
||||
// all variables are in the range [area.start, area.end]
|
||||
for &variable in variables.iter() {
|
||||
for &variable in variables {
|
||||
solver.add_constraint(variable | GE(REQUIRED) | area.start)?;
|
||||
solver.add_constraint(variable | LE(REQUIRED) | area.end)?;
|
||||
}
|
||||
@@ -733,7 +735,7 @@ fn configure_constraints(
|
||||
}
|
||||
}
|
||||
Constraint::Length(length) => {
|
||||
solver.add_constraint(element.has_int_size(length, LENGTH_SIZE_EQ))?
|
||||
solver.add_constraint(element.has_int_size(length, LENGTH_SIZE_EQ))?;
|
||||
}
|
||||
Constraint::Percentage(p) => {
|
||||
let size = area.size() * f64::from(p) / 100.00;
|
||||
@@ -764,7 +766,7 @@ fn configure_flex_constraints(
|
||||
let spacing_f64 = f64::from(spacing) * FLOAT_PRECISION_MULTIPLIER;
|
||||
match flex {
|
||||
Flex::Legacy => {
|
||||
for spacer in spacers_except_first_and_last.iter() {
|
||||
for spacer in spacers_except_first_and_last {
|
||||
solver.add_constraint(spacer.has_size(spacing_f64, SPACER_SIZE_EQ))?;
|
||||
}
|
||||
if let (Some(first), Some(last)) = (spacers.first(), spacers.last()) {
|
||||
@@ -776,9 +778,9 @@ fn configure_flex_constraints(
|
||||
// constraints are satisfied
|
||||
Flex::SpaceAround => {
|
||||
for (left, right) in spacers.iter().tuple_combinations() {
|
||||
solver.add_constraint(left.has_size(right, SPACER_SIZE_EQ))?
|
||||
solver.add_constraint(left.has_size(right, SPACER_SIZE_EQ))?;
|
||||
}
|
||||
for spacer in spacers.iter() {
|
||||
for spacer in spacers {
|
||||
solver.add_constraint(spacer.has_min_size(spacing, SPACER_SIZE_EQ))?;
|
||||
solver.add_constraint(spacer.has_size(area, SPACE_GROW))?;
|
||||
}
|
||||
@@ -788,12 +790,12 @@ fn configure_flex_constraints(
|
||||
// constraints are satisfied, but the first and last spacers are zero size
|
||||
Flex::SpaceBetween => {
|
||||
for (left, right) in spacers_except_first_and_last.iter().tuple_combinations() {
|
||||
solver.add_constraint(left.has_size(right.size(), SPACER_SIZE_EQ))?
|
||||
solver.add_constraint(left.has_size(right.size(), SPACER_SIZE_EQ))?;
|
||||
}
|
||||
for spacer in spacers_except_first_and_last.iter() {
|
||||
for spacer in spacers_except_first_and_last {
|
||||
solver.add_constraint(spacer.has_min_size(spacing, SPACER_SIZE_EQ))?;
|
||||
}
|
||||
for spacer in spacers_except_first_and_last.iter() {
|
||||
for spacer in spacers_except_first_and_last {
|
||||
solver.add_constraint(spacer.has_size(area, SPACE_GROW))?;
|
||||
}
|
||||
if let (Some(first), Some(last)) = (spacers.first(), spacers.last()) {
|
||||
@@ -846,7 +848,7 @@ fn configure_flex_constraints(
|
||||
/// │abcdef││abcdefabcdef│
|
||||
/// └──────┘└────────────┘
|
||||
///
|
||||
/// size == base_element * scaling_factor
|
||||
/// `size == base_element * scaling_factor`
|
||||
fn configure_fill_constraints(
|
||||
solver: &mut Solver,
|
||||
segments: &[Element],
|
||||
@@ -1075,8 +1077,6 @@ mod strengths {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::iter;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
@@ -1104,7 +1104,7 @@ mod tests {
|
||||
assert!(!Layout::init_cache(15));
|
||||
LAYOUT_CACHE.with(|c| {
|
||||
assert_eq!(c.get().unwrap().borrow().cap().get(), 10);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1130,7 +1130,7 @@ mod tests {
|
||||
c.get().unwrap().borrow().cap().get(),
|
||||
Layout::DEFAULT_CACHE_SIZE
|
||||
);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1207,7 +1207,7 @@ mod tests {
|
||||
}
|
||||
|
||||
/// The purpose of this test is to ensure that layout can be constructed with any type that
|
||||
/// implements IntoIterator<Item = AsRef<Constraint>>.
|
||||
/// implements `IntoIterator<Item = AsRef<Constraint>>`.
|
||||
#[test]
|
||||
#[allow(
|
||||
clippy::needless_borrow,
|
||||
@@ -1253,14 +1253,14 @@ mod tests {
|
||||
"constraints should be settable with an iter"
|
||||
);
|
||||
|
||||
let iterator = CONSTRAINTS.iter().map(|c| c.to_owned());
|
||||
let iterator = CONSTRAINTS.iter().map(ToOwned::to_owned);
|
||||
assert_eq!(
|
||||
Layout::default().constraints(iterator).constraints,
|
||||
CONSTRAINTS,
|
||||
"constraints should be settable with an iterator"
|
||||
);
|
||||
|
||||
let iterator_ref = CONSTRAINTS.iter().map(|c| c.as_ref());
|
||||
let iterator_ref = CONSTRAINTS.iter().map(AsRef::as_ref);
|
||||
assert_eq!(
|
||||
Layout::default().constraints(iterator_ref).constraints,
|
||||
CONSTRAINTS,
|
||||
@@ -1334,7 +1334,6 @@ mod tests {
|
||||
use rstest::rstest;
|
||||
|
||||
use crate::{
|
||||
assert_buffer_eq,
|
||||
layout::flex::Flex,
|
||||
prelude::{Constraint::*, *},
|
||||
widgets::Paragraph,
|
||||
@@ -1361,8 +1360,7 @@ mod tests {
|
||||
let s = c.to_string().repeat(area.width as usize);
|
||||
Paragraph::new(s).render(layout[i], &mut buffer);
|
||||
}
|
||||
let expected = Buffer::with_lines(vec![expected]);
|
||||
assert_buffer_eq!(buffer, expected);
|
||||
assert_eq!(buffer, Buffer::with_lines([expected]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
@@ -1406,7 +1404,7 @@ mod tests {
|
||||
#[case] constraints: &[Constraint],
|
||||
#[case] expected: &str,
|
||||
) {
|
||||
letters(flex, constraints, width, expected)
|
||||
letters(flex, constraints, width, expected);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
@@ -1449,7 +1447,7 @@ mod tests {
|
||||
#[case] constraints: &[Constraint],
|
||||
#[case] expected: &str,
|
||||
) {
|
||||
letters(flex, constraints, width, expected)
|
||||
letters(flex, constraints, width, expected);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
@@ -1485,7 +1483,7 @@ mod tests {
|
||||
#[case] constraints: &[Constraint],
|
||||
#[case] expected: &str,
|
||||
) {
|
||||
letters(flex, constraints, width, expected)
|
||||
letters(flex, constraints, width, expected);
|
||||
}
|
||||
|
||||
#[rstest] // flex, width, lengths, expected
|
||||
@@ -1618,7 +1616,7 @@ mod tests {
|
||||
#[case] constraints: &[Constraint],
|
||||
#[case] expected: &str,
|
||||
) {
|
||||
letters(flex, constraints, width, expected)
|
||||
letters(flex, constraints, width, expected);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
@@ -1655,7 +1653,7 @@ mod tests {
|
||||
#[case] constraints: &[Constraint],
|
||||
#[case] expected: &str,
|
||||
) {
|
||||
letters(flex, constraints, width, expected)
|
||||
letters(flex, constraints, width, expected);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
@@ -1692,7 +1690,7 @@ mod tests {
|
||||
#[case] constraints: &[Constraint],
|
||||
#[case] expected: &str,
|
||||
) {
|
||||
letters(flex, constraints, width, expected)
|
||||
letters(flex, constraints, width, expected);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
@@ -1795,7 +1793,7 @@ mod tests {
|
||||
#[case] constraints: &[Constraint],
|
||||
#[case] expected: &str,
|
||||
) {
|
||||
letters(flex, constraints, width, expected)
|
||||
letters(flex, constraints, width, expected);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
@@ -1832,7 +1830,7 @@ mod tests {
|
||||
#[case] constraints: &[Constraint],
|
||||
#[case] expected: &str,
|
||||
) {
|
||||
letters(flex, constraints, width, expected)
|
||||
letters(flex, constraints, width, expected);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
@@ -1869,7 +1867,7 @@ mod tests {
|
||||
#[case] constraints: &[Constraint],
|
||||
#[case] expected: &str,
|
||||
) {
|
||||
letters(flex, constraints, width, expected)
|
||||
letters(flex, constraints, width, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1984,7 +1982,6 @@ mod tests {
|
||||
.flex(Flex::Legacy)
|
||||
.split(rect)
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|r| r.width)
|
||||
.collect::<Vec<u16>>();
|
||||
assert_eq!(expected, r);
|
||||
@@ -2004,7 +2001,6 @@ mod tests {
|
||||
.flex(Flex::Start)
|
||||
.split(rect)
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|r| (r.x, r.width))
|
||||
.collect::<Vec<(u16, u16)>>();
|
||||
assert_eq!(expected, r);
|
||||
@@ -2036,7 +2032,6 @@ mod tests {
|
||||
.flex(Flex::Legacy)
|
||||
.split(rect)
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|r| r.width)
|
||||
.collect::<Vec<u16>>();
|
||||
assert_eq!(expected, r);
|
||||
@@ -2067,7 +2062,6 @@ mod tests {
|
||||
.flex(Flex::Start)
|
||||
.split(rect)
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|r| r.width)
|
||||
.collect::<Vec<u16>>();
|
||||
assert_eq!(expected, r);
|
||||
@@ -2077,7 +2071,6 @@ mod tests {
|
||||
.flex(Flex::Center)
|
||||
.split(rect)
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|r| r.width)
|
||||
.collect::<Vec<u16>>();
|
||||
assert_eq!(expected, r);
|
||||
@@ -2087,7 +2080,6 @@ mod tests {
|
||||
.flex(Flex::End)
|
||||
.split(rect)
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|r| r.width)
|
||||
.collect::<Vec<u16>>();
|
||||
assert_eq!(expected, r);
|
||||
@@ -2097,7 +2089,6 @@ mod tests {
|
||||
.flex(Flex::SpaceAround)
|
||||
.split(rect)
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|r| r.width)
|
||||
.collect::<Vec<u16>>();
|
||||
assert_eq!(expected, r);
|
||||
@@ -2107,7 +2098,6 @@ mod tests {
|
||||
.flex(Flex::SpaceBetween)
|
||||
.split(rect)
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|r| r.width)
|
||||
.collect::<Vec<u16>>();
|
||||
assert_eq!(expected, r);
|
||||
@@ -2122,7 +2112,6 @@ mod tests {
|
||||
.flex(Flex::Legacy)
|
||||
.split(rect)
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|r| r.width)
|
||||
.collect::<Vec<u16>>();
|
||||
assert_eq!(expected, r);
|
||||
@@ -2170,7 +2159,6 @@ mod tests {
|
||||
.flex(Flex::Legacy)
|
||||
.split(rect)
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|r| r.width)
|
||||
.collect::<Vec<u16>>();
|
||||
assert_eq!(expected, r);
|
||||
@@ -2188,7 +2176,6 @@ mod tests {
|
||||
.flex(Flex::Legacy)
|
||||
.split(rect)
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|r| r.width)
|
||||
.collect::<Vec<u16>>();
|
||||
assert_eq!(expected, r);
|
||||
@@ -2207,7 +2194,6 @@ mod tests {
|
||||
.flex(Flex::Legacy)
|
||||
.split(rect)
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|r| r.width)
|
||||
.collect::<Vec<u16>>();
|
||||
assert_eq!(expected, r);
|
||||
@@ -2272,7 +2258,6 @@ mod tests {
|
||||
.flex(flex)
|
||||
.split(rect)
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|r| (r.x, r.width))
|
||||
.collect::<Vec<(u16, u16)>>();
|
||||
assert_eq!(expected, r);
|
||||
@@ -2331,7 +2316,6 @@ mod tests {
|
||||
.flex(Flex::Legacy)
|
||||
.split(rect)
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|r| (r.x, r.width))
|
||||
.collect::<Vec<(u16, u16)>>();
|
||||
assert_eq!(expected, r);
|
||||
@@ -2356,7 +2340,6 @@ mod tests {
|
||||
.flex(flex)
|
||||
.split(rect)
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|r| (r.x, r.width))
|
||||
.collect::<Vec<(u16, u16)>>();
|
||||
assert_eq!(expected, r);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::fmt::{self, Display};
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub struct Margin {
|
||||
@@ -7,15 +7,15 @@ pub struct Margin {
|
||||
}
|
||||
|
||||
impl Margin {
|
||||
pub const fn new(horizontal: u16, vertical: u16) -> Margin {
|
||||
Margin {
|
||||
pub const fn new(horizontal: u16, vertical: u16) -> Self {
|
||||
Self {
|
||||
horizontal,
|
||||
vertical,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Margin {
|
||||
impl fmt::Display for Margin {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}x{}", self.horizontal, self.vertical)
|
||||
}
|
||||
|
||||
@@ -38,14 +38,14 @@ pub struct Position {
|
||||
|
||||
impl Position {
|
||||
/// Create a new position
|
||||
pub fn new(x: u16, y: u16) -> Self {
|
||||
Position { x, y }
|
||||
pub const fn new(x: u16, y: u16) -> Self {
|
||||
Self { x, y }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(u16, u16)> for Position {
|
||||
fn from((x, y): (u16, u16)) -> Self {
|
||||
Position { x, y }
|
||||
Self { x, y }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,78 +4,40 @@ use std::{
|
||||
fmt,
|
||||
};
|
||||
|
||||
use layout::{Position, Size};
|
||||
|
||||
use super::{Position, Size};
|
||||
use crate::prelude::*;
|
||||
|
||||
mod offset;
|
||||
pub use offset::*;
|
||||
mod iter;
|
||||
pub use iter::*;
|
||||
|
||||
/// A Rectangular area.
|
||||
///
|
||||
/// 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)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Rect {
|
||||
/// The x coordinate of the top left corner of the rect.
|
||||
/// The x coordinate of the top left corner of the `Rect`.
|
||||
pub x: u16,
|
||||
/// The y coordinate of the top left corner of the rect.
|
||||
/// The y coordinate of the top left corner of the `Rect`.
|
||||
pub y: u16,
|
||||
/// The width of the rect.
|
||||
/// The width of the `Rect`.
|
||||
pub width: u16,
|
||||
/// The height of the rect.
|
||||
/// The height of the `Rect`.
|
||||
pub height: u16,
|
||||
}
|
||||
|
||||
/// Manages row divisions within a `Rect`.
|
||||
/// Amounts by which to move a [`Rect`](super::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`.
|
||||
/// Positive numbers move to the right/bottom and negative to the left/top.
|
||||
///
|
||||
/// 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)
|
||||
}
|
||||
/// See [`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,
|
||||
}
|
||||
|
||||
impl fmt::Display for Rect {
|
||||
@@ -85,9 +47,17 @@ impl fmt::Display for Rect {
|
||||
}
|
||||
|
||||
impl Rect {
|
||||
/// Creates a new rect, with width and height limited to keep the area under max u16. If
|
||||
/// A zero sized Rect at position 0,0
|
||||
pub const ZERO: Self = Self {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
};
|
||||
|
||||
/// Creates a new `Rect`, with width and height limited to keep the area under max `u16`. If
|
||||
/// clipped, aspect ratio will be preserved.
|
||||
pub fn new(x: u16, y: u16, width: u16, height: u16) -> Rect {
|
||||
pub fn new(x: u16, y: u16, width: u16, height: u16) -> Self {
|
||||
let max_area = u16::max_value();
|
||||
let (clipped_width, clipped_height) =
|
||||
if u32::from(width) * u32::from(height) > u32::from(max_area) {
|
||||
@@ -99,7 +69,7 @@ impl Rect {
|
||||
} else {
|
||||
(width, height)
|
||||
};
|
||||
Rect {
|
||||
Self {
|
||||
x,
|
||||
y,
|
||||
width: clipped_width,
|
||||
@@ -107,54 +77,58 @@ impl Rect {
|
||||
}
|
||||
}
|
||||
|
||||
/// The area of the rect. If the area is larger than the maximum value of u16, it will be
|
||||
/// clamped to u16::MAX.
|
||||
/// The area of the `Rect`. If the area is larger than the maximum value of `u16`, it will be
|
||||
/// clamped to `u16::MAX`.
|
||||
pub const fn area(self) -> u16 {
|
||||
self.width.saturating_mul(self.height)
|
||||
}
|
||||
|
||||
/// Returns true if the rect has no area.
|
||||
/// Returns true if the `Rect` has no area.
|
||||
pub const fn is_empty(self) -> bool {
|
||||
self.width == 0 || self.height == 0
|
||||
}
|
||||
|
||||
/// Returns the left coordinate of the rect.
|
||||
/// Returns the left coordinate of the `Rect`.
|
||||
pub const fn left(self) -> u16 {
|
||||
self.x
|
||||
}
|
||||
|
||||
/// Returns the right coordinate of the rect. This is the first coordinate outside of the rect.
|
||||
/// Returns the right coordinate of the `Rect`. This is the first coordinate outside of the
|
||||
/// `Rect`.
|
||||
///
|
||||
/// If the right coordinate is larger than the maximum value of u16, it will be clamped to
|
||||
/// u16::MAX.
|
||||
/// `u16::MAX`.
|
||||
pub const fn right(self) -> u16 {
|
||||
self.x.saturating_add(self.width)
|
||||
}
|
||||
|
||||
/// Returns the top coordinate of the rect.
|
||||
/// Returns the top coordinate of the `Rect`.
|
||||
pub const fn top(self) -> u16 {
|
||||
self.y
|
||||
}
|
||||
|
||||
/// Returns the bottom coordinate of the rect. This is the first coordinate outside of the rect.
|
||||
/// Returns the bottom coordinate of the `Rect`. This is the first coordinate outside of the
|
||||
/// `Rect`.
|
||||
///
|
||||
/// If the bottom coordinate is larger than the maximum value of u16, it will be clamped to
|
||||
/// u16::MAX.
|
||||
/// `u16::MAX`.
|
||||
pub const fn bottom(self) -> u16 {
|
||||
self.y.saturating_add(self.height)
|
||||
}
|
||||
|
||||
/// Returns a new rect inside the current one, with the given margin on each side.
|
||||
/// Returns a new `Rect` inside the current one, with the given margin on each side.
|
||||
///
|
||||
/// If the margin is larger than the rect, the returned rect will have no area.
|
||||
pub fn inner(self, margin: &Margin) -> Rect {
|
||||
/// If the margin is larger than the `Rect`, the returned `Rect` will have no area.
|
||||
#[allow(clippy::trivially_copy_pass_by_ref)] // See PR #1008
|
||||
#[must_use = "method returns the modified value"]
|
||||
pub const fn inner(self, margin: &Margin) -> Self {
|
||||
let doubled_margin_horizontal = margin.horizontal.saturating_mul(2);
|
||||
let doubled_margin_vertical = margin.vertical.saturating_mul(2);
|
||||
|
||||
if self.width < doubled_margin_horizontal || self.height < doubled_margin_vertical {
|
||||
Rect::default()
|
||||
Self::ZERO
|
||||
} else {
|
||||
Rect {
|
||||
Self {
|
||||
x: self.x.saturating_add(margin.horizontal),
|
||||
y: self.y.saturating_add(margin.vertical),
|
||||
width: self.width.saturating_sub(doubled_margin_horizontal),
|
||||
@@ -171,25 +145,27 @@ impl Rect {
|
||||
/// - 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 {
|
||||
#[must_use = "method returns the modified value"]
|
||||
pub fn offset(self, offset: Offset) -> Self {
|
||||
Self {
|
||||
x: i32::from(self.x)
|
||||
.saturating_add(offset.x)
|
||||
.clamp(0, (u16::MAX - self.width) as i32) as u16,
|
||||
.clamp(0, i32::from(u16::MAX - self.width)) as u16,
|
||||
y: i32::from(self.y)
|
||||
.saturating_add(offset.y)
|
||||
.clamp(0, (u16::MAX - self.height) as i32) as u16,
|
||||
.clamp(0, i32::from(u16::MAX - self.height)) as u16,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a new rect that contains both the current one and the given one.
|
||||
pub fn union(self, other: Rect) -> Rect {
|
||||
/// Returns a new `Rect` that contains both the current one and the given one.
|
||||
#[must_use = "method returns the modified value"]
|
||||
pub fn union(self, other: Self) -> Self {
|
||||
let x1 = min(self.x, other.x);
|
||||
let y1 = min(self.y, other.y);
|
||||
let x2 = max(self.right(), other.right());
|
||||
let y2 = max(self.bottom(), other.bottom());
|
||||
Rect {
|
||||
Self {
|
||||
x: x1,
|
||||
y: y1,
|
||||
width: x2.saturating_sub(x1),
|
||||
@@ -197,15 +173,16 @@ impl Rect {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a new rect that is the intersection of the current one and the given one.
|
||||
/// Returns a new `Rect` that is the intersection of the current one and the given one.
|
||||
///
|
||||
/// If the two rects do not intersect, the returned rect will have no area.
|
||||
pub fn intersection(self, other: Rect) -> Rect {
|
||||
/// If the two `Rect`s do not intersect, the returned `Rect` will have no area.
|
||||
#[must_use = "method returns the modified value"]
|
||||
pub fn intersection(self, other: Self) -> Self {
|
||||
let x1 = max(self.x, other.x);
|
||||
let y1 = max(self.y, other.y);
|
||||
let x2 = min(self.right(), other.right());
|
||||
let y2 = min(self.bottom(), other.bottom());
|
||||
Rect {
|
||||
Self {
|
||||
x: x1,
|
||||
y: y1,
|
||||
width: x2.saturating_sub(x1),
|
||||
@@ -213,17 +190,17 @@ impl Rect {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the two rects intersect.
|
||||
pub const fn intersects(self, other: Rect) -> bool {
|
||||
/// Returns true if the two `Rect`s intersect.
|
||||
pub const fn intersects(self, other: Self) -> bool {
|
||||
self.x < other.right()
|
||||
&& self.right() > other.x
|
||||
&& self.y < other.bottom()
|
||||
&& self.bottom() > other.y
|
||||
}
|
||||
|
||||
/// Returns true if the given position is inside the rect.
|
||||
/// Returns true if the given position is inside the `Rect`.
|
||||
///
|
||||
/// The position is considered inside the rect if it is on the rect's border.
|
||||
/// The position is considered inside the `Rect` if it is on the `Rect`'s border.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
@@ -239,20 +216,20 @@ impl Rect {
|
||||
&& position.y < self.bottom()
|
||||
}
|
||||
|
||||
/// Clamp this rect to fit inside the other rect.
|
||||
/// 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 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 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.
|
||||
/// 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.
|
||||
/// 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
|
||||
///
|
||||
@@ -263,58 +240,66 @@ impl Rect {
|
||||
/// let rect = Rect::new(0, 0, 100, 100).clamp(area);
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn clamp(self, other: Rect) -> Rect {
|
||||
#[must_use = "method returns the modified value"]
|
||||
pub fn clamp(self, other: Self) -> Self {
|
||||
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)
|
||||
Self::new(x, y, width, height)
|
||||
}
|
||||
|
||||
/// Creates an iterator over rows within the `Rect`.
|
||||
/// An iterator over rows within the `Rect`.
|
||||
///
|
||||
/// This method returns a `Rows` iterator that allows iterating through rows of the `Rect`.
|
||||
///
|
||||
/// # Examples
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// 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);
|
||||
/// # use ratatui::prelude::*;
|
||||
/// fn render(area: Rect, buf: &mut Buffer) {
|
||||
/// for row in area.rows() {
|
||||
/// Line::raw("Hello, world!").render(row, buf);
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub fn rows(&self) -> Rows {
|
||||
Rows {
|
||||
rect: *self,
|
||||
current_row: self.y,
|
||||
}
|
||||
pub const fn rows(self) -> Rows {
|
||||
Rows::new(self)
|
||||
}
|
||||
|
||||
/// Creates an iterator over columns within the `Rect`.
|
||||
/// An iterator over columns within the `Rect`.
|
||||
///
|
||||
/// This method returns a `Columns` iterator that allows iterating through columns of the
|
||||
/// `Rect`.
|
||||
///
|
||||
/// # Examples
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// 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);
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// fn render(area: Rect, buf: &mut Buffer) {
|
||||
/// if let Some(left) = area.columns().next() {
|
||||
/// Block::new().borders(Borders::LEFT).render(left, buf);
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub fn columns(&self) -> Columns {
|
||||
Columns {
|
||||
rect: *self,
|
||||
current_column: self.x,
|
||||
}
|
||||
pub const fn columns(self) -> Columns {
|
||||
Columns::new(self)
|
||||
}
|
||||
|
||||
/// Returns a [`Position`] with the same coordinates as this rect.
|
||||
/// An iterator over the positions within the `Rect`.
|
||||
///
|
||||
/// The positions are returned in a row-major order (left-to-right, top-to-bottom).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::prelude::*;
|
||||
/// fn render(area: Rect, buf: &mut Buffer) {
|
||||
/// for position in area.positions() {
|
||||
/// buf.get_mut(position.x, position.y).set_symbol("x");
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub const fn positions(self) -> Positions {
|
||||
Positions::new(self)
|
||||
}
|
||||
|
||||
/// Returns a [`Position`] with the same coordinates as this `Rect`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
@@ -323,25 +308,37 @@ impl Rect {
|
||||
/// let rect = Rect::new(1, 2, 3, 4);
|
||||
/// let position = rect.as_position();
|
||||
/// ````
|
||||
pub fn as_position(self) -> Position {
|
||||
pub const 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 {
|
||||
/// Converts the `Rect` into a size struct.
|
||||
pub const fn as_size(self) -> Size {
|
||||
Size {
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
}
|
||||
}
|
||||
|
||||
/// indents the x value of the `Rect` by a given `offset`
|
||||
///
|
||||
/// This is pub(crate) for now as we need to stabilize the naming / design of this API.
|
||||
#[must_use]
|
||||
pub(crate) const fn indent_x(self, offset: u16) -> Self {
|
||||
Self {
|
||||
x: self.x.saturating_add(offset),
|
||||
width: self.width.saturating_sub(offset),
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(Position, Size)> for Rect {
|
||||
fn from((position, size): (Position, Size)) -> Self {
|
||||
Rect {
|
||||
Self {
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
width: size.width,
|
||||
|
||||
144
src/layout/rect/iter.rs
Normal file
144
src/layout/rect/iter.rs
Normal file
@@ -0,0 +1,144 @@
|
||||
use self::layout::Position;
|
||||
use crate::prelude::*;
|
||||
|
||||
/// An iterator over rows within a `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 Rows {
|
||||
/// Creates a new `Rows` iterator.
|
||||
pub const fn new(rect: Rect) -> Self {
|
||||
Self {
|
||||
rect,
|
||||
current_row: rect.y,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
/// An iterator over columns within a `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 Columns {
|
||||
/// Creates a new `Columns` iterator.
|
||||
pub const fn new(rect: Rect) -> Self {
|
||||
Self {
|
||||
rect,
|
||||
current_column: rect.x,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
/// An iterator over positions within a `Rect`.
|
||||
///
|
||||
/// The iterator will yield all positions within the `Rect` in a row-major order.
|
||||
pub struct Positions {
|
||||
/// The `Rect` associated with the positions.
|
||||
pub rect: Rect,
|
||||
/// The current position within the `Rect`.
|
||||
pub current_position: Position,
|
||||
}
|
||||
|
||||
impl Positions {
|
||||
/// Creates a new `Positions` iterator.
|
||||
pub const fn new(rect: Rect) -> Self {
|
||||
Self {
|
||||
rect,
|
||||
current_position: Position::new(rect.x, rect.y),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for Positions {
|
||||
type Item = Position;
|
||||
|
||||
/// Retrieves the next position within the `Rect`.
|
||||
///
|
||||
/// Returns `None` when there are no more positions to iterate through.
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.current_position.y >= self.rect.bottom() {
|
||||
return None;
|
||||
}
|
||||
let position = self.current_position;
|
||||
self.current_position.x += 1;
|
||||
if self.current_position.x >= self.rect.right() {
|
||||
self.current_position.x = self.rect.x;
|
||||
self.current_position.y += 1;
|
||||
}
|
||||
Some(position)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn rows() {
|
||||
let rect = Rect::new(0, 0, 2, 2);
|
||||
let mut rows = Rows::new(rect);
|
||||
assert_eq!(rows.next(), Some(Rect::new(0, 0, 2, 1)));
|
||||
assert_eq!(rows.next(), Some(Rect::new(0, 1, 2, 1)));
|
||||
assert_eq!(rows.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn columns() {
|
||||
let rect = Rect::new(0, 0, 2, 2);
|
||||
let mut columns = Columns::new(rect);
|
||||
assert_eq!(columns.next(), Some(Rect::new(0, 0, 1, 2)));
|
||||
assert_eq!(columns.next(), Some(Rect::new(1, 0, 1, 2)));
|
||||
assert_eq!(columns.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn positions() {
|
||||
let rect = Rect::new(0, 0, 2, 2);
|
||||
let mut positions = Positions::new(rect);
|
||||
assert_eq!(positions.next(), Some(Position::new(0, 0)));
|
||||
assert_eq!(positions.next(), Some(Position::new(1, 0)));
|
||||
assert_eq!(positions.next(), Some(Position::new(0, 1)));
|
||||
assert_eq!(positions.next(), Some(Position::new(1, 1)));
|
||||
assert_eq!(positions.next(), None);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
/// 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,
|
||||
}
|
||||
@@ -15,14 +15,14 @@ pub struct Size {
|
||||
|
||||
impl Size {
|
||||
/// Create a new `Size` struct
|
||||
pub fn new(width: u16, height: u16) -> Self {
|
||||
Size { width, height }
|
||||
pub const fn new(width: u16, height: u16) -> Self {
|
||||
Self { width, height }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(u16, u16)> for Size {
|
||||
fn from((width, height): (u16, u16)) -> Self {
|
||||
Size { width, height }
|
||||
Self { width, height }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
23
src/lib.rs
23
src/lib.rs
@@ -1,11 +1,9 @@
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
//! 
|
||||
//!
|
||||
//! <div align="center">
|
||||
//!
|
||||
//! [![Crate Badge]][Crate] [![Docs Badge]][API Docs] [![CI Badge]][CI Workflow] [![License
|
||||
//! Badge]](./LICENSE)<br>
|
||||
//! Badge]](./LICENSE) [![Sponsors Badge]][GitHub Sponsors]<br>
|
||||
//! [![Codecov Badge]][Codecov] [![Deps.rs Badge]][Deps.rs] [![Discord Badge]][Discord Server]
|
||||
//! [![Matrix Badge]][Matrix]<br>
|
||||
//!
|
||||
@@ -40,6 +38,9 @@
|
||||
//! automatically redrawn on the next frame. See the [Rendering] section of the [Ratatui Website]
|
||||
//! for more info.
|
||||
//!
|
||||
//! You can also watch the [FOSDEM 2024 talk] about Ratatui which gives a brief introduction to
|
||||
//! terminal user interfaces and showcases the features of Ratatui, along with a hello world demo.
|
||||
//!
|
||||
//! ## Other documentation
|
||||
//!
|
||||
//! - [Ratatui Website] - explains the library's concepts and provides step-by-step tutorials
|
||||
@@ -138,8 +139,7 @@
|
||||
//!
|
||||
//! fn ui(frame: &mut Frame) {
|
||||
//! frame.render_widget(
|
||||
//! Paragraph::new("Hello World!")
|
||||
//! .block(Block::default().title("Greeting").borders(Borders::ALL)),
|
||||
//! Paragraph::new("Hello World!").block(Block::bordered().title("Greeting")),
|
||||
//! frame.size(),
|
||||
//! );
|
||||
//! }
|
||||
@@ -183,14 +183,8 @@
|
||||
//! [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],
|
||||
//! );
|
||||
//! frame.render_widget(Block::bordered().title("Left"), inner_layout[0]);
|
||||
//! frame.render_widget(Block::bordered().title("Right"), inner_layout[1]);
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
@@ -299,6 +293,7 @@
|
||||
//! [Changelog]: https://github.com/ratatui-org/ratatui/blob/main/CHANGELOG.md
|
||||
//! [Contributing]: https://github.com/ratatui-org/ratatui/blob/main/CONTRIBUTING.md
|
||||
//! [Breaking Changes]: https://github.com/ratatui-org/ratatui/blob/main/BREAKING-CHANGES.md
|
||||
//! [FOSDEM 2024 talk]: https://www.youtube.com/watch?v=NU0q6NOLJ20
|
||||
//! [docsrs-hello]: https://github.com/ratatui-org/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-hello.png?raw=true
|
||||
//! [docsrs-layout]: https://github.com/ratatui-org/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-layout.png?raw=true
|
||||
//! [docsrs-styling]: https://github.com/ratatui-org/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-styling.png?raw=true
|
||||
@@ -320,6 +315,7 @@
|
||||
//! [Termion]: https://crates.io/crates/termion
|
||||
//! [Termwiz]: https://crates.io/crates/termwiz
|
||||
//! [tui-rs]: https://crates.io/crates/tui
|
||||
//! [GitHub Sponsors]: https://github.com/sponsors/ratatui-org
|
||||
//! [Crate Badge]: https://img.shields.io/crates/v/ratatui?logo=rust&style=flat-square
|
||||
//! [License Badge]: https://img.shields.io/crates/l/ratatui?style=flat-square
|
||||
//! [CI Badge]:
|
||||
@@ -337,6 +333,7 @@
|
||||
//! [Matrix Badge]:
|
||||
//! https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix
|
||||
//! [Matrix]: https://matrix.to/#/#ratatui:matrix.org
|
||||
//! [Sponsors Badge]: https://img.shields.io/github/sponsors/ratatui-org?logo=github&style=flat-square
|
||||
|
||||
// show the feature flags in the generated documentation
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
|
||||
120
src/style.rs
120
src/style.rs
@@ -33,7 +33,7 @@
|
||||
//! that implements [`Styled`]. E.g.:
|
||||
//! - Strings and string slices when styled return a [`Span`]
|
||||
//! - [`Span`]s can be styled again, which will merge the styles.
|
||||
//! - Many widget types can be styled directly rather than calling their style() method.
|
||||
//! - Many widget types can be styled directly rather than calling their `style()` method.
|
||||
//!
|
||||
//! See the [`Stylize`] and [`Styled`] traits for more information. These traits are re-exported in
|
||||
//! the [`prelude`] module for convenience.
|
||||
@@ -68,14 +68,14 @@
|
||||
//! [`prelude`]: crate::prelude
|
||||
//! [`Span`]: crate::text::Span
|
||||
|
||||
use std::fmt::{self, Debug};
|
||||
use std::fmt;
|
||||
|
||||
use bitflags::bitflags;
|
||||
|
||||
mod color;
|
||||
mod stylize;
|
||||
|
||||
pub use color::Color;
|
||||
pub use color::{Color, ParseColorError};
|
||||
pub use stylize::{Styled, Stylize};
|
||||
pub mod palette;
|
||||
|
||||
@@ -119,7 +119,7 @@ impl fmt::Debug for Modifier {
|
||||
if self.is_empty() {
|
||||
return write!(f, "NONE");
|
||||
}
|
||||
fmt::Debug::fmt(&self.0, f)
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,26 +234,26 @@ pub struct Style {
|
||||
}
|
||||
|
||||
impl Default for Style {
|
||||
fn default() -> Style {
|
||||
Style::new()
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for Style {
|
||||
type Item = Style;
|
||||
type Item = Self;
|
||||
|
||||
fn style(&self) -> Style {
|
||||
*self
|
||||
}
|
||||
|
||||
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
|
||||
fn set_style<S: Into<Self>>(self, style: S) -> Self::Item {
|
||||
self.patch(style)
|
||||
}
|
||||
}
|
||||
|
||||
impl Style {
|
||||
pub const fn new() -> Style {
|
||||
Style {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
fg: None,
|
||||
bg: None,
|
||||
#[cfg(feature = "underline-color")]
|
||||
@@ -264,8 +264,8 @@ impl Style {
|
||||
}
|
||||
|
||||
/// Returns a `Style` resetting all properties.
|
||||
pub const fn reset() -> Style {
|
||||
Style {
|
||||
pub const fn reset() -> Self {
|
||||
Self {
|
||||
fg: Some(Color::Reset),
|
||||
bg: Some(Color::Reset),
|
||||
#[cfg(feature = "underline-color")]
|
||||
@@ -286,7 +286,7 @@ impl Style {
|
||||
/// 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 {
|
||||
pub const fn fg(mut self, color: Color) -> Self {
|
||||
self.fg = Some(color);
|
||||
self
|
||||
}
|
||||
@@ -302,7 +302,7 @@ impl Style {
|
||||
/// 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 {
|
||||
pub const fn bg(mut self, color: Color) -> Self {
|
||||
self.bg = Some(color);
|
||||
self
|
||||
}
|
||||
@@ -336,7 +336,7 @@ impl Style {
|
||||
/// ```
|
||||
#[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 {
|
||||
pub const fn underline_color(mut self, color: Color) -> Self {
|
||||
self.underline_color = Some(color);
|
||||
self
|
||||
}
|
||||
@@ -356,7 +356,7 @@ impl Style {
|
||||
/// 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 {
|
||||
pub const fn add_modifier(mut self, modifier: Modifier) -> Self {
|
||||
self.sub_modifier = self.sub_modifier.difference(modifier);
|
||||
self.add_modifier = self.add_modifier.union(modifier);
|
||||
self
|
||||
@@ -377,7 +377,7 @@ impl Style {
|
||||
/// 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 {
|
||||
pub const fn remove_modifier(mut self, modifier: Modifier) -> Self {
|
||||
self.add_modifier = self.add_modifier.difference(modifier);
|
||||
self.sub_modifier = self.sub_modifier.union(modifier);
|
||||
self
|
||||
@@ -401,7 +401,7 @@ impl Style {
|
||||
/// );
|
||||
/// ```
|
||||
#[must_use = "`patch` returns the modified style without modifying the original"]
|
||||
pub fn patch<S: Into<Style>>(mut self, other: S) -> Style {
|
||||
pub fn patch<S: Into<Self>>(mut self, other: S) -> Self {
|
||||
let other = other.into();
|
||||
self.fg = other.fg.or(self.fg);
|
||||
self.bg = other.bg.or(self.bg);
|
||||
@@ -550,34 +550,30 @@ impl From<(Color, Color, Modifier, Modifier)> for Style {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rstest::rstest;
|
||||
|
||||
fn styles() -> Vec<Style> {
|
||||
vec![
|
||||
Style::default(),
|
||||
Style::default().fg(Color::Yellow),
|
||||
Style::default().bg(Color::Yellow),
|
||||
Style::default().add_modifier(Modifier::BOLD),
|
||||
Style::default().remove_modifier(Modifier::BOLD),
|
||||
Style::default().add_modifier(Modifier::ITALIC),
|
||||
Style::default().remove_modifier(Modifier::ITALIC),
|
||||
Style::default().add_modifier(Modifier::ITALIC | Modifier::BOLD),
|
||||
Style::default().remove_modifier(Modifier::ITALIC | Modifier::BOLD),
|
||||
]
|
||||
}
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn combined_patch_gives_same_result_as_individual_patch() {
|
||||
let styles = styles();
|
||||
let styles = [
|
||||
Style::new(),
|
||||
Style::new().fg(Color::Yellow),
|
||||
Style::new().bg(Color::Yellow),
|
||||
Style::new().add_modifier(Modifier::BOLD),
|
||||
Style::new().remove_modifier(Modifier::BOLD),
|
||||
Style::new().add_modifier(Modifier::ITALIC),
|
||||
Style::new().remove_modifier(Modifier::ITALIC),
|
||||
Style::new().add_modifier(Modifier::ITALIC | Modifier::BOLD),
|
||||
Style::new().remove_modifier(Modifier::ITALIC | Modifier::BOLD),
|
||||
];
|
||||
for &a in &styles {
|
||||
for &b in &styles {
|
||||
for &c in &styles {
|
||||
for &d in &styles {
|
||||
let combined = a.patch(b.patch(c.patch(d)));
|
||||
|
||||
assert_eq!(
|
||||
Style::default().patch(a).patch(b).patch(c).patch(d),
|
||||
Style::default().patch(combined)
|
||||
Style::new().patch(a).patch(b).patch(c).patch(d),
|
||||
Style::new().patch(a.patch(b.patch(c.patch(d))))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -589,7 +585,7 @@ mod tests {
|
||||
fn combine_individual_modifiers() {
|
||||
use crate::{buffer::Buffer, layout::Rect};
|
||||
|
||||
let mods = vec![
|
||||
let mods = [
|
||||
Modifier::BOLD,
|
||||
Modifier::DIM,
|
||||
Modifier::ITALIC,
|
||||
@@ -603,37 +599,30 @@ mod tests {
|
||||
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
|
||||
|
||||
for m in &mods {
|
||||
for m in mods {
|
||||
buffer.get_mut(0, 0).set_style(Style::reset());
|
||||
buffer
|
||||
.get_mut(0, 0)
|
||||
.set_style(Style::default().add_modifier(*m));
|
||||
buffer.get_mut(0, 0).set_style(Style::new().add_modifier(m));
|
||||
let style = buffer.get(0, 0).style();
|
||||
assert!(style.add_modifier.contains(*m));
|
||||
assert!(!style.sub_modifier.contains(*m));
|
||||
assert!(style.add_modifier.contains(m));
|
||||
assert!(!style.sub_modifier.contains(m));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn modifier_debug() {
|
||||
assert_eq!(format!("{:?}", Modifier::empty()), "NONE");
|
||||
assert_eq!(format!("{:?}", Modifier::BOLD), "BOLD");
|
||||
assert_eq!(format!("{:?}", Modifier::DIM), "DIM");
|
||||
assert_eq!(format!("{:?}", Modifier::ITALIC), "ITALIC");
|
||||
assert_eq!(format!("{:?}", Modifier::UNDERLINED), "UNDERLINED");
|
||||
assert_eq!(format!("{:?}", Modifier::SLOW_BLINK), "SLOW_BLINK");
|
||||
assert_eq!(format!("{:?}", Modifier::RAPID_BLINK), "RAPID_BLINK");
|
||||
assert_eq!(format!("{:?}", Modifier::REVERSED), "REVERSED");
|
||||
assert_eq!(format!("{:?}", Modifier::HIDDEN), "HIDDEN");
|
||||
assert_eq!(format!("{:?}", Modifier::CROSSED_OUT), "CROSSED_OUT");
|
||||
assert_eq!(
|
||||
format!("{:?}", Modifier::BOLD | Modifier::DIM),
|
||||
"BOLD | DIM"
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{:?}", Modifier::all()),
|
||||
"BOLD | DIM | ITALIC | UNDERLINED | SLOW_BLINK | RAPID_BLINK | REVERSED | HIDDEN | CROSSED_OUT"
|
||||
);
|
||||
#[rstest]
|
||||
#[case(Modifier::empty(), "NONE")]
|
||||
#[case(Modifier::BOLD, "BOLD")]
|
||||
#[case(Modifier::DIM, "DIM")]
|
||||
#[case(Modifier::ITALIC, "ITALIC")]
|
||||
#[case(Modifier::UNDERLINED, "UNDERLINED")]
|
||||
#[case(Modifier::SLOW_BLINK, "SLOW_BLINK")]
|
||||
#[case(Modifier::RAPID_BLINK, "RAPID_BLINK")]
|
||||
#[case(Modifier::REVERSED, "REVERSED")]
|
||||
#[case(Modifier::HIDDEN, "HIDDEN")]
|
||||
#[case(Modifier::CROSSED_OUT, "CROSSED_OUT")]
|
||||
#[case(Modifier::BOLD | Modifier::DIM, "BOLD | DIM")]
|
||||
#[case(Modifier::all(), "BOLD | DIM | ITALIC | UNDERLINED | SLOW_BLINK | RAPID_BLINK | REVERSED | HIDDEN | CROSSED_OUT")]
|
||||
fn modifier_debug(#[case] modifier: Modifier, #[case] expected: &str) {
|
||||
assert_eq!(format!("{modifier:?}"), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -660,9 +649,10 @@ mod tests {
|
||||
.bg(Color::Black)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.remove_modifier(Modifier::ITALIC)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
#[test]
|
||||
fn style_can_be_stylized() {
|
||||
// foreground colors
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::{
|
||||
fmt::{self, Debug, Display},
|
||||
str::FromStr,
|
||||
};
|
||||
#![allow(clippy::unreadable_literal)]
|
||||
|
||||
use std::{fmt, str::FromStr};
|
||||
|
||||
/// ANSI Color
|
||||
///
|
||||
@@ -64,7 +63,6 @@ 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))]
|
||||
pub enum Color {
|
||||
/// Resets the foreground or background color
|
||||
#[default]
|
||||
@@ -131,22 +129,110 @@ impl Color {
|
||||
/// Convert a u32 to a Color
|
||||
///
|
||||
/// The u32 should be in the format 0x00RRGGBB.
|
||||
pub const fn from_u32(u: u32) -> Color {
|
||||
pub const fn from_u32(u: u32) -> Self {
|
||||
let r = (u >> 16) as u8;
|
||||
let g = (u >> 8) as u8;
|
||||
let b = u as u8;
|
||||
Color::Rgb(r, g, b)
|
||||
Self::Rgb(r, g, b)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl serde::Serialize for Color {
|
||||
/// This utilises the [`fmt::Display`] implementation for serialization.
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl<'de> serde::Deserialize<'de> for Color {
|
||||
/// This is used to deserialize a value into Color via serde.
|
||||
///
|
||||
/// This implementation uses the `FromStr` trait to deserialize strings, so named colours, RGB,
|
||||
/// and indexed values are able to be deserialized. In addition, values that were produced by
|
||||
/// the the older serialization implementation of Color are also able to be deserialized.
|
||||
///
|
||||
/// Prior to v0.26.0, Ratatui would be serialized using a map for indexed and RGB values, for
|
||||
/// examples in json `{"Indexed": 10}` and `{"Rgb": [255, 0, 255]}` respectively. Now they are
|
||||
/// serialized using the string representation of the index and the RGB hex value, for example
|
||||
/// in json it would now be `"10"` and `"#FF00FF"` respectively.
|
||||
///
|
||||
/// See the [`Color`] documentation for more information on color names.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::prelude::*;
|
||||
///
|
||||
/// #[derive(Debug, serde::Deserialize)]
|
||||
/// struct Theme {
|
||||
/// color: Color,
|
||||
/// }
|
||||
///
|
||||
/// # fn get_theme() -> Result<(), serde_json::Error> {
|
||||
/// let theme: Theme = serde_json::from_str(r#"{"color": "bright-white"}"#)?;
|
||||
/// assert_eq!(theme.color, Color::White);
|
||||
///
|
||||
/// let theme: Theme = serde_json::from_str(r##"{"color": "#00FF00"}"##)?;
|
||||
/// assert_eq!(theme.color, Color::Rgb(0, 255, 0));
|
||||
///
|
||||
/// let theme: Theme = serde_json::from_str(r#"{"color": "42"}"#)?;
|
||||
/// assert_eq!(theme.color, Color::Indexed(42));
|
||||
///
|
||||
/// let err = serde_json::from_str::<Theme>(r#"{"color": "invalid"}"#).unwrap_err();
|
||||
/// assert!(err.is_data());
|
||||
/// assert_eq!(
|
||||
/// err.to_string(),
|
||||
/// "Failed to parse Colors at line 1 column 20"
|
||||
/// );
|
||||
///
|
||||
/// // Deserializing from the previous serialization implementation
|
||||
/// let theme: Theme = serde_json::from_str(r#"{"color": {"Rgb":[255,0,255]}}"#)?;
|
||||
/// assert_eq!(theme.color, Color::Rgb(255, 0, 255));
|
||||
///
|
||||
/// let theme: Theme = serde_json::from_str(r#"{"color": {"Indexed":10}}"#)?;
|
||||
/// assert_eq!(theme.color, Color::Indexed(10));
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
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)
|
||||
/// Colors are currently serialized with the `Display` implementation, so
|
||||
/// RGB values are serialized via hex, for example "#FFFFFF".
|
||||
///
|
||||
/// Previously they were serialized using serde derive, which encoded
|
||||
/// RGB values as a map, for example { "rgb": [255, 255, 255] }.
|
||||
///
|
||||
/// The deserialization implementation utilises a `Helper` struct
|
||||
/// to be able to support both formats for backwards compatibility.
|
||||
#[derive(serde::Deserialize)]
|
||||
enum ColorWrapper {
|
||||
Rgb(u8, u8, u8),
|
||||
Indexed(u8),
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum ColorFormat {
|
||||
V2(String),
|
||||
V1(ColorWrapper),
|
||||
}
|
||||
|
||||
let multi_type = ColorFormat::deserialize(deserializer)
|
||||
.map_err(|err| serde::de::Error::custom(format!("Failed to parse Colors: {err}")))?;
|
||||
match multi_type {
|
||||
ColorFormat::V2(s) => FromStr::from_str(&s).map_err(serde::de::Error::custom),
|
||||
ColorFormat::V1(color_wrapper) => match color_wrapper {
|
||||
ColorWrapper::Rgb(red, green, blue) => Ok(Self::Rgb(red, green, blue)),
|
||||
ColorWrapper::Indexed(index) => Ok(Self::Indexed(index)),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,8 +240,8 @@ impl<'de> serde::Deserialize<'de> for Color {
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub struct ParseColorError;
|
||||
|
||||
impl std::fmt::Display for ParseColorError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
impl fmt::Display for ParseColorError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "Failed to parse Colors")
|
||||
}
|
||||
}
|
||||
@@ -247,28 +333,28 @@ impl FromStr for Color {
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Color {
|
||||
impl fmt::Display for Color {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Color::Reset => write!(f, "Reset"),
|
||||
Color::Black => write!(f, "Black"),
|
||||
Color::Red => write!(f, "Red"),
|
||||
Color::Green => write!(f, "Green"),
|
||||
Color::Yellow => write!(f, "Yellow"),
|
||||
Color::Blue => write!(f, "Blue"),
|
||||
Color::Magenta => write!(f, "Magenta"),
|
||||
Color::Cyan => write!(f, "Cyan"),
|
||||
Color::Gray => write!(f, "Gray"),
|
||||
Color::DarkGray => write!(f, "DarkGray"),
|
||||
Color::LightRed => write!(f, "LightRed"),
|
||||
Color::LightGreen => write!(f, "LightGreen"),
|
||||
Color::LightYellow => write!(f, "LightYellow"),
|
||||
Color::LightBlue => write!(f, "LightBlue"),
|
||||
Color::LightMagenta => write!(f, "LightMagenta"),
|
||||
Color::LightCyan => write!(f, "LightCyan"),
|
||||
Color::White => write!(f, "White"),
|
||||
Color::Rgb(r, g, b) => write!(f, "#{:02X}{:02X}{:02X}", r, g, b),
|
||||
Color::Indexed(i) => write!(f, "{}", i),
|
||||
Self::Reset => write!(f, "Reset"),
|
||||
Self::Black => write!(f, "Black"),
|
||||
Self::Red => write!(f, "Red"),
|
||||
Self::Green => write!(f, "Green"),
|
||||
Self::Yellow => write!(f, "Yellow"),
|
||||
Self::Blue => write!(f, "Blue"),
|
||||
Self::Magenta => write!(f, "Magenta"),
|
||||
Self::Cyan => write!(f, "Cyan"),
|
||||
Self::Gray => write!(f, "Gray"),
|
||||
Self::DarkGray => write!(f, "DarkGray"),
|
||||
Self::LightRed => write!(f, "LightRed"),
|
||||
Self::LightGreen => write!(f, "LightGreen"),
|
||||
Self::LightYellow => write!(f, "LightYellow"),
|
||||
Self::LightBlue => write!(f, "LightBlue"),
|
||||
Self::LightMagenta => write!(f, "LightMagenta"),
|
||||
Self::LightCyan => write!(f, "LightCyan"),
|
||||
Self::White => write!(f, "White"),
|
||||
Self::Rgb(r, g, b) => write!(f, "#{r:02X}{g:02X}{b:02X}"),
|
||||
Self::Indexed(i) => write!(f, "{i}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -308,8 +394,8 @@ impl Color {
|
||||
/// Converts normalized HSL (Hue, Saturation, Lightness) values to RGB (Red, Green, Blue) color
|
||||
/// representation. H, S, and L values should be in the range [0, 1].
|
||||
///
|
||||
/// Based on https://github.com/killercup/hsl-rs/blob/b8a30e11afd75f262e0550725333293805f4ead0/src/lib.rs
|
||||
fn normalized_hsl_to_rgb(h: f64, s: f64, l: f64) -> Color {
|
||||
/// Based on <https://github.com/killercup/hsl-rs/blob/b8a30e11afd75f262e0550725333293805f4ead0/src/lib.rs>
|
||||
fn normalized_hsl_to_rgb(hue: f64, saturation: f64, lightness: f64) -> Color {
|
||||
// This function can be made into `const` in the future.
|
||||
// This comment contains the relevant information for making it `const`.
|
||||
//
|
||||
@@ -329,33 +415,33 @@ fn normalized_hsl_to_rgb(h: f64, s: f64, l: f64) -> Color {
|
||||
// ```
|
||||
|
||||
// Initialize RGB components
|
||||
let r: f64;
|
||||
let g: f64;
|
||||
let b: f64;
|
||||
let red: f64;
|
||||
let green: f64;
|
||||
let blue: f64;
|
||||
|
||||
// Check if the color is achromatic (grayscale)
|
||||
if s == 0.0 {
|
||||
r = l;
|
||||
g = l;
|
||||
b = l;
|
||||
if saturation == 0.0 {
|
||||
red = lightness;
|
||||
green = lightness;
|
||||
blue = lightness;
|
||||
} else {
|
||||
// Calculate RGB components for colored cases
|
||||
let q = if l < 0.5 {
|
||||
l * (1.0 + s)
|
||||
let q = if lightness < 0.5 {
|
||||
lightness * (1.0 + saturation)
|
||||
} else {
|
||||
l + s - l * s
|
||||
lightness + saturation - lightness * saturation
|
||||
};
|
||||
let p = 2.0 * l - q;
|
||||
r = hue_to_rgb(p, q, h + 1.0 / 3.0);
|
||||
g = hue_to_rgb(p, q, h);
|
||||
b = hue_to_rgb(p, q, h - 1.0 / 3.0);
|
||||
let p = 2.0 * lightness - q;
|
||||
red = hue_to_rgb(p, q, hue + 1.0 / 3.0);
|
||||
green = hue_to_rgb(p, q, hue);
|
||||
blue = hue_to_rgb(p, q, hue - 1.0 / 3.0);
|
||||
}
|
||||
|
||||
// Scale RGB components to the range [0, 255] and create a Color::Rgb instance
|
||||
Color::Rgb(
|
||||
(r * 255.0).round() as u8,
|
||||
(g * 255.0).round() as u8,
|
||||
(b * 255.0).round() as u8,
|
||||
(red * 255.0).round() as u8,
|
||||
(green * 255.0).round() as u8,
|
||||
(blue * 255.0).round() as u8,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -577,4 +663,42 @@ mod tests {
|
||||
Color::deserialize("#00000000".into_deserializer());
|
||||
assert!(color.is_err());
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
#[test]
|
||||
fn serialize_then_deserialize() -> Result<(), serde_json::Error> {
|
||||
let json_rgb = serde_json::to_string(&Color::Rgb(255, 0, 255))?;
|
||||
assert_eq!(json_rgb, r##""#FF00FF""##);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<Color>(&json_rgb)?,
|
||||
Color::Rgb(255, 0, 255)
|
||||
);
|
||||
|
||||
let json_white = serde_json::to_string(&Color::White)?;
|
||||
assert_eq!(json_white, r#""White""#);
|
||||
|
||||
let json_indexed = serde_json::to_string(&Color::Indexed(10))?;
|
||||
assert_eq!(json_indexed, r#""10""#);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<Color>(&json_indexed)?,
|
||||
Color::Indexed(10)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
#[test]
|
||||
fn deserialize_with_previous_format() -> Result<(), serde_json::Error> {
|
||||
assert_eq!(Color::White, serde_json::from_str::<Color>("\"White\"")?);
|
||||
assert_eq!(
|
||||
Color::Rgb(255, 0, 255),
|
||||
serde_json::from_str::<Color>(r#"{"Rgb":[255,0,255]}"#)?
|
||||
);
|
||||
assert_eq!(
|
||||
Color::Indexed(10),
|
||||
serde_json::from_str::<Color>(r#"{"Indexed":10}"#)?
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![allow(clippy::unreadable_literal)]
|
||||
|
||||
//! A module for defining color palettes.
|
||||
|
||||
pub mod material;
|
||||
|
||||
@@ -455,11 +455,11 @@ pub struct NonAccentedPalette {
|
||||
}
|
||||
|
||||
impl AccentedPalette {
|
||||
/// Create a new AccentedPalette from the given variants
|
||||
/// 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 {
|
||||
pub const fn from_variants(variants: [u32; 14]) -> Self {
|
||||
Self {
|
||||
c50: Color::from_u32(variants[0]),
|
||||
c100: Color::from_u32(variants[1]),
|
||||
c200: Color::from_u32(variants[2]),
|
||||
@@ -479,11 +479,11 @@ impl AccentedPalette {
|
||||
}
|
||||
|
||||
impl NonAccentedPalette {
|
||||
/// Create a new NonAccented from the given variants
|
||||
/// 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 {
|
||||
pub const fn from_variants(variants: [u32; 10]) -> Self {
|
||||
Self {
|
||||
c50: Color::from_u32(variants[0]),
|
||||
c100: Color::from_u32(variants[1]),
|
||||
c200: Color::from_u32(variants[2]),
|
||||
|
||||
@@ -133,11 +133,7 @@ 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::bordered().title("Title").on_white().bold();
|
||||
/// ```
|
||||
pub trait Stylize<'a, T>: Sized {
|
||||
#[must_use = "`bg` returns the modified style without modifying the original"]
|
||||
@@ -348,7 +344,7 @@ mod tests {
|
||||
|
||||
// format!() is used to create a temporary String inside a closure, which suffers the same
|
||||
// issue as above without the `Styled` trait impl for `String`
|
||||
let items = vec![String::from("a"), String::from("b")];
|
||||
let items = [String::from("a"), String::from("b")];
|
||||
let sss = items.iter().map(|s| format!("{s}{s}").red()).collect_vec();
|
||||
assert_eq!(sss, vec![Span::from("aa").red(), Span::from("bb").red()]);
|
||||
}
|
||||
@@ -393,12 +389,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn repeated_attributes() {
|
||||
let cyan_bg = Style::default().bg(Color::Cyan);
|
||||
let cyan_fg = Style::default().fg(Color::Cyan);
|
||||
let bg = Style::default().bg(Color::Cyan);
|
||||
let fg = Style::default().fg(Color::Cyan);
|
||||
|
||||
// Behavior: the last one set is the definitive one
|
||||
assert_eq!("hello".on_red().on_cyan(), Span::styled("hello", cyan_bg));
|
||||
assert_eq!("hello".red().cyan(), Span::styled("hello", cyan_fg));
|
||||
assert_eq!("hello".on_red().on_cyan(), Span::styled("hello", bg));
|
||||
assert_eq!("hello".red().cyan(), Span::styled("hello", fg));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -395,6 +395,7 @@ pub mod border {
|
||||
/// ▏xxxxx▕
|
||||
/// ▔▔▔▔▔▔▔
|
||||
/// ```
|
||||
#[allow(clippy::doc_markdown)]
|
||||
pub const ONE_EIGHTH_WIDE: Set = Set {
|
||||
top_right: ONE_EIGHTH_BOTTOM_EIGHT,
|
||||
top_left: ONE_EIGHTH_BOTTOM_EIGHT,
|
||||
@@ -414,6 +415,7 @@ pub mod border {
|
||||
/// ▕xx▏
|
||||
/// ▕▁▁▏
|
||||
/// ```
|
||||
#[allow(clippy::doc_markdown)]
|
||||
pub const ONE_EIGHTH_TALL: Set = Set {
|
||||
top_right: ONE_EIGHTH_LEFT_EIGHT,
|
||||
top_left: ONE_EIGHTH_RIGHT_EIGHT,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{prelude::*, widgets::WidgetRef};
|
||||
use crate::prelude::*;
|
||||
|
||||
/// A consistent view into the terminal state for rendering a single frame.
|
||||
///
|
||||
@@ -49,7 +49,7 @@ impl Frame<'_> {
|
||||
/// 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 {
|
||||
pub const fn size(&self) -> Rect {
|
||||
self.viewport_area
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ impl Frame<'_> {
|
||||
/// # let backend = TestBackend::new(5, 5);
|
||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||
/// # let mut frame = terminal.get_frame();
|
||||
/// let block = Block::default();
|
||||
/// let block = Block::new();
|
||||
/// let area = Rect::new(0, 0, 5, 5);
|
||||
/// frame.render_widget(block, area);
|
||||
/// ```
|
||||
@@ -87,10 +87,11 @@ impl Frame<'_> {
|
||||
/// # let backend = TestBackend::new(5, 5);
|
||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||
/// # let mut frame = terminal.get_frame();
|
||||
/// let block = Block::default();
|
||||
/// let block = Block::new();
|
||||
/// let area = Rect::new(0, 0, 5, 5);
|
||||
/// frame.render_widget_ref(block, area);
|
||||
/// ```
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
#[stability::unstable(feature = "widget-ref")]
|
||||
pub fn render_widget_ref<W: WidgetRef>(&mut self, widget: W, area: Rect) {
|
||||
widget.render_ref(area, self.buffer);
|
||||
@@ -146,6 +147,7 @@ impl Frame<'_> {
|
||||
/// let area = Rect::new(0, 0, 5, 5);
|
||||
/// frame.render_stateful_widget_ref(list, area, &mut state);
|
||||
/// ```
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
#[stability::unstable(feature = "widget-ref")]
|
||||
pub fn render_stateful_widget_ref<W>(&mut self, widget: W, area: Rect, state: &mut W::State)
|
||||
where
|
||||
@@ -177,7 +179,7 @@ impl Frame<'_> {
|
||||
///
|
||||
/// 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.
|
||||
/// 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
|
||||
@@ -193,7 +195,7 @@ impl Frame<'_> {
|
||||
/// let current_count = frame.count();
|
||||
/// println!("Current frame count: {}", current_count);
|
||||
/// ```
|
||||
pub fn count(&self) -> usize {
|
||||
pub const fn count(&self) -> usize {
|
||||
self.count
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,8 +111,8 @@ where
|
||||
/// let terminal = Terminal::new(backend)?;
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
pub fn new(backend: B) -> io::Result<Terminal<B>> {
|
||||
Terminal::with_options(
|
||||
pub fn new(backend: B) -> io::Result<Self> {
|
||||
Self::with_options(
|
||||
backend,
|
||||
TerminalOptions {
|
||||
viewport: Viewport::Fullscreen,
|
||||
@@ -132,7 +132,7 @@ where
|
||||
/// 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>> {
|
||||
pub fn with_options(mut backend: B, options: TerminalOptions) -> io::Result<Self> {
|
||||
let size = match options.viewport {
|
||||
Viewport::Fullscreen | Viewport::Inline(_) => backend.size()?,
|
||||
Viewport::Fixed(area) => area,
|
||||
@@ -142,7 +142,7 @@ where
|
||||
Viewport::Inline(height) => compute_inline_size(&mut backend, height, size, 0)?,
|
||||
Viewport::Fixed(area) => (area, (area.left(), area.top())),
|
||||
};
|
||||
Ok(Terminal {
|
||||
Ok(Self {
|
||||
backend,
|
||||
buffers: [Buffer::empty(viewport_area), Buffer::empty(viewport_area)],
|
||||
current: 0,
|
||||
@@ -172,7 +172,7 @@ where
|
||||
}
|
||||
|
||||
/// Gets the backend
|
||||
pub fn backend(&self) -> &B {
|
||||
pub const fn backend(&self) -> &B {
|
||||
&self.backend
|
||||
}
|
||||
|
||||
|
||||
@@ -31,9 +31,9 @@ pub enum Viewport {
|
||||
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),
|
||||
Self::Fullscreen => write!(f, "Fullscreen"),
|
||||
Self::Inline(height) => write!(f, "Inline({height})"),
|
||||
Self::Fixed(area) => write!(f, "Fixed({area})"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,21 +25,20 @@
|
||||
//! // Converted to Line(vec![
|
||||
//! // Span { content: Cow::Borrowed("My title"), style: Style { .. } }
|
||||
//! // ])
|
||||
//! let block = Block::default().title("My title");
|
||||
//! let block = Block::new().title("My title");
|
||||
//!
|
||||
//! // A simple string with a unique style.
|
||||
//! // 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::new().title(Span::styled("My title", Style::default().fg(Color::Yellow)));
|
||||
//!
|
||||
//! // A string with multiple styles.
|
||||
//! // Converted to Line(vec![
|
||||
//! // Span { content: Cow::Borrowed("My"), style: Style { fg: Some(Color::Yellow), .. } },
|
||||
//! // Span { content: Cow::Borrowed(" title"), .. }
|
||||
//! // ])
|
||||
//! let block = Block::default().title(vec![
|
||||
//! let block = Block::new().title(vec![
|
||||
//! Span::styled("My", Style::default().fg(Color::Yellow)),
|
||||
//! Span::raw(" title"),
|
||||
//! ]);
|
||||
|
||||
@@ -16,8 +16,8 @@ impl<'a> StyledGrapheme<'a> {
|
||||
///
|
||||
/// `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 {
|
||||
pub fn new<S: Into<Style>>(symbol: &'a str, style: S) -> Self {
|
||||
Self {
|
||||
symbol,
|
||||
style: style.into(),
|
||||
}
|
||||
@@ -25,7 +25,7 @@ impl<'a> StyledGrapheme<'a> {
|
||||
}
|
||||
|
||||
impl<'a> Styled for StyledGrapheme<'a> {
|
||||
type Item = StyledGrapheme<'a>;
|
||||
type Item = Self;
|
||||
|
||||
fn style(&self) -> Style {
|
||||
self.style
|
||||
|
||||
735
src/text/line.rs
735
src/text/line.rs
@@ -1,8 +1,11 @@
|
||||
#![deny(missing_docs)]
|
||||
use std::borrow::Cow;
|
||||
#![warn(clippy::pedantic, clippy::nursery, clippy::arithmetic_side_effects)]
|
||||
use std::{borrow::Cow, fmt};
|
||||
|
||||
use unicode_truncate::UnicodeTruncateStr;
|
||||
|
||||
use super::StyledGrapheme;
|
||||
use crate::{prelude::*, widgets::Widget};
|
||||
use crate::prelude::*;
|
||||
|
||||
/// A line of text, consisting of one or more [`Span`]s.
|
||||
///
|
||||
@@ -10,28 +13,21 @@ use crate::{prelude::*, widgets::Widget};
|
||||
/// 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.
|
||||
///
|
||||
/// # Conversion Methods
|
||||
///
|
||||
/// - [`Line::from`] creates a `Line` from a [`String`].
|
||||
/// - [`Line::from`] creates a `Line` from a [`&str`].
|
||||
/// - [`Line::from`] creates a `Line` from a [`Vec`] of [`Span`]s.
|
||||
/// - [`Line::from`] creates a `Line` from single [`Span`].
|
||||
/// - [`String::from`] converts a line into a [`String`].
|
||||
/// - [`Line::from_iter`] creates a line from an iterator of items that are convertible to [`Span`].
|
||||
///
|
||||
/// # Setter Methods
|
||||
///
|
||||
/// These methods are fluent setters. They return a `Line` with the property set.
|
||||
@@ -39,6 +35,15 @@ use crate::{prelude::*, widgets::Widget};
|
||||
/// - [`Line::spans`] sets the content of the line.
|
||||
/// - [`Line::style`] sets the style of the line.
|
||||
/// - [`Line::alignment`] sets the alignment of the line.
|
||||
/// - [`Line::left_aligned`] sets the alignment of the line to [`Alignment::Left`].
|
||||
/// - [`Line::centered`] sets the alignment of the line to [`Alignment::Center`].
|
||||
/// - [`Line::right_aligned`] sets the alignment of the line to [`Alignment::Right`].
|
||||
///
|
||||
/// # Iteration Methods
|
||||
///
|
||||
/// - [`Line::iter`] returns an iterator over the spans of this line.
|
||||
/// - [`Line::iter_mut`] returns a mutable iterator over the spans of this line.
|
||||
/// - [`Line::into_iter`] returns an iterator over the spans of this line.
|
||||
///
|
||||
/// # Other Methods
|
||||
///
|
||||
@@ -46,6 +51,7 @@ use crate::{prelude::*, widgets::Widget};
|
||||
/// - [`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.
|
||||
/// - [`Line::push_span`] adds a span to the line.
|
||||
///
|
||||
/// # Compatibility Notes
|
||||
///
|
||||
@@ -56,19 +62,90 @@ use crate::{prelude::*, widgets::Widget};
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ## Creating Lines
|
||||
/// [`Line`]s can be created from [`Span`]s, [`String`]s, and [`&str`]s. They can be styled with a
|
||||
/// [`Style`].
|
||||
///
|
||||
/// ```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![
|
||||
/// let style = Style::new().yellow();
|
||||
/// let line = Line::raw("Hello, world!").style(style);
|
||||
/// let line = Line::styled("Hello, world!", style);
|
||||
/// let line = Line::styled("Hello, world!", (Color::Yellow, Modifier::BOLD));
|
||||
///
|
||||
/// let line = Line::from("Hello, world!");
|
||||
/// let line = Line::from(String::from("Hello, world!"));
|
||||
/// let line = Line::from(vec![
|
||||
/// Span::styled("Hello", Style::new().blue()),
|
||||
/// Span::raw(" world!"),
|
||||
/// ]);
|
||||
/// ```
|
||||
///
|
||||
/// ## Styling Lines
|
||||
///
|
||||
/// The line's [`Style`] is used by the rendering widget to determine how to style the line. Each
|
||||
/// [`Span`] in the line will be styled with the [`Style`] of the line, and then with its own
|
||||
/// [`Style`]. If the line is longer than the available space, the style is applied to the entire
|
||||
/// line, and the line is truncated. `Line` also implements [`Styled`] which means you can use the
|
||||
/// methods of the [`Stylize`] trait.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let line = Line::from("Hello world!").style(Style::new().yellow().italic());
|
||||
/// let line = Line::from("Hello world!").style(Color::Yellow);
|
||||
/// let line = Line::from("Hello world!").style((Color::Yellow, Color::Black));
|
||||
/// let line = Line::from("Hello world!").style((Color::Yellow, Modifier::ITALIC));
|
||||
/// let line = Line::from("Hello world!").yellow().italic();
|
||||
/// ```
|
||||
///
|
||||
/// ## Aligning Lines
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let line = Line::from("Hello world!").alignment(Alignment::Right);
|
||||
/// let line = Line::from("Hello world!").centered();
|
||||
/// let line = Line::from("Hello world!").left_aligned();
|
||||
/// let line = Line::from("Hello world!").right_aligned();
|
||||
/// ```
|
||||
///
|
||||
/// ## Rendering Lines
|
||||
///
|
||||
/// `Line` implements the [`Widget`] trait, which means it can be rendered to a [`Buffer`].
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// # fn render(area: Rect, buf: &mut Buffer) {
|
||||
/// // in another widget's render method
|
||||
/// let line = Line::from("Hello world!").style(Style::new().yellow().italic());
|
||||
/// line.render(area, buf);
|
||||
/// # }
|
||||
///
|
||||
/// # fn draw(frame: &mut Frame, area: Rect) {
|
||||
/// // in a terminal.draw closure
|
||||
/// let line = Line::from("Hello world!").style(Style::new().yellow().italic());
|
||||
/// frame.render_widget(line, area);
|
||||
/// # }
|
||||
/// ```
|
||||
/// ## Rendering Lines with a Paragraph widget
|
||||
///
|
||||
/// Usually apps will use the [`Paragraph`] widget instead of rendering a [`Line`] directly as it
|
||||
/// provides more functionality.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// # fn render(area: Rect, buf: &mut Buffer) {
|
||||
/// let line = Line::from("Hello world!").yellow().italic();
|
||||
/// Paragraph::new(line)
|
||||
/// .wrap(Wrap { trim: true })
|
||||
/// .render(area, buf);
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// [`Paragraph`]: crate::widgets::Paragraph
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Line<'a> {
|
||||
@@ -102,11 +179,11 @@ impl<'a> Line<'a> {
|
||||
/// Line::raw(String::from("test content"));
|
||||
/// Line::raw(Cow::from("test content"));
|
||||
/// ```
|
||||
pub fn raw<T>(content: T) -> Line<'a>
|
||||
pub fn raw<T>(content: T) -> Self
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
{
|
||||
Line {
|
||||
Self {
|
||||
spans: content
|
||||
.into()
|
||||
.lines()
|
||||
@@ -135,12 +212,12 @@ impl<'a> Line<'a> {
|
||||
/// 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>
|
||||
pub fn styled<T, S>(content: T, style: S) -> Self
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
S: Into<Style>,
|
||||
{
|
||||
Line {
|
||||
Self {
|
||||
spans: content
|
||||
.into()
|
||||
.lines()
|
||||
@@ -378,6 +455,23 @@ impl<'a> Line<'a> {
|
||||
pub fn iter_mut(&mut self) -> std::slice::IterMut<Span<'a>> {
|
||||
self.spans.iter_mut()
|
||||
}
|
||||
|
||||
/// Adds a span to the line.
|
||||
///
|
||||
/// `span` can be any type that is convertible into a `Span`. For example, you can pass a
|
||||
/// `&str`, a `String`, or a `Span`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let mut line = Line::from("Hello, ");
|
||||
/// line.push_span(Span::raw("world!"));
|
||||
/// line.push_span(" How are you?");
|
||||
/// ```
|
||||
pub fn push_span<T: Into<Span<'a>>>(&mut self, span: T) {
|
||||
self.spans.push(span.into());
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for Line<'a> {
|
||||
@@ -435,14 +529,23 @@ impl<'a> From<Span<'a>> for Line<'a> {
|
||||
}
|
||||
|
||||
impl<'a> From<Line<'a>> for String {
|
||||
fn from(line: Line<'a>) -> String {
|
||||
line.iter().fold(String::new(), |mut acc, s| {
|
||||
fn from(line: Line<'a>) -> Self {
|
||||
line.iter().fold(Self::new(), |mut acc, s| {
|
||||
acc.push_str(s.content.as_ref());
|
||||
acc
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> FromIterator<T> for Line<'a>
|
||||
where
|
||||
T: Into<Span<'a>>,
|
||||
{
|
||||
fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
|
||||
Self::from(iter.into_iter().map(Into::into).collect::<Vec<_>>())
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Line<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.render_ref(area, buf);
|
||||
@@ -452,33 +555,100 @@ impl Widget for Line<'_> {
|
||||
impl WidgetRef for Line<'_> {
|
||||
fn render_ref(&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.iter() {
|
||||
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;
|
||||
}
|
||||
if area.is_empty() {
|
||||
return;
|
||||
}
|
||||
let line_width = self.width();
|
||||
if line_width == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
buf.set_style(area, self.style);
|
||||
|
||||
let area_width = usize::from(area.width);
|
||||
let can_render_complete_line = line_width <= area_width;
|
||||
if can_render_complete_line {
|
||||
let indent_width = match self.alignment {
|
||||
Some(Alignment::Center) => (area_width.saturating_sub(line_width)) / 2,
|
||||
Some(Alignment::Right) => area_width.saturating_sub(line_width),
|
||||
Some(Alignment::Left) | None => 0,
|
||||
};
|
||||
let indent_width = u16::try_from(indent_width).unwrap_or(u16::MAX);
|
||||
let area = area.indent_x(indent_width);
|
||||
render_spans(&self.spans, area, buf, 0);
|
||||
} else {
|
||||
// There is not enough space to render the whole line. As the right side is truncated by
|
||||
// the area width, only truncate the left.
|
||||
let skip_width = match self.alignment {
|
||||
Some(Alignment::Center) => (line_width.saturating_sub(area_width)) / 2,
|
||||
Some(Alignment::Right) => line_width.saturating_sub(area_width),
|
||||
Some(Alignment::Left) | None => 0,
|
||||
};
|
||||
render_spans(&self.spans, area, buf, skip_width);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Line<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
/// Renders all the spans of the line that should be visible.
|
||||
fn render_spans(spans: &[Span], mut area: Rect, buf: &mut Buffer, span_skip_width: usize) {
|
||||
for (span, span_width, offset) in spans_after_width(spans, span_skip_width) {
|
||||
area = area.indent_x(offset);
|
||||
if area.is_empty() {
|
||||
break;
|
||||
}
|
||||
span.render_ref(area, buf);
|
||||
let span_width = u16::try_from(span_width).unwrap_or(u16::MAX);
|
||||
area = area.indent_x(span_width);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an iterator over the spans that lie after a given skip widtch from the start of the
|
||||
/// `Line` (including a partially visible span if the `skip_width` lands within a span).
|
||||
fn spans_after_width<'a>(
|
||||
spans: &'a [Span],
|
||||
mut skip_width: usize,
|
||||
) -> impl Iterator<Item = (Span<'a>, usize, u16)> {
|
||||
spans
|
||||
.iter()
|
||||
.map(|span| (span, span.width()))
|
||||
// Filter non visible spans out.
|
||||
.filter_map(move |(span, span_width)| {
|
||||
// Ignore spans that are completely before the offset. Decrement `span_skip_width` by
|
||||
// the span width until we find a span that is partially or completely visible.
|
||||
if skip_width >= span_width {
|
||||
skip_width = skip_width.saturating_sub(span_width);
|
||||
return None;
|
||||
}
|
||||
|
||||
// Apply the skip from the start of the span, not the end as the end will be trimmed
|
||||
// when rendering the span to the buffer.
|
||||
let available_width = span_width.saturating_sub(skip_width);
|
||||
skip_width = 0; // ensure the next span is rendered in full
|
||||
Some((span, span_width, available_width))
|
||||
})
|
||||
.map(|(span, span_width, available_width)| {
|
||||
if span_width <= available_width {
|
||||
// Span is fully visible. Clone here is fast as the underlying content is `Cow`.
|
||||
return (span.clone(), span_width, 0u16);
|
||||
}
|
||||
// Span is only partially visible. As the end is truncated by the area width, only
|
||||
// truncate the start of the span.
|
||||
let (content, actual_width) = span.content.unicode_truncate_start(available_width);
|
||||
|
||||
// When the first grapheme of the span was truncated, start rendering from a position
|
||||
// that takes that into account by indenting the start of the area
|
||||
let first_grapheme_offset = available_width.saturating_sub(actual_width);
|
||||
let first_grapheme_offset = u16::try_from(first_grapheme_offset).unwrap_or(u16::MAX);
|
||||
(
|
||||
Span::styled(content, span.style),
|
||||
actual_width,
|
||||
first_grapheme_offset,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
impl fmt::Display for Line<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
for span in &self.spans {
|
||||
write!(f, "{span}")?;
|
||||
}
|
||||
@@ -486,12 +656,31 @@ impl std::fmt::Display for Line<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Styled for Line<'a> {
|
||||
type Item = Self;
|
||||
|
||||
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 std::iter;
|
||||
|
||||
use rstest::{fixture, rstest};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[fixture]
|
||||
fn small_buf() -> Buffer {
|
||||
Buffer::empty(Rect::new(0, 0, 10, 1))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_str() {
|
||||
let line = Line::raw("test content");
|
||||
@@ -601,6 +790,16 @@ mod tests {
|
||||
assert_eq!(Style::reset(), line.style);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stylize() {
|
||||
assert_eq!(Line::default().green().style, Color::Green.into());
|
||||
assert_eq!(
|
||||
Line::default().on_green().style,
|
||||
Style::new().bg(Color::Green)
|
||||
);
|
||||
assert_eq!(Line::default().italic().style, Modifier::ITALIC.into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_string() {
|
||||
let s = String::from("Hello, world!");
|
||||
@@ -625,6 +824,32 @@ mod tests {
|
||||
assert_eq!(spans, line.spans);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_iter() {
|
||||
let line = Line::from_iter(vec!["Hello".blue(), " world!".green()]);
|
||||
assert_eq!(
|
||||
line.spans,
|
||||
vec![
|
||||
Span::styled("Hello", Style::new().blue()),
|
||||
Span::styled(" world!", Style::new().green()),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collect() {
|
||||
let line: Line = iter::once("Hello".blue())
|
||||
.chain(iter::once(" world!".green()))
|
||||
.collect();
|
||||
assert_eq!(
|
||||
line.spans,
|
||||
vec![
|
||||
Span::styled("Hello", Style::new().blue()),
|
||||
Span::styled(" world!", Style::new().green()),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_span() {
|
||||
let span = Span::styled("Hello, world!", Style::default().fg(Color::Yellow));
|
||||
@@ -694,76 +919,6 @@ mod tests {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn left_aligned() {
|
||||
let line = Line::from("Hello, world!").left_aligned();
|
||||
@@ -782,6 +937,342 @@ mod tests {
|
||||
assert_eq!(line.alignment, Some(Alignment::Right));
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn push_span() {
|
||||
let mut line = Line::from("A");
|
||||
line.push_span(Span::raw("B"));
|
||||
line.push_span("C");
|
||||
assert_eq!(
|
||||
line.spans,
|
||||
vec![Span::raw("A"), Span::raw("B"), Span::raw("C")]
|
||||
);
|
||||
}
|
||||
|
||||
mod widget {
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use super::*;
|
||||
use crate::buffer::Cell;
|
||||
|
||||
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);
|
||||
|
||||
#[fixture]
|
||||
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(["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_eq!(buf, expected);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn render_out_of_bounds(hello_world: Line<'static>, mut small_buf: Buffer) {
|
||||
let out_of_bounds = Rect::new(20, 20, 10, 1);
|
||||
hello_world.render(out_of_bounds, &mut small_buf);
|
||||
assert_eq!(small_buf, Buffer::empty(small_buf.area));
|
||||
}
|
||||
|
||||
#[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(["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_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);
|
||||
assert_eq!(buf, Buffer::with_lines(["Hello "]));
|
||||
}
|
||||
|
||||
#[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([" 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_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([" 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_eq!(buf, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_truncates_left() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 5, 1));
|
||||
Line::from("Hello world")
|
||||
.left_aligned()
|
||||
.render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["Hello"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_truncates_right() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 5, 1));
|
||||
Line::from("Hello world")
|
||||
.right_aligned()
|
||||
.render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["world"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_truncates_center() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 5, 1));
|
||||
Line::from("Hello world")
|
||||
.centered()
|
||||
.render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["lo wo"]));
|
||||
}
|
||||
|
||||
/// Part of a regression test for <https://github.com/ratatui-org/ratatui/issues/1032> which
|
||||
/// found panics with truncating lines that contained multi-byte characters.
|
||||
#[test]
|
||||
fn regression_1032() {
|
||||
let line = Line::from(
|
||||
"🦀 RFC8628 OAuth 2.0 Device Authorization GrantでCLIからGithubのaccess tokenを取得する"
|
||||
);
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 83, 1));
|
||||
line.render_ref(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines([
|
||||
"🦀 RFC8628 OAuth 2.0 Device Authorization GrantでCLIからGithubのaccess tokenを取得 "
|
||||
]));
|
||||
}
|
||||
|
||||
/// Documentary test to highlight the crab emoji width / length discrepancy
|
||||
///
|
||||
/// Part of a regression test for <https://github.com/ratatui-org/ratatui/issues/1032> which
|
||||
/// found panics with truncating lines that contained multi-byte characters.
|
||||
#[test]
|
||||
fn crab_emoji_width() {
|
||||
let crab = "🦀";
|
||||
assert_eq!(crab.len(), 4); // bytes
|
||||
assert_eq!(crab.chars().count(), 1);
|
||||
assert_eq!(crab.graphemes(true).count(), 1);
|
||||
assert_eq!(crab.width(), 2); // display width
|
||||
}
|
||||
|
||||
/// Part of a regression test for <https://github.com/ratatui-org/ratatui/issues/1032> which
|
||||
/// found panics with truncating lines that contained multi-byte characters.
|
||||
#[rstest]
|
||||
#[case::left_4(Alignment::Left, 4, "1234")]
|
||||
#[case::left_5(Alignment::Left, 5, "1234 ")]
|
||||
#[case::left_6(Alignment::Left, 6, "1234🦀")]
|
||||
#[case::left_7(Alignment::Left, 7, "1234🦀7")]
|
||||
#[case::right_4(Alignment::Right, 4, "7890")]
|
||||
#[case::right_5(Alignment::Right, 5, " 7890")]
|
||||
#[case::right_6(Alignment::Right, 6, "🦀7890")]
|
||||
#[case::right_7(Alignment::Right, 7, "4🦀7890")]
|
||||
fn render_truncates_emoji(
|
||||
#[case] alignment: Alignment,
|
||||
#[case] buf_width: u16,
|
||||
#[case] expected: &str,
|
||||
) {
|
||||
let line = Line::from("1234🦀7890").alignment(alignment);
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, buf_width, 1));
|
||||
line.render_ref(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines([expected]));
|
||||
}
|
||||
|
||||
/// Part of a regression test for <https://github.com/ratatui-org/ratatui/issues/1032> which
|
||||
/// found panics with truncating lines that contained multi-byte characters.
|
||||
///
|
||||
/// centering is tricky because there's an ambiguity about whether to take one more char
|
||||
/// from the left or the right when the line width is odd. This interacts with the width of
|
||||
/// the crab emoji, which is 2 characters wide by hitting the left or right side of the
|
||||
/// emoji.
|
||||
#[rstest]
|
||||
#[case::center_6_0(6, 0, "")]
|
||||
#[case::center_6_1(6, 1, " ")] // lef side of "🦀"
|
||||
#[case::center_6_2(6, 2, "🦀")]
|
||||
#[case::center_6_3(6, 3, "b🦀")]
|
||||
#[case::center_6_4(6, 4, "b🦀c")]
|
||||
#[case::center_7_0(7, 0, "")]
|
||||
#[case::center_7_1(7, 1, " ")] // right side of "🦀"
|
||||
#[case::center_7_2(7, 2, "🦀")]
|
||||
#[case::center_7_3(7, 3, "🦀c")]
|
||||
#[case::center_7_4(7, 4, "b🦀c")]
|
||||
#[case::center_8_0(8, 0, "")]
|
||||
#[case::center_8_1(8, 1, " ")] // right side of "🦀"
|
||||
#[case::center_8_2(8, 2, " c")] // right side of "🦀c"
|
||||
#[case::center_8_3(8, 3, "🦀c")]
|
||||
#[case::center_8_4(8, 4, "🦀cd")]
|
||||
#[case::center_8_5(8, 5, "b🦀cd")]
|
||||
#[case::center_9_0(9, 0, "")]
|
||||
#[case::center_9_1(9, 1, "c")]
|
||||
#[case::center_9_2(9, 2, " c")] // right side of "🦀c"
|
||||
#[case::center_9_3(9, 3, " cd")]
|
||||
#[case::center_9_4(9, 4, "🦀cd")]
|
||||
#[case::center_9_5(9, 5, "🦀cde")]
|
||||
#[case::center_9_6(9, 6, "b🦀cde")]
|
||||
fn render_truncates_emoji_center(
|
||||
#[case] line_width: u16,
|
||||
#[case] buf_width: u16,
|
||||
#[case] expected: &str,
|
||||
) {
|
||||
// because the crab emoji is 2 characters wide, it will can cause the centering tests
|
||||
// intersect with either the left or right part of the emoji, which causes the emoji to
|
||||
// be not rendered. Checking for four different widths of the line is enough to cover
|
||||
// all the possible cases.
|
||||
let value = match line_width {
|
||||
6 => "ab🦀cd",
|
||||
7 => "ab🦀cde",
|
||||
8 => "ab🦀cdef",
|
||||
9 => "ab🦀cdefg",
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let line = Line::from(value).centered();
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, buf_width, 1));
|
||||
line.render_ref(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines([expected]));
|
||||
}
|
||||
|
||||
/// Ensures the rendering also works away from the 0x0 position.
|
||||
///
|
||||
/// Particularly of note is that an emoji that is truncated will not overwrite the
|
||||
/// characters that are already in the buffer. This is inentional (consider how a line
|
||||
/// that is rendered on a border should not overwrite the border with a partial emoji).
|
||||
#[rstest]
|
||||
#[case::left(Alignment::Left, "XXa🦀bcXXX")]
|
||||
#[case::center(Alignment::Center, "XX🦀bc🦀XX")]
|
||||
#[case::right(Alignment::Right, "XXXbc🦀dXX")]
|
||||
fn render_truncates_away_from_0x0(#[case] alignment: Alignment, #[case] expected: &str) {
|
||||
let line = Line::from(vec![Span::raw("a🦀b"), Span::raw("c🦀d")]).alignment(alignment);
|
||||
// Fill buffer with stuff to ensure the output is indeed padded
|
||||
let mut buf = Buffer::filled(Rect::new(0, 0, 10, 1), Cell::default().set_symbol("X"));
|
||||
let area = Rect::new(2, 0, 6, 1);
|
||||
line.render_ref(area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines([expected]));
|
||||
}
|
||||
|
||||
/// When two spans are rendered after each other the first needs to be padded in accordance
|
||||
/// to the skipped unicode width. In this case the first crab does not fit at width 6 which
|
||||
/// takes a front white space.
|
||||
#[rstest]
|
||||
#[case::right_4(4, "c🦀d")]
|
||||
#[case::right_5(5, "bc🦀d")]
|
||||
#[case::right_6(6, "Xbc🦀d")]
|
||||
#[case::right_7(7, "🦀bc🦀d")]
|
||||
#[case::right_8(8, "a🦀bc🦀d")]
|
||||
fn render_right_aligned_multi_span(#[case] buf_width: u16, #[case] expected: &str) {
|
||||
let line = Line::from(vec![Span::raw("a🦀b"), Span::raw("c🦀d")]).right_aligned();
|
||||
let area = Rect::new(0, 0, buf_width, 1);
|
||||
// Fill buffer with stuff to ensure the output is indeed padded
|
||||
let mut buf = Buffer::filled(area, Cell::default().set_symbol("X"));
|
||||
line.render_ref(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines([expected]));
|
||||
}
|
||||
|
||||
/// Part of a regression test for <https://github.com/ratatui-org/ratatui/issues/1032> which
|
||||
/// found panics with truncating lines that contained multi-byte characters.
|
||||
///
|
||||
/// Flag emoji are actually two independent characters, so they can be truncated in the
|
||||
/// middle of the emoji. This test documents just the emoji part of the test.
|
||||
#[test]
|
||||
fn flag_emoji() {
|
||||
let str = "🇺🇸1234";
|
||||
assert_eq!(str.len(), 12); // flag is 4 bytes
|
||||
assert_eq!(str.chars().count(), 6); // flag is 2 chars
|
||||
assert_eq!(str.graphemes(true).count(), 5); // flag is 1 grapheme
|
||||
assert_eq!(str.width(), 6); // flag is 2 display width
|
||||
}
|
||||
|
||||
/// Part of a regression test for <https://github.com/ratatui-org/ratatui/issues/1032> which
|
||||
/// found panics with truncating lines that contained multi-byte characters.
|
||||
#[rstest]
|
||||
#[case::flag_1(1, " ")]
|
||||
#[case::flag_2(2, "🇺🇸")]
|
||||
#[case::flag_3(3, "🇺🇸1")]
|
||||
#[case::flag_4(4, "🇺🇸12")]
|
||||
#[case::flag_5(5, "🇺🇸123")]
|
||||
#[case::flag_6(6, "🇺🇸1234")]
|
||||
#[case::flag_7(7, "🇺🇸1234 ")]
|
||||
fn render_truncates_flag(#[case] buf_width: u16, #[case] expected: &str) {
|
||||
let line = Line::from("🇺🇸1234");
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, buf_width, 1));
|
||||
line.render_ref(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines([expected]));
|
||||
}
|
||||
|
||||
// Buffer width is `u16`. A line can be longer.
|
||||
#[rstest]
|
||||
#[case::left(Alignment::Left, "This is some content with a some")]
|
||||
#[case::right(Alignment::Right, "horribly long Line over u16::MAX")]
|
||||
fn render_truncates_very_long_line_of_many_spans(
|
||||
#[case] alignment: Alignment,
|
||||
#[case] expected: &str,
|
||||
) {
|
||||
let part = "This is some content with a somewhat long width to be repeated over and over again to create horribly long Line over u16::MAX";
|
||||
let min_width = usize::from(u16::MAX).saturating_add(1);
|
||||
|
||||
// width == len as only ASCII is used here
|
||||
let factor = min_width.div_ceil(part.len());
|
||||
|
||||
let line = Line::from(vec![Span::raw(part); factor]).alignment(alignment);
|
||||
|
||||
dbg!(line.width());
|
||||
assert!(line.width() >= min_width);
|
||||
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 32, 1));
|
||||
line.render_ref(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines([expected]));
|
||||
}
|
||||
|
||||
// Buffer width is `u16`. A single span inside a line can be longer.
|
||||
#[rstest]
|
||||
#[case::left(Alignment::Left, "This is some content with a some")]
|
||||
#[case::right(Alignment::Right, "horribly long Line over u16::MAX")]
|
||||
fn render_truncates_very_long_single_span_line(
|
||||
#[case] alignment: Alignment,
|
||||
#[case] expected: &str,
|
||||
) {
|
||||
let part = "This is some content with a somewhat long width to be repeated over and over again to create horribly long Line over u16::MAX";
|
||||
let min_width = usize::from(u16::MAX).saturating_add(1);
|
||||
|
||||
// width == len as only ASCII is used here
|
||||
let factor = min_width.div_ceil(part.len());
|
||||
|
||||
let line = Line::from(vec![Span::raw(part.repeat(factor))]).alignment(alignment);
|
||||
|
||||
dbg!(line.width());
|
||||
assert!(line.width() >= min_width);
|
||||
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 32, 1));
|
||||
line.render_ref(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines([expected]));
|
||||
}
|
||||
}
|
||||
|
||||
mod iterators {
|
||||
use super::*;
|
||||
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
fmt::{self, Debug, Display},
|
||||
};
|
||||
use std::{borrow::Cow, fmt};
|
||||
|
||||
use super::Text;
|
||||
|
||||
@@ -19,7 +16,7 @@ use super::Text;
|
||||
/// let password = Masked::new("12345", 'x');
|
||||
///
|
||||
/// Paragraph::new(password).render(buffer.area, &mut buffer);
|
||||
/// assert_eq!(buffer, Buffer::with_lines(vec!["xxxxx"]));
|
||||
/// assert_eq!(buffer, Buffer::with_lines(["xxxxx"]));
|
||||
/// ```
|
||||
#[derive(Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Masked<'a> {
|
||||
@@ -36,7 +33,7 @@ impl<'a> Masked<'a> {
|
||||
}
|
||||
|
||||
/// The character to use for masking.
|
||||
pub fn mask_char(&self) -> char {
|
||||
pub const fn mask_char(&self) -> char {
|
||||
self.mask_char
|
||||
}
|
||||
|
||||
@@ -46,40 +43,41 @@ impl<'a> Masked<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Masked<'_> {
|
||||
impl fmt::Debug for Masked<'_> {
|
||||
/// Debug representation of a masked string is the underlying string
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(&self.inner).map_err(|_| fmt::Error)
|
||||
// note that calling display instead of Debug here is intentional
|
||||
fmt::Display::fmt(&self.inner, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Masked<'_> {
|
||||
impl fmt::Display for Masked<'_> {
|
||||
/// Display representation of a masked string is the masked string
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(&self.value()).map_err(|_| fmt::Error)
|
||||
fmt::Display::fmt(&self.value(), f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Masked<'a>> for Cow<'a, str> {
|
||||
fn from(masked: &'a Masked) -> Cow<'a, str> {
|
||||
fn from(masked: &'a Masked) -> Self {
|
||||
masked.value()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Masked<'a>> for Cow<'a, str> {
|
||||
fn from(masked: Masked<'a>) -> Cow<'a, str> {
|
||||
fn from(masked: Masked<'a>) -> Self {
|
||||
masked.value()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Masked<'_>> for Text<'a> {
|
||||
fn from(masked: &'a Masked) -> Text<'a> {
|
||||
fn from(masked: &'a Masked) -> Self {
|
||||
Text::raw(masked.value())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Masked<'a>> for Text<'a> {
|
||||
fn from(masked: Masked<'a>) -> Text<'a> {
|
||||
fn from(masked: Masked<'a>) -> Self {
|
||||
Text::raw(masked.value())
|
||||
}
|
||||
}
|
||||
@@ -112,12 +110,14 @@ mod tests {
|
||||
fn debug() {
|
||||
let masked = Masked::new("12345", 'x');
|
||||
assert_eq!(format!("{masked:?}"), "12345");
|
||||
assert_eq!(format!("{masked:.3?}"), "123", "Debug truncates");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display() {
|
||||
let masked = Masked::new("12345", 'x');
|
||||
assert_eq!(format!("{masked}"), "xxxxx");
|
||||
assert_eq!(format!("{masked:.3}"), "xxx", "Display truncates");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
152
src/text/span.rs
152
src/text/span.rs
@@ -1,10 +1,10 @@
|
||||
use std::{borrow::Cow, fmt::Debug};
|
||||
use std::{borrow::Cow, fmt};
|
||||
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use super::StyledGrapheme;
|
||||
use crate::{prelude::*, widgets::Widget};
|
||||
use crate::prelude::*;
|
||||
|
||||
/// Represents a part of a line that is contiguous and where all characters share the same style.
|
||||
///
|
||||
@@ -107,11 +107,11 @@ impl<'a> Span<'a> {
|
||||
/// Span::raw("test content");
|
||||
/// Span::raw(String::from("test content"));
|
||||
/// ```
|
||||
pub fn raw<T>(content: T) -> Span<'a>
|
||||
pub fn raw<T>(content: T) -> Self
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
{
|
||||
Span {
|
||||
Self {
|
||||
content: content.into(),
|
||||
style: Style::default(),
|
||||
}
|
||||
@@ -133,12 +133,12 @@ impl<'a> Span<'a> {
|
||||
/// Span::styled("test content", style);
|
||||
/// Span::styled(String::from("test content"), style);
|
||||
/// ```
|
||||
pub fn styled<T, S>(content: T, style: S) -> Span<'a>
|
||||
pub fn styled<T, S>(content: T, style: S) -> Self
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
S: Into<Style>,
|
||||
{
|
||||
Span {
|
||||
Self {
|
||||
content: content.into(),
|
||||
style: style.into(),
|
||||
}
|
||||
@@ -282,36 +282,56 @@ impl<'a> Span<'a> {
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let l = "Test Content".green().italic().to_left_aligned_line();
|
||||
/// let line = "Test Content".green().italic().into_left_aligned_line();
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn to_left_aligned_line(self) -> Line<'a> {
|
||||
pub fn into_left_aligned_line(self) -> Line<'a> {
|
||||
Line::from(self).left_aligned()
|
||||
}
|
||||
|
||||
#[allow(clippy::wrong_self_convention)]
|
||||
#[deprecated = "use into_left_aligned_line"]
|
||||
pub fn to_left_aligned_line(self) -> Line<'a> {
|
||||
self.into_left_aligned_line()
|
||||
}
|
||||
|
||||
/// Converts this Span into a center-aligned [`Line`]
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let l = "Test Content".green().italic().to_centered_line();
|
||||
/// let line = "Test Content".green().italic().into_centered_line();
|
||||
/// ```
|
||||
pub fn to_centered_line(self) -> Line<'a> {
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn into_centered_line(self) -> Line<'a> {
|
||||
Line::from(self).centered()
|
||||
}
|
||||
|
||||
#[allow(clippy::wrong_self_convention)]
|
||||
#[deprecated = "use into_centered_line"]
|
||||
pub fn to_centered_line(self) -> Line<'a> {
|
||||
self.into_centered_line()
|
||||
}
|
||||
|
||||
/// Converts this Span into a right-aligned [`Line`]
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let l = "Test Content".green().italic().to_right_aligned_line();
|
||||
/// let line = "Test Content".green().italic().into_right_aligned_line();
|
||||
/// ```
|
||||
pub fn to_right_aligned_line(self) -> Line<'a> {
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn into_right_aligned_line(self) -> Line<'a> {
|
||||
Line::from(self).right_aligned()
|
||||
}
|
||||
|
||||
#[allow(clippy::wrong_self_convention)]
|
||||
#[deprecated = "use into_right_aligned_line"]
|
||||
pub fn to_right_aligned_line(self) -> Line<'a> {
|
||||
self.into_right_aligned_line()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> From<T> for Span<'a>
|
||||
@@ -324,7 +344,7 @@ where
|
||||
}
|
||||
|
||||
impl<'a> Styled for Span<'a> {
|
||||
type Item = Span<'a>;
|
||||
type Item = Self;
|
||||
|
||||
fn style(&self) -> Style {
|
||||
self.style
|
||||
@@ -343,6 +363,7 @@ impl Widget for Span<'_> {
|
||||
|
||||
impl WidgetRef for Span<'_> {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
let area = area.intersection(buf.area);
|
||||
let Rect {
|
||||
x: mut current_x,
|
||||
y,
|
||||
@@ -374,16 +395,23 @@ impl WidgetRef for Span<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Span<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", &self.content)
|
||||
impl fmt::Display for Span<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
fmt::Display::fmt(&self.content, f)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rstest::fixture;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[fixture]
|
||||
fn small_buf() -> Buffer {
|
||||
Buffer::empty(Rect::new(0, 0, 10, 1))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default() {
|
||||
let span = Span::default();
|
||||
@@ -501,20 +529,42 @@ mod tests {
|
||||
#[test]
|
||||
fn display_span() {
|
||||
let span = Span::raw("test content");
|
||||
|
||||
assert_eq!(format!("{span}"), "test content");
|
||||
assert_eq!(format!("{span:.4}"), "test");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_styled_span() {
|
||||
let stylized_span = Span::styled("stylized test content", Style::new().green());
|
||||
|
||||
assert_eq!(format!("{stylized_span}"), "stylized test content");
|
||||
assert_eq!(format!("{stylized_span:.8}"), "stylized");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn left_aligned() {
|
||||
let span = Span::styled("Test Content", Style::new().green().italic());
|
||||
let line = span.into_left_aligned_line();
|
||||
assert_eq!(line.alignment, Some(Alignment::Left));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn centered() {
|
||||
let span = Span::styled("Test Content", Style::new().green().italic());
|
||||
let line = span.into_centered_line();
|
||||
assert_eq!(line.alignment, Some(Alignment::Center));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn right_aligned() {
|
||||
let span = Span::styled("Test Content", Style::new().green().italic());
|
||||
let line = span.into_right_aligned_line();
|
||||
assert_eq!(line.alignment, Some(Alignment::Right));
|
||||
}
|
||||
|
||||
mod widget {
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
use crate::{assert_buffer_eq, style::Stylize};
|
||||
|
||||
#[test]
|
||||
fn render() {
|
||||
@@ -522,12 +572,18 @@ mod tests {
|
||||
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![
|
||||
let expected = Buffer::with_lines([Line::from(vec![
|
||||
"test content".green().on_yellow(),
|
||||
" ".into(),
|
||||
])]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
assert_eq!(buf, expected);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn render_out_of_bounds(mut small_buf: Buffer) {
|
||||
let out_of_bounds = Rect::new(20, 20, 10, 1);
|
||||
Span::raw("Hello, World!").render(out_of_bounds, &mut small_buf);
|
||||
assert_eq!(small_buf, Buffer::empty(small_buf.area));
|
||||
}
|
||||
|
||||
/// When the content of the span is longer than the area passed to render, the content
|
||||
@@ -540,10 +596,11 @@ mod tests {
|
||||
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);
|
||||
let expected = Buffer::with_lines([Line::from(vec![
|
||||
"test ".green().on_yellow(),
|
||||
" ".into(),
|
||||
])]);
|
||||
assert_eq!(buf, expected);
|
||||
}
|
||||
|
||||
/// When there is already a style set on the buffer, the style of the span should be
|
||||
@@ -555,12 +612,11 @@ mod tests {
|
||||
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![
|
||||
let expected = Buffer::with_lines([Line::from(vec![
|
||||
"test content".green().on_yellow().italic(),
|
||||
" ".italic(),
|
||||
])]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
assert_eq!(buf, expected);
|
||||
}
|
||||
|
||||
/// When the span contains a multi-width grapheme, the grapheme will ensure that the cells
|
||||
@@ -571,12 +627,11 @@ mod tests {
|
||||
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);
|
||||
let expected = Buffer::with_lines(["test 😃 content".green().on_yellow()]);
|
||||
assert_eq!(buf, expected);
|
||||
}
|
||||
|
||||
/// When the span contains a multi-width grapheme that does not fit in the area passed to
|
||||
@@ -589,11 +644,9 @@ mod tests {
|
||||
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);
|
||||
let expected =
|
||||
Buffer::with_lines([Line::from(vec!["test ".green().on_yellow(), " ".into()])]);
|
||||
assert_eq!(buf, expected);
|
||||
}
|
||||
|
||||
/// When the area passed to render overflows the buffer, the content should be truncated
|
||||
@@ -605,32 +658,11 @@ mod tests {
|
||||
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![
|
||||
let expected = Buffer::with_lines([Line::from(vec![
|
||||
" ".into(),
|
||||
"test ".green().on_yellow(),
|
||||
])]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
assert_eq!(buf, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn left_aligned() {
|
||||
let span = Span::styled("Test Content", Style::new().green().italic());
|
||||
let line = span.to_left_aligned_line();
|
||||
assert_eq!(line.alignment, Some(Alignment::Left));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn centered() {
|
||||
let span = Span::styled("Test Content", Style::new().green().italic());
|
||||
let line = span.to_centered_line();
|
||||
assert_eq!(line.alignment, Some(Alignment::Center));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn right_aligned() {
|
||||
let span = Span::styled("Test Content", Style::new().green().italic());
|
||||
let line = span.to_right_aligned_line();
|
||||
assert_eq!(line.alignment, Some(Alignment::Right));
|
||||
}
|
||||
}
|
||||
|
||||
514
src/text/text.rs
514
src/text/text.rs
@@ -1,34 +1,32 @@
|
||||
#![warn(missing_docs)]
|
||||
use std::borrow::Cow;
|
||||
use std::{borrow::Cow, fmt};
|
||||
|
||||
use itertools::{Itertools, Position};
|
||||
|
||||
use crate::{prelude::*, widgets::Widget};
|
||||
use crate::prelude::*;
|
||||
|
||||
/// A string split over multiple lines where each line is composed of several clusters, each with
|
||||
/// their own style.
|
||||
/// A string split over one or more lines.
|
||||
///
|
||||
/// 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.
|
||||
/// [`Text`] is used wherever text is displayed in the terminal and represents one or more [`Line`]s
|
||||
/// of text. When a [`Text`] is rendered, each line is rendered as a single line of text from top to
|
||||
/// bottom of the area. The text can be styled and aligned.
|
||||
///
|
||||
/// # Constructor Methods
|
||||
///
|
||||
/// - [`Text::default`] creates a `Text` with empty content and the default style.
|
||||
/// - [`Text::raw`] creates a `Text` (potentially multiple lines) with no style.
|
||||
/// - [`Text::styled`] creates a `Text` (potentially multiple lines) with a style.
|
||||
/// - [`Text::default`] creates a `Text` with empty content and the default style.
|
||||
///
|
||||
/// # Conversion Methods
|
||||
///
|
||||
/// - [`Text::from`] creates a `Text` from a `String`.
|
||||
/// - [`Text::from`] creates a `Text` from a `&str`.
|
||||
/// - [`Text::from`] creates a `Text` from a `Cow<str>`.
|
||||
/// - [`Text::from`] creates a `Text` from a [`Span`].
|
||||
/// - [`Text::from`] creates a `Text` from a [`Line`].
|
||||
/// - [`Text::from`] creates a `Text` from a `Vec<Line>`.
|
||||
/// - [`Text::from_iter`] creates a `Text` from an iterator of items that can be converted into
|
||||
/// `Line`.
|
||||
///
|
||||
/// # Setter Methods
|
||||
///
|
||||
@@ -36,6 +34,15 @@ use crate::{prelude::*, widgets::Widget};
|
||||
///
|
||||
/// - [`Text::style`] sets the style of this `Text`.
|
||||
/// - [`Text::alignment`] sets the alignment for this `Text`.
|
||||
/// - [`Text::left_aligned`] sets the alignment to [`Alignment::Left`].
|
||||
/// - [`Text::centered`] sets the alignment to [`Alignment::Center`].
|
||||
/// - [`Text::right_aligned`] sets the alignment to [`Alignment::Right`].
|
||||
///
|
||||
/// # Iteration Methods
|
||||
///
|
||||
/// - [`Text::iter`] returns an iterator over the lines of the text.
|
||||
/// - [`Text::iter_mut`] returns an iterator that allows modifying each line.
|
||||
/// - [`Text::into_iter`] returns an iterator over the lines of the text.
|
||||
///
|
||||
/// # Other Methods
|
||||
///
|
||||
@@ -43,29 +50,121 @@ use crate::{prelude::*, widgets::Widget};
|
||||
/// - [`Text::height`] returns the height.
|
||||
/// - [`Text::patch_style`] patches the style of this `Text`, adding modifiers from the given style.
|
||||
/// - [`Text::reset_style`] resets the style of the `Text`.
|
||||
/// - [`Text::push_line`] adds a line to the text.
|
||||
/// - [`Text::push_span`] adds a span to the last line of the text.
|
||||
///
|
||||
/// [`Paragraph`]: crate::widgets::Paragraph
|
||||
/// [`Widget`]: crate::widgets::Widget
|
||||
/// # Examples
|
||||
///
|
||||
/// ## Creating Text
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// ```rust
|
||||
/// use std::{borrow::Cow, iter};
|
||||
///
|
||||
/// use ratatui::prelude::*;
|
||||
///
|
||||
/// let style = Style::default()
|
||||
/// .fg(Color::Yellow)
|
||||
/// .add_modifier(Modifier::ITALIC);
|
||||
/// let style = Style::new().yellow().italic();
|
||||
/// let text = Text::raw("The first line\nThe second line").style(style);
|
||||
/// let text = Text::styled("The first line\nThe second line", style);
|
||||
/// let text = Text::styled(
|
||||
/// "The first line\nThe second line",
|
||||
/// (Color::Yellow, Modifier::ITALIC),
|
||||
/// );
|
||||
///
|
||||
/// // An initial two lines of `Text` built from a `&str`
|
||||
/// let mut text = Text::from("The first line\nThe second line");
|
||||
/// assert_eq!(2, text.height());
|
||||
/// let text = Text::from("The first line\nThe second line");
|
||||
/// let text = Text::from(String::from("The first line\nThe second line"));
|
||||
/// let text = Text::from(Cow::Borrowed("The first line\nThe second line"));
|
||||
/// let text = Text::from(Span::styled("The first line\nThe second line", style));
|
||||
/// let text = Text::from(Line::from("The first line"));
|
||||
/// let text = Text::from(vec![
|
||||
/// Line::from("The first line"),
|
||||
/// Line::from("The second line"),
|
||||
/// ]);
|
||||
/// let text = Text::from_iter(iter::once("The first line").chain(iter::once("The second line")));
|
||||
///
|
||||
/// // Adding two more unstyled lines
|
||||
/// text.extend(Text::raw("These are two\nmore lines!"));
|
||||
/// assert_eq!(4, text.height());
|
||||
///
|
||||
/// // Adding a final two styled lines
|
||||
/// text.extend(Text::styled("Some more lines\nnow with more style!", style));
|
||||
/// assert_eq!(6, text.height());
|
||||
/// let mut text = Text::default();
|
||||
/// text.extend(vec![
|
||||
/// Line::from("The first line"),
|
||||
/// Line::from("The second line"),
|
||||
/// ]);
|
||||
/// text.extend(Text::from("The third line\nThe fourth line"));
|
||||
/// ```
|
||||
///
|
||||
/// ## Styling Text
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let text = Text::from("The first line\nThe second line").style(Style::new().yellow().italic());
|
||||
/// let text = Text::from("The first line\nThe second line")
|
||||
/// .yellow()
|
||||
/// .italic();
|
||||
/// let text = Text::from(vec![
|
||||
/// Line::from("The first line").yellow(),
|
||||
/// Line::from("The second line").yellow(),
|
||||
/// ])
|
||||
/// .italic();
|
||||
/// ```
|
||||
///
|
||||
/// ## Aligning Text
|
||||
/// The text's [`Alignment`] can be set using [`Text::alignment`] or the related helper methods.
|
||||
/// Lines composing the text can also be individually aligned with [`Line::alignment`].
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let text = Text::from("The first line\nThe second line").alignment(Alignment::Right);
|
||||
/// let text = Text::from("The first line\nThe second line").right_aligned();
|
||||
/// let text = Text::from(vec![
|
||||
/// Line::from("The first line").left_aligned(),
|
||||
/// Line::from("The second line").right_aligned(),
|
||||
/// Line::from("The third line"),
|
||||
/// ])
|
||||
/// .centered();
|
||||
/// ```
|
||||
///
|
||||
/// ## Rendering Text
|
||||
/// `Text` implements the [`Widget`] trait, which means it can be rendered to a [`Buffer`] or to a
|
||||
/// [`Frame`].
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// // within another widget's `render` method:
|
||||
/// # fn render(area: Rect, buf: &mut Buffer) {
|
||||
/// let text = Text::from("The first line\nThe second line");
|
||||
/// text.render(area, buf);
|
||||
/// # }
|
||||
///
|
||||
/// // within a terminal.draw closure:
|
||||
/// # fn draw(frame: &mut Frame, area: Rect) {
|
||||
/// let text = Text::from("The first line\nThe second line");
|
||||
/// frame.render_widget(text, area);
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// ## Rendering Text with a Paragraph Widget
|
||||
///
|
||||
/// Usually apps will use the [`Paragraph`] widget instead of rendering a `Text` directly as it
|
||||
/// provides more functionality.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// # fn render(area: Rect, buf: &mut Buffer) {
|
||||
/// let text = Text::from("The first line\nThe second line");
|
||||
/// let paragraph = Paragraph::new(text)
|
||||
/// .wrap(Wrap { trim: true })
|
||||
/// .scroll((1, 1))
|
||||
/// .render(area, buf);
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// [`Paragraph`]: crate::widgets::Paragraph
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Text<'a> {
|
||||
/// The lines that make up this piece of text.
|
||||
@@ -86,7 +185,7 @@ impl<'a> Text<'a> {
|
||||
/// Text::raw("The first line\nThe second line");
|
||||
/// Text::raw(String::from("The first line\nThe second line"));
|
||||
/// ```
|
||||
pub fn raw<T>(content: T) -> Text<'a>
|
||||
pub fn raw<T>(content: T) -> Self
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
{
|
||||
@@ -96,8 +195,7 @@ impl<'a> Text<'a> {
|
||||
Cow::Owned(s) if s.is_empty() => vec![Line::from("")],
|
||||
Cow::Owned(s) => s.lines().map(|l| Line::from(l.to_owned())).collect(),
|
||||
};
|
||||
|
||||
Text::from(lines)
|
||||
Self::from(lines)
|
||||
}
|
||||
|
||||
/// Create some text (potentially multiple lines) with a style.
|
||||
@@ -115,12 +213,12 @@ impl<'a> Text<'a> {
|
||||
/// Text::styled("The first line\nThe second line", style);
|
||||
/// Text::styled(String::from("The first line\nThe second line"), style);
|
||||
/// ```
|
||||
pub fn styled<T, S>(content: T, style: S) -> Text<'a>
|
||||
pub fn styled<T, S>(content: T, style: S) -> Self
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
S: Into<Style>,
|
||||
{
|
||||
Text::raw(content).patch_style(style)
|
||||
Self::raw(content).patch_style(style)
|
||||
}
|
||||
|
||||
/// Returns the max width of all the lines.
|
||||
@@ -345,6 +443,46 @@ impl<'a> Text<'a> {
|
||||
pub fn iter_mut(&mut self) -> std::slice::IterMut<Line<'a>> {
|
||||
self.lines.iter_mut()
|
||||
}
|
||||
|
||||
/// Adds a line to the text.
|
||||
///
|
||||
/// `line` can be any type that can be converted into a `Line`. For example, you can pass a
|
||||
/// `&str`, a `String`, a `Span`, or a `Line`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let mut text = Text::from("Hello, world!");
|
||||
/// text.push_line(Line::from("How are you?"));
|
||||
/// text.push_line(Span::from("How are you?"));
|
||||
/// text.push_line("How are you?");
|
||||
/// ```
|
||||
pub fn push_line<T: Into<Line<'a>>>(&mut self, line: T) {
|
||||
self.lines.push(line.into());
|
||||
}
|
||||
|
||||
/// Adds a span to the last line of the text.
|
||||
///
|
||||
/// `span` can be any type that is convertible into a `Span`. For example, you can pass a
|
||||
/// `&str`, a `String`, or a `Span`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let mut text = Text::from("Hello, world!");
|
||||
/// text.push_span(Span::from("How are you?"));
|
||||
/// text.push_span("How are you?");
|
||||
/// ```
|
||||
pub fn push_span<T: Into<Span<'a>>>(&mut self, span: T) {
|
||||
let span = span.into();
|
||||
if let Some(last) = self.lines.last_mut() {
|
||||
last.push_span(span);
|
||||
} else {
|
||||
self.lines.push(Line::from(span));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for Text<'a> {
|
||||
@@ -375,26 +513,26 @@ impl<'a> IntoIterator for &'a mut Text<'a> {
|
||||
}
|
||||
|
||||
impl<'a> From<String> for Text<'a> {
|
||||
fn from(s: String) -> Text<'a> {
|
||||
Text::raw(s)
|
||||
fn from(s: String) -> Self {
|
||||
Self::raw(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for Text<'a> {
|
||||
fn from(s: &'a str) -> Text<'a> {
|
||||
Text::raw(s)
|
||||
fn from(s: &'a str) -> Self {
|
||||
Self::raw(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Cow<'a, str>> for Text<'a> {
|
||||
fn from(s: Cow<'a, str>) -> Text<'a> {
|
||||
Text::raw(s)
|
||||
fn from(s: Cow<'a, str>) -> Self {
|
||||
Self::raw(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Span<'a>> for Text<'a> {
|
||||
fn from(span: Span<'a>) -> Text<'a> {
|
||||
Text {
|
||||
fn from(span: Span<'a>) -> Self {
|
||||
Self {
|
||||
lines: vec![Line::from(span)],
|
||||
..Default::default()
|
||||
}
|
||||
@@ -402,8 +540,8 @@ impl<'a> From<Span<'a>> for Text<'a> {
|
||||
}
|
||||
|
||||
impl<'a> From<Line<'a>> for Text<'a> {
|
||||
fn from(line: Line<'a>) -> Text<'a> {
|
||||
Text {
|
||||
fn from(line: Line<'a>) -> Self {
|
||||
Self {
|
||||
lines: vec![line],
|
||||
..Default::default()
|
||||
}
|
||||
@@ -411,8 +549,21 @@ impl<'a> From<Line<'a>> for Text<'a> {
|
||||
}
|
||||
|
||||
impl<'a> From<Vec<Line<'a>>> for Text<'a> {
|
||||
fn from(lines: Vec<Line<'a>>) -> Text<'a> {
|
||||
Text {
|
||||
fn from(lines: Vec<Line<'a>>) -> Self {
|
||||
Self {
|
||||
lines,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> FromIterator<T> for Text<'a>
|
||||
where
|
||||
T: Into<Line<'a>>,
|
||||
{
|
||||
fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
|
||||
let lines = iter.into_iter().map(Into::into).collect();
|
||||
Self {
|
||||
lines,
|
||||
..Default::default()
|
||||
}
|
||||
@@ -429,8 +580,8 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Text<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
impl fmt::Display for Text<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
for (position, line) in self.iter().with_position() {
|
||||
if position == Position::Last {
|
||||
write!(f, "{line}")?;
|
||||
@@ -450,6 +601,7 @@ impl Widget for Text<'_> {
|
||||
|
||||
impl WidgetRef for Text<'_> {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
let area = area.intersection(buf.area);
|
||||
buf.set_style(area, self.style);
|
||||
for (line, row) in self.iter().zip(area.rows()) {
|
||||
let line_width = line.width() as u16;
|
||||
@@ -473,7 +625,7 @@ impl WidgetRef for Text<'_> {
|
||||
}
|
||||
|
||||
impl<'a> Styled for Text<'a> {
|
||||
type Item = Text<'a>;
|
||||
type Item = Self;
|
||||
|
||||
fn style(&self) -> Style {
|
||||
self.style
|
||||
@@ -486,10 +638,16 @@ impl<'a> Styled for Text<'a> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::iter;
|
||||
|
||||
use rstest::{fixture, rstest};
|
||||
|
||||
use super::*;
|
||||
use crate::style::Stylize;
|
||||
|
||||
#[fixture]
|
||||
fn small_buf() -> Buffer {
|
||||
Buffer::empty(Rect::new(0, 0, 10, 1))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw() {
|
||||
@@ -601,6 +759,26 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_iterator() {
|
||||
let text = Text::from_iter(vec!["The first line", "The second line"]);
|
||||
assert_eq!(
|
||||
text.lines,
|
||||
vec![Line::from("The first line"), Line::from("The second line")]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collect() {
|
||||
let text: Text = iter::once("The first line")
|
||||
.chain(iter::once("The second line"))
|
||||
.collect();
|
||||
assert_eq!(
|
||||
text.lines,
|
||||
vec![Line::from("The first line"), Line::from("The second line")]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn into_iter() {
|
||||
let text = Text::from("The first line\nThe second line");
|
||||
@@ -718,103 +896,6 @@ mod tests {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn left_aligned() {
|
||||
let text = Text::from("Hello, world!").left_aligned();
|
||||
@@ -833,6 +914,133 @@ mod tests {
|
||||
assert_eq!(text.alignment, Some(Alignment::Right));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn push_line() {
|
||||
let mut text = Text::from("A");
|
||||
text.push_line(Line::from("B"));
|
||||
text.push_line(Span::from("C"));
|
||||
text.push_line("D");
|
||||
assert_eq!(
|
||||
text.lines,
|
||||
vec![
|
||||
Line::raw("A"),
|
||||
Line::raw("B"),
|
||||
Line::raw("C"),
|
||||
Line::raw("D")
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn push_line_empty() {
|
||||
let mut text = Text::default();
|
||||
text.push_line(Line::from("Hello, world!"));
|
||||
assert_eq!(text.lines, vec![Line::from("Hello, world!")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn push_span() {
|
||||
let mut text = Text::from("A");
|
||||
text.push_span(Span::raw("B"));
|
||||
text.push_span("C");
|
||||
assert_eq!(
|
||||
text.lines,
|
||||
vec![Line::from(vec![
|
||||
Span::raw("A"),
|
||||
Span::raw("B"),
|
||||
Span::raw("C")
|
||||
])],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn push_span_empty() {
|
||||
let mut text = Text::default();
|
||||
text.push_span(Span::raw("Hello, world!"));
|
||||
assert_eq!(text.lines, vec![Line::from(Span::raw("Hello, world!"))],);
|
||||
}
|
||||
|
||||
mod widget {
|
||||
use super::*;
|
||||
|
||||
#[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);
|
||||
assert_eq!(buf, Buffer::with_lines(["foo "]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn render_out_of_bounds(mut small_buf: Buffer) {
|
||||
let out_of_bounds_area = Rect::new(20, 20, 10, 1);
|
||||
Text::from("Hello, world!").render(out_of_bounds_area, &mut small_buf);
|
||||
assert_eq!(small_buf, Buffer::empty(small_buf.area));
|
||||
}
|
||||
|
||||
#[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);
|
||||
assert_eq!(buf, Buffer::with_lines([" foo"]));
|
||||
}
|
||||
|
||||
#[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);
|
||||
assert_eq!(buf, Buffer::with_lines([" foo "]));
|
||||
}
|
||||
|
||||
#[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);
|
||||
assert_eq!(buf, Buffer::with_lines([" foo "]));
|
||||
}
|
||||
|
||||
#[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);
|
||||
assert_eq!(buf, Buffer::with_lines([" foo", " bar "]));
|
||||
}
|
||||
|
||||
#[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(["foo "]);
|
||||
expected.set_style(Rect::new(0, 0, 3, 1), Style::new().bg(Color::Blue));
|
||||
assert_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(["foo "]);
|
||||
expected.set_style(Rect::new(0, 0, 3, 1), Style::new().bg(Color::Blue));
|
||||
assert_eq!(buf, expected);
|
||||
}
|
||||
}
|
||||
|
||||
mod iterators {
|
||||
use super::*;
|
||||
|
||||
|
||||
111
src/widgets.rs
111
src/widgets.rs
@@ -1,3 +1,4 @@
|
||||
#![warn(missing_docs)]
|
||||
//! `widgets` is a collection of types that implement [`Widget`] or [`StatefulWidget`] or both.
|
||||
//!
|
||||
//! Widgets are created for each frame as they are consumed after rendered.
|
||||
@@ -69,6 +70,7 @@ use crate::{buffer::Buffer, layout::Rect};
|
||||
/// used where backwards compatibility is required (all the internal widgets use this approach).
|
||||
///
|
||||
/// A blanket implementation of `Widget` for `&W` where `W` implements `WidgetRef` is provided.
|
||||
/// Widget is also implemented for `&str` and `String` types.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
@@ -215,7 +217,12 @@ pub trait Widget {
|
||||
/// }
|
||||
/// ```
|
||||
pub trait StatefulWidget {
|
||||
/// State associated with the stateful widget.
|
||||
///
|
||||
/// If you don't need this then you probably want to implement [`Widget`] instead.
|
||||
type State;
|
||||
/// Draws the current state of the widget in the given buffer. That is the only method required
|
||||
/// to implement a custom stateful widget.
|
||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State);
|
||||
}
|
||||
|
||||
@@ -290,6 +297,8 @@ pub trait StatefulWidget {
|
||||
/// ```
|
||||
#[stability::unstable(feature = "widget-ref")]
|
||||
pub trait WidgetRef {
|
||||
/// Draws the current state of the widget in the given buffer. That is the only method required
|
||||
/// to implement a custom widget.
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer);
|
||||
}
|
||||
|
||||
@@ -346,7 +355,7 @@ impl<W: WidgetRef> WidgetRef for Option<W> {
|
||||
/// to a stateful widget and render it later. It also allows you to render boxed stateful widgets.
|
||||
///
|
||||
/// This trait was introduced in Ratatui 0.26.0 and is implemented for all the internal stateful
|
||||
/// widgets. Implemetors should prefer to implement this over the `StatefulWidget` trait and add an
|
||||
/// widgets. Implementors should prefer to implement this over the `StatefulWidget` trait and add an
|
||||
/// implementation of `StatefulWidget` that calls `StatefulWidgetRef::render_ref` where backwards
|
||||
/// compatibility is required.
|
||||
///
|
||||
@@ -385,7 +394,12 @@ impl<W: WidgetRef> WidgetRef for Option<W> {
|
||||
/// ```
|
||||
#[stability::unstable(feature = "widget-ref")]
|
||||
pub trait StatefulWidgetRef {
|
||||
/// State associated with the stateful widget.
|
||||
///
|
||||
/// If you don't need this then you probably want to implement [`WidgetRef`] instead.
|
||||
type State;
|
||||
/// Draws the current state of the widget in the given buffer. That is the only method required
|
||||
/// to implement a custom stateful widget.
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State);
|
||||
}
|
||||
|
||||
@@ -404,6 +418,51 @@ pub trait StatefulWidgetRef {
|
||||
// }
|
||||
// }
|
||||
|
||||
/// Renders a string slice as a widget.
|
||||
///
|
||||
/// This implementation allows a string slice (`&str`) to act as a widget, meaning it can be drawn
|
||||
/// onto a [`Buffer`] in a specified [`Rect`]. The slice represents a static string which can be
|
||||
/// rendered by reference, thereby avoiding the need for string cloning or ownership transfer when
|
||||
/// drawing the text to the screen.
|
||||
impl Widget for &str {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.render_ref(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides the ability to render a string slice by reference.
|
||||
///
|
||||
/// This trait implementation ensures that a string slice, which is an immutable view over a
|
||||
/// `String`, can be drawn on demand without requiring ownership of the string itself. It utilizes
|
||||
/// the default text style when rendering onto the provided [`Buffer`] at the position defined by
|
||||
/// [`Rect`].
|
||||
impl WidgetRef for &str {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
buf.set_string(area.x, area.y, self, crate::style::Style::default());
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders a `String` object as a widget.
|
||||
///
|
||||
/// This implementation enables an owned `String` to be treated as a widget, which can be rendered
|
||||
/// on a [`Buffer`] within the bounds of a given [`Rect`].
|
||||
impl Widget for String {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.render_ref(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides the ability to render a `String` by reference.
|
||||
///
|
||||
/// This trait allows for a `String` to be rendered onto the [`Buffer`], similarly using the default
|
||||
/// style settings. It ensures that an owned `String` can be rendered efficiently by reference,
|
||||
/// without the need to give up ownership of the underlying text.
|
||||
impl WidgetRef for String {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
buf.set_string(area.x, area.y, self, crate::style::Style::default());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rstest::{fixture, rstest};
|
||||
@@ -449,7 +508,7 @@ mod tests {
|
||||
impl StatefulWidgetRef for PersonalGreeting {
|
||||
type State = String;
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||
Line::from(format!("Hello {}", state)).render(area, buf);
|
||||
Line::from(format!("Hello {state}")).render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -548,4 +607,52 @@ mod tests {
|
||||
widget.render_ref(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines([" "]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn str_render(mut buf: Buffer) {
|
||||
"hello world".render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["hello world "]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn str_render_ref(mut buf: Buffer) {
|
||||
"hello world".render_ref(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["hello world "]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn str_option_render(mut buf: Buffer) {
|
||||
Some("hello world").render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["hello world "]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn str_option_render_ref(mut buf: Buffer) {
|
||||
Some("hello world").render_ref(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["hello world "]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn string_render(mut buf: Buffer) {
|
||||
String::from("hello world").render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["hello world "]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn string_render_ref(mut buf: Buffer) {
|
||||
String::from("hello world").render_ref(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["hello world "]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn string_option_render(mut buf: Buffer) {
|
||||
Some(String::from("hello world")).render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["hello world "]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn string_option_render_ref(mut buf: Buffer) {
|
||||
Some(String::from("hello world")).render_ref(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["hello world "]));
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user