Compare commits
82 Commits
v0.24.0
...
layout-hel
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84243c7ce8 | ||
|
|
6a50f2085e | ||
|
|
beaa2bf58d | ||
|
|
50374b2456 | ||
|
|
49df5d4626 | ||
|
|
7ab12ed8ce | ||
|
|
b459228e26 | ||
|
|
8f56fabcdd | ||
|
|
a62632a947 | ||
|
|
f025d2bfa2 | ||
|
|
63645333d6 | ||
|
|
5d410c6895 | ||
|
|
8d77b734bb | ||
|
|
9574198958 | ||
|
|
ee54493163 | ||
|
|
c977293f14 | ||
|
|
b0ed658970 | ||
|
|
37c183636b | ||
|
|
e67d3c64e0 | ||
|
|
4f2db82a77 | ||
|
|
d6b851301e | ||
|
|
b7a479392e | ||
|
|
e1cc849554 | ||
|
|
7f5884829c | ||
|
|
a15c3b2660 | ||
|
|
41c44a4af6 | ||
|
|
1b8b6261e2 | ||
|
|
5bf4f52119 | ||
|
|
f4c8de041d | ||
|
|
910ad00059 | ||
|
|
b282a06932 | ||
|
|
b8f71c0d6e | ||
|
|
113b4b7a4e | ||
|
|
b82451fb33 | ||
|
|
4be18aba8b | ||
|
|
ebf1f42942 | ||
|
|
2169a0da01 | ||
|
|
d118565ef6 | ||
|
|
aaeba2709c | ||
|
|
d19b266e0e | ||
|
|
f767ea7d37 | ||
|
|
0576a8aa32 | ||
|
|
03401cd46e | ||
|
|
f69d57c3b5 | ||
|
|
2a87251152 | ||
|
|
aef495604c | ||
|
|
8bfd6661e2 | ||
|
|
3ec4e24d00 | ||
|
|
7ced7c0aa3 | ||
|
|
dd22e721e3 | ||
|
|
4424637af2 | ||
|
|
37c70dbb8e | ||
|
|
91c67eb100 | ||
|
|
e49385b78c | ||
|
|
6b2efd0f6c | ||
|
|
34d099c99a | ||
|
|
987f7eed4c | ||
|
|
e4579f0db2 | ||
|
|
6a6e9dde9d | ||
|
|
28ac55bc62 | ||
|
|
458fa90362 | ||
|
|
56fc410105 | ||
|
|
753e246531 | ||
|
|
211160ca16 | ||
|
|
1229b96e42 | ||
|
|
fe632d70cb | ||
|
|
c862aa5e9e | ||
|
|
18e19f6ce6 | ||
|
|
7ef0afcb62 | ||
|
|
1e2f0be75a | ||
|
|
a58cce2dba | ||
|
|
ffa78aa67c | ||
|
|
7cbb1060ac | ||
|
|
a05541358e | ||
|
|
1f88da7538 | ||
|
|
36d8c53645 | ||
|
|
ec7b3872b4 | ||
|
|
edacaf7ff4 | ||
|
|
df0eb1f8e9 | ||
|
|
59b9c32fbc | ||
|
|
9f37100096 | ||
|
|
a2f2bd5df5 |
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -5,4 +5,4 @@
|
||||
# https://git-scm.com/docs/gitignore#_pattern_format
|
||||
|
||||
# Maintainers
|
||||
* @orhun @mindoodoo @sayanarijit @joshka @kdheepak
|
||||
* @orhun @mindoodoo @sayanarijit @joshka @kdheepak @Valentin271
|
||||
|
||||
52
.github/workflows/calculate-alpha-release.bash
vendored
Executable file
52
.github/workflows/calculate-alpha-release.bash
vendored
Executable file
@@ -0,0 +1,52 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Exit on error. Append "|| true" if you expect an error.
|
||||
set -o errexit
|
||||
# Exit on error inside any functions or subshells.
|
||||
set -o errtrace
|
||||
# Do not allow use of undefined vars. Use ${VAR:-} to use an undefined VAR
|
||||
set -o nounset
|
||||
# Catch the error in case mysqldump fails (but gzip succeeds) in `mysqldump |gzip`
|
||||
set -o pipefail
|
||||
# Turn on traces, useful while debugging but commented out by default
|
||||
# set -o xtrace
|
||||
|
||||
last_release="$(git tag --sort=committerdate | grep -E "v0\.\d+\.\d+$" | tail -1)"
|
||||
echo "🐭 Last release: ${last_release}"
|
||||
|
||||
# detect breaking changes
|
||||
if git log --oneline ${last_release}..HEAD | grep -q '!:' || true; then
|
||||
echo "🐭 Breaking changes detected since ${last_release}"
|
||||
git log --oneline ${last_release}..HEAD | grep '!:'
|
||||
# increment the minor version
|
||||
minor="${last_release##v0.}"
|
||||
minor="${minor%.*}"
|
||||
next_minor="$((minor + 1))"
|
||||
next_release="v0.${next_minor}.0"
|
||||
else
|
||||
# increment the patch version
|
||||
patch="${last_release##*.}"
|
||||
next_patch="$((patch + 1))"
|
||||
next_release="${last_release/%${patch}/${next_patch}}"
|
||||
fi
|
||||
echo "🐭 Next release: ${next_release}"
|
||||
|
||||
suffix="alpha"
|
||||
last_tag="$(git tag --sort=committerdate | tail -1)"
|
||||
if [[ "${last_tag}" = "${next-release}-${suffix}"* ]]; then
|
||||
echo "🐭 Last alpha release: ${last_tag}"
|
||||
# increment the alpha version
|
||||
# e.g. v0.22.1-alpha.12 -> v0.22.1-alpha.13
|
||||
alpha="${last_tag##*-${suffix}.}"
|
||||
next_alpha="$((alpha + 1))"
|
||||
next_tag="${last_tag/%${alpha}/${next_alpha}}"
|
||||
else
|
||||
# increment the patch and start the alpha version from 0
|
||||
# e.g. v0.22.0 -> v0.22.1-alpha.0
|
||||
next_tag="${next_release}-${suffix}.0"
|
||||
fi
|
||||
# update the crate version
|
||||
msg="# crate version"
|
||||
sed -E -i "s/^version = .* ${msg}$/version = \"${next_tag#v}\" ${msg}/" Cargo.toml
|
||||
echo "NEXT_TAG=${next_tag}" >> $GITHUB_ENV
|
||||
echo "🐭 Next alpha release: ${next_tag}"
|
||||
22
.github/workflows/cd.yml
vendored
22
.github/workflows/cd.yml
vendored
@@ -28,27 +28,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Calculate the next release
|
||||
run: |
|
||||
suffix="alpha"
|
||||
last_tag="$(git tag --sort=committerdate | tail -1)"
|
||||
if [[ "${last_tag}" = *"-${suffix}"* ]]; then
|
||||
# increment the alpha version
|
||||
# e.g. v0.22.1-alpha.12 -> v0.22.1-alpha.13
|
||||
alpha="${last_tag##*-${suffix}.}"
|
||||
next_alpha="$((alpha + 1))"
|
||||
next_tag="${last_tag/%${alpha}/${next_alpha}}"
|
||||
else
|
||||
# increment the patch and start the alpha version from 0
|
||||
# e.g. v0.22.0 -> v0.22.1-alpha.0
|
||||
patch="${last_tag##*.}"
|
||||
next_patch="$((patch + 1))"
|
||||
next_tag="${last_tag/%${patch}/${next_patch}}-${suffix}.0"
|
||||
fi
|
||||
# update the crate version
|
||||
msg="# crate version"
|
||||
sed -E -i "s/^version = .* ${msg}$/version = \"${next_tag#v}\" ${msg}/" Cargo.toml
|
||||
echo "NEXT_TAG=${next_tag}" >> $GITHUB_ENV
|
||||
echo "Next alpha release: ${next_tag} 🐭"
|
||||
run: .github/workflows/calculate-alpha-release.bash
|
||||
|
||||
- name: Publish on crates.io
|
||||
uses: actions-rs/cargo@v1
|
||||
|
||||
8
.github/workflows/check-pr.yml
vendored
8
.github/workflows/check-pr.yml
vendored
@@ -46,20 +46,24 @@ jobs:
|
||||
|
||||
check-breaking-change-label:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
# use an environment variable to pass untrusted input to the script
|
||||
# see https://securitylab.github.com/research/github-actions-untrusted-input/
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
steps:
|
||||
- name: Check breaking change label
|
||||
id: check_breaking_change
|
||||
run: |
|
||||
pattern='^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\(\w+\))?!:'
|
||||
# Check if pattern matches
|
||||
if echo "${{ github.event.pull_request.title }}" | grep -qE "$pattern"; then
|
||||
if echo "${PR_TITLE}" | grep -qE "$pattern"; then
|
||||
echo "breaking_change=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "breaking_change=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
- name: Add label
|
||||
if: steps.check_breaking_change.outputs.breaking_change == 'true'
|
||||
uses: actions/github-script@v6
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
|
||||
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -96,7 +96,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ ubuntu-latest, windows-latest, macos-latest ]
|
||||
toolchain: [ "1.67.0", "stable" ]
|
||||
toolchain: [ "1.70.0", "stable" ]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -135,7 +135,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ ubuntu-latest, windows-latest, macos-latest ]
|
||||
toolchain: [ "1.67.0", "stable" ]
|
||||
toolchain: [ "1.70.0", "stable" ]
|
||||
backend: [ crossterm, termion, termwiz ]
|
||||
exclude:
|
||||
# termion is not supported on windows
|
||||
@@ -151,6 +151,8 @@ jobs:
|
||||
toolchain: ${{ matrix.toolchain }}
|
||||
- name: Install cargo-make
|
||||
uses: taiki-e/install-action@cargo-make
|
||||
- name: Install cargo-make
|
||||
uses: taiki-e/install-action@nextest
|
||||
- name: Test ${{ matrix.backend }}
|
||||
run: cargo make test-backend ${{ matrix.backend }}
|
||||
env:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Breaking Changes
|
||||
|
||||
This document contains a list of breaking changes in each version and some notes to help migrate
|
||||
between versions. It is compile manually from the commit history and changelog. We also tag PRs on
|
||||
between versions. It is compiled manually from the commit history and changelog. We also tag PRs on
|
||||
github with a [breaking change] label.
|
||||
|
||||
[breaking change]: (https://github.com/ratatui-org/ratatui/issues?q=label%3A%22breaking+change%22)
|
||||
@@ -10,7 +10,18 @@ github with a [breaking change] label.
|
||||
|
||||
This is a quick summary of the sections below:
|
||||
|
||||
- [v0.26.0 (unreleased)](#v0260-unreleased)
|
||||
- `Line` now has an extra `style` field which applies the style to the entire line
|
||||
- `Block` style methods cannot be created in a const context
|
||||
- [v0.25.0](#v0250)
|
||||
- Removed `Axis::title_style` and `Buffer::set_background`
|
||||
- `List::new()` now accepts `IntoIterator<Item = Into<ListItem<'a>>>`
|
||||
- `Table::new()` now requires specifying the widths
|
||||
- `Table::widths()` now accepts `IntoIterator<Item = AsRef<Constraint>>`
|
||||
- Layout::new() now accepts direction and constraint parameters
|
||||
- The default `Tabs::highlight_style` is now `Style::new().reversed()`
|
||||
- [v0.24.0](#v0240)
|
||||
- MSRV is now 1.70.0
|
||||
- `ScrollbarState`: `position`, `content_length`, and `viewport_content_length` are now `usize`
|
||||
- `BorderType`: `line_symbols` is now `border_symbols` and returns `symbols::border::set`
|
||||
- `Frame<'a, B: Backend>` is now `Frame<'a>`
|
||||
@@ -31,6 +42,146 @@ This is a quick summary of the sections below:
|
||||
- MSRV is now 1.63.0
|
||||
- `List` no longer ignores empty strings
|
||||
|
||||
## v0.26.0 (unreleased)
|
||||
|
||||
### `Block` style methods cannot be used in a const context ([#720])
|
||||
|
||||
[#720]: https://github.com/ratatui-org/ratatui/pull/720
|
||||
|
||||
Previously the `style()`, `border_style()` and `title_style()` methods could be used to create a
|
||||
`Block` in a constant context. These now accept `Into<Style>` instead of `Style`. These methods no
|
||||
longer can be called from a constant context.
|
||||
|
||||
### `Line` now has a `style` field that applies to the entire line ([#708])
|
||||
|
||||
[#708]: https://github.com/ratatui-org/ratatui/pull/708
|
||||
|
||||
Previously the style of a `Line` was stored in the `Span`s that make up the line. Now the `Line`
|
||||
itself has a `style` field, which can be set with the `Line::style` method. Any code that creates
|
||||
`Line`s using the struct initializer instead of constructors will fail to compile due to the added
|
||||
field. This can be easily fixed by adding `..Default::default()` to the field list or by using a
|
||||
constructor method (`Line::styled()`, `Line::raw()`) or conversion method (`Line::from()`).
|
||||
|
||||
Each `Span` contained within the line will no longer have the style that is applied to the line in
|
||||
the `Span::style` field.
|
||||
|
||||
```diff
|
||||
let line = Line {
|
||||
spans: vec!["".into()],
|
||||
alignment: Alignment::Left,
|
||||
+ ..Default::default()
|
||||
};
|
||||
|
||||
// or
|
||||
|
||||
let line = Line::raw(vec!["".into()])
|
||||
.alignment(Alignment::Left);
|
||||
```
|
||||
|
||||
## [v0.25.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.25.0)
|
||||
|
||||
### Removed `Axis::title_style` and `Buffer::set_background` ([#691])
|
||||
|
||||
[#691]: https://github.com/ratatui-org/ratatui/pull/691
|
||||
|
||||
These items were deprecated since 0.10.
|
||||
|
||||
- You should use styling capabilities of [`text::Line`] given as argument of [`Axis::title`]
|
||||
instead of `Axis::title_style`
|
||||
- You should use styling capabilities of [`Buffer::set_style`] instead of `Buffer::set_background`
|
||||
|
||||
[`text::Line`]: https://docs.rs/ratatui/latest/ratatui/text/struct.Line.html
|
||||
[`Axis::title`]: https://docs.rs/ratatui/latest/ratatui/widgets/struct.Axis.html#method.title
|
||||
[`Buffer::set_style`]: https://docs.rs/ratatui/latest/ratatui/buffer/struct.Buffer.html#method.set_style
|
||||
|
||||
### `List::new()` now accepts `IntoIterator<Item = Into<ListItem<'a>>>` ([#672])
|
||||
|
||||
[#672]: https://github.com/ratatui-org/ratatui/pull/672
|
||||
|
||||
Previously `List::new()` took `Into<Vec<ListItem<'a>>>`. This change will throw a compilation
|
||||
error for `IntoIterator`s with an indeterminate item (e.g. empty vecs).
|
||||
|
||||
E.g.
|
||||
|
||||
```diff
|
||||
- let list = List::new(vec![]);
|
||||
// becomes
|
||||
+ let list = List::default();
|
||||
```
|
||||
|
||||
### The default `Tabs::highlight_style` is now `Style::new().reversed()` ([#635])
|
||||
|
||||
[#635]: https://github.com/ratatui-org/ratatui/pull/635
|
||||
|
||||
Previously the default highlight style for tabs was `Style::default()`, which meant that a `Tabs`
|
||||
widget in the default configuration would not show any indication of the selected tab.
|
||||
|
||||
### The default `Tabs::highlight_style` is now `Style::new().reversed()` ([#635])
|
||||
|
||||
Previously the default highlight style for tabs was `Style::default()`, which meant that a `Tabs`
|
||||
widget in the default configuration would not show any indication of the selected tab.
|
||||
|
||||
### `Table::new()` now requires specifying the widths of the columns ([#664])
|
||||
|
||||
[#664]: https://github.com/ratatui-org/ratatui/pull/664
|
||||
|
||||
Previously `Table`s could be constructed without widths. In almost all cases this is an error.
|
||||
A new widths parameter is now mandatory on `Table::new()`. Existing code of the form:
|
||||
|
||||
```diff
|
||||
- Table::new(rows).widths(widths)
|
||||
```
|
||||
|
||||
Should be updated to:
|
||||
|
||||
```diff
|
||||
+ Table::new(rows, widths)
|
||||
```
|
||||
|
||||
For ease of automated replacement in cases where the amount of code broken by this change is large
|
||||
or complex, it may be convenient to replace `Table::new` with `Table::default().rows`.
|
||||
|
||||
```diff
|
||||
- Table::new(rows).block(block).widths(widths);
|
||||
// becomes
|
||||
+ Table::default().rows(rows).widths(widths)
|
||||
```
|
||||
|
||||
### `Table::widths()` now accepts `IntoIterator<Item = AsRef<Constraint>>` ([#663])
|
||||
|
||||
[#663]: https://github.com/ratatui-org/ratatui/pull/663
|
||||
|
||||
Previously `Table::widths()` took a slice (`&'a [Constraint]`). This change will introduce clippy
|
||||
`needless_borrow` warnings for places where slices are passed to this method. To fix these, remove
|
||||
the `&`.
|
||||
|
||||
E.g.
|
||||
|
||||
```diff
|
||||
- let table = Table::new(rows).widths(&[Constraint::Length(1)]);
|
||||
// becomes
|
||||
+ let table = Table::new(rows, [Constraint::Length(1)]);
|
||||
```
|
||||
|
||||
### Layout::new() now accepts direction and constraint parameters ([#557])
|
||||
|
||||
[#557]: https://github.com/ratatui-org/ratatui/pull/557
|
||||
|
||||
Previously layout new took no parameters. Existing code should either use `Layout::default()` or
|
||||
the new constructor.
|
||||
|
||||
```rust
|
||||
let layout = layout::new()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Min(1), Constraint::Max(2)]);
|
||||
// becomes either
|
||||
let layout = layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Min(1), Constraint::Max(2)]);
|
||||
// or
|
||||
let layout = layout::new(Direction::Vertical, [Constraint::Min(1), Constraint::Max(2)]);
|
||||
```
|
||||
|
||||
## [v0.24.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.24.0)
|
||||
|
||||
### ScrollbarState field type changed from `u16` to `usize` ([#456])
|
||||
@@ -48,10 +199,10 @@ Applications can now set custom borders on a `Block` by calling `border_set()`.
|
||||
`BorderType::line_symbols()` is renamed to `border_symbols()` and now returns a new struct
|
||||
`symbols::border::Set`. E.g.:
|
||||
|
||||
```rust
|
||||
let line_set: symbols::line::Set = BorderType::line_symbols(BorderType::Plain);
|
||||
```diff
|
||||
- let line_set: symbols::line::Set = BorderType::line_symbols(BorderType::Plain);
|
||||
// becomes
|
||||
let border_set: symbols::border::Set = BorderType::border_symbols(BorderType::Plain);
|
||||
+ let border_set: symbols::border::Set = BorderType::border_symbols(BorderType::Plain);
|
||||
```
|
||||
|
||||
### Generic `Backend` parameter removed from `Frame` ([#530])
|
||||
@@ -62,10 +213,10 @@ let border_set: symbols::border::Set = BorderType::border_symbols(BorderType::Pl
|
||||
accept `Frame`. To migrate existing code, remove any generic parameters from code that uses an
|
||||
instance of a Frame. E.g.:
|
||||
|
||||
```rust
|
||||
fn ui<B: Backend>(frame: &mut Frame<B>) { ... }
|
||||
```diff
|
||||
- fn ui<B: Backend>(frame: &mut Frame<B>) { ... }
|
||||
// becomes
|
||||
fn ui(frame: Frame) { ... }
|
||||
+ fn ui(frame: Frame) { ... }
|
||||
```
|
||||
|
||||
### `Stylize` shorthands now consume rather than borrow `String` ([#466])
|
||||
@@ -77,13 +228,13 @@ new implementation of `Stylize` was added that returns a `Span<'static>`. This c
|
||||
be consumed rather than borrowed. Existing code that expects to use the string after a call will no
|
||||
longer compile. E.g.
|
||||
|
||||
```rust
|
||||
let s = String::new("foo");
|
||||
let span1 = s.red();
|
||||
let span2 = s.blue(); // will no longer compile as s is consumed by the previous line
|
||||
```diff
|
||||
- let s = String::new("foo");
|
||||
- let span1 = s.red();
|
||||
- let span2 = s.blue(); // will no longer compile as s is consumed by the previous line
|
||||
// becomes
|
||||
let span1 = s.clone().red();
|
||||
let span2 = s.blue();
|
||||
+ let span1 = s.clone().red();
|
||||
+ let span2 = s.blue();
|
||||
```
|
||||
|
||||
### Deprecated `Spans` type removed (replaced with `Line`) ([#426])
|
||||
@@ -93,12 +244,12 @@ let span2 = s.blue();
|
||||
`Spans` was replaced with `Line` in 0.21.0. `Buffer::set_spans` was replaced with
|
||||
`Buffer::set_line`.
|
||||
|
||||
```rust
|
||||
let spans = Spans::from(some_string_str_span_or_vec_span);
|
||||
buffer.set_spans(0, 0, spans, 10);
|
||||
```diff
|
||||
- let spans = Spans::from(some_string_str_span_or_vec_span);
|
||||
- buffer.set_spans(0, 0, spans, 10);
|
||||
// becomes
|
||||
let line - Line::from(some_string_str_span_or_vec_span);
|
||||
buffer.set_line(0, 0, line, 10);
|
||||
+ let line - Line::from(some_string_str_span_or_vec_span);
|
||||
+ buffer.set_line(0, 0, line, 10);
|
||||
```
|
||||
|
||||
## [v0.23.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.23.0)
|
||||
@@ -109,10 +260,10 @@ buffer.set_line(0, 0, line, 10);
|
||||
|
||||
The track symbol of `Scrollbar` is now optional, this method now takes an optional value.
|
||||
|
||||
```rust
|
||||
let scrollbar = Scrollbar::default().track_symbol("|");
|
||||
```diff
|
||||
- let scrollbar = Scrollbar::default().track_symbol("|");
|
||||
// becomes
|
||||
let scrollbar = Scrollbar::default().track_symbol(Some("|"));
|
||||
+ let scrollbar = Scrollbar::default().track_symbol(Some("|"));
|
||||
```
|
||||
|
||||
### `Scrollbar` symbols moved to `symbols::scrollbar` and `widgets::scrollbar` module is private ([#330])
|
||||
@@ -123,10 +274,10 @@ The symbols for defining scrollbars have been moved to the `symbols` module from
|
||||
`widgets::scrollbar` module which is no longer public. To update your code update any imports to the
|
||||
new module locations. E.g.:
|
||||
|
||||
```rust
|
||||
use ratatui::{widgets::scrollbar::{Scrollbar, Set}};
|
||||
```diff
|
||||
- use ratatui::{widgets::scrollbar::{Scrollbar, Set}};
|
||||
// becomes
|
||||
use ratatui::{widgets::Scrollbar, symbols::scrollbar::Set}
|
||||
+ use ratatui::{widgets::Scrollbar, symbols::scrollbar::Set}
|
||||
```
|
||||
|
||||
### MSRV updated to 1.67 ([#361])
|
||||
@@ -160,13 +311,13 @@ The minimum supported rust version is now 1.65.0.
|
||||
In order to support inline viewports, the unstable method `Terminal::with_options()` was stabilized
|
||||
and `ViewPort` was changed from a struct to an enum.
|
||||
|
||||
```rust
|
||||
```diff
|
||||
let terminal = Terminal::with_options(backend, TerminalOptions {
|
||||
viewport: Viewport::fixed(area),
|
||||
- viewport: Viewport::fixed(area),
|
||||
});
|
||||
// becomes
|
||||
let terminal = Terminal::with_options(backend, TerminalOptions {
|
||||
viewport: Viewport::Fixed(area),
|
||||
+ viewport: Viewport::Fixed(area),
|
||||
});
|
||||
```
|
||||
|
||||
@@ -178,10 +329,10 @@ A new type `Masked` was introduced that implements `From<Text<'a>>`. This causes
|
||||
previously did not need to use type annotations to fail to compile. To fix this, annotate or call
|
||||
to_string() / to_owned() / as_str() on the value. E.g.:
|
||||
|
||||
```rust
|
||||
let paragraph = Paragraph::new("".as_ref());
|
||||
```diff
|
||||
- let paragraph = Paragraph::new("".as_ref());
|
||||
// becomes
|
||||
let paragraph = Paragraph::new("".as_str());
|
||||
+ let paragraph = Paragraph::new("".as_str());
|
||||
```
|
||||
|
||||
### `Marker::Block` now renders as a block rather than a bar character ([#133])
|
||||
@@ -192,10 +343,10 @@ Code using the `Block` marker that previously rendered using a half block charac
|
||||
renders using the full block character (`'█'`). A new marker variant`Bar` is introduced to replace
|
||||
the existing code.
|
||||
|
||||
```rust
|
||||
let canvas = Canvas::default().marker(Marker::Block);
|
||||
```diff
|
||||
- let canvas = Canvas::default().marker(Marker::Block);
|
||||
// becomes
|
||||
let canvas = Canvas::default().marker(Marker::Bar);
|
||||
+ let canvas = Canvas::default().marker(Marker::Bar);
|
||||
```
|
||||
|
||||
## [v0.20.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.20.0)
|
||||
|
||||
487
CHANGELOG.md
487
CHANGELOG.md
@@ -2,6 +2,491 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [0.25.0](https://github.com/ratatui-org/ratatui/releases/tag/0.25.0) - 2023-12-18
|
||||
|
||||
We are thrilled to announce the new version of `ratatui` - a Rust library that's all about cooking up TUIs 🐭
|
||||
|
||||
In this version, we made improvements on widgets such as List, Table and Layout and changed some of the defaults for a better user experience.
|
||||
Also, we renewed our website and updated our documentation/tutorials to get started with `ratatui`: <https://ratatui.rs> 🚀
|
||||
|
||||
✨ **Release highlights**: <https://ratatui.rs/highlights/v025/>
|
||||
|
||||
⚠️ List of breaking changes can be found [here](https://github.com/ratatui-org/ratatui/blob/main/BREAKING-CHANGES.md).
|
||||
|
||||
💖 We also enabled GitHub Sponsors for our organization, consider sponsoring us if you like `ratatui`: <https://github.com/sponsors/ratatui-org>
|
||||
|
||||
### Features
|
||||
|
||||
- [aef4956](https://github.com/ratatui-org/ratatui/commit/aef495604c52e563fbacfb1a6e730cd441a99129)
|
||||
*(list)* `List::new` now accepts `IntoIterator<Item = Into<ListItem>>` ([#672](https://github.com/ratatui-org/ratatui/issues/672)) [**breaking**]
|
||||
|
||||
````text
|
||||
This allows to build list like
|
||||
|
||||
```
|
||||
List::new(["Item 1", "Item 2"])
|
||||
```
|
||||
````
|
||||
|
||||
- [8bfd666](https://github.com/ratatui-org/ratatui/commit/8bfd6661e251b6943f74bda626e4708b2e9f4b51)
|
||||
*(paragraph)* Add `line_count` and `line_width` unstable helper methods
|
||||
|
||||
````text
|
||||
This is an unstable feature that may be removed in the future
|
||||
````
|
||||
|
||||
- [1229b96](https://github.com/ratatui-org/ratatui/commit/1229b96e428df880a951ef57f53ca73e74ef1ea2)
|
||||
*(rect)* Add `offset` method ([#533](https://github.com/ratatui-org/ratatui/issues/533))
|
||||
|
||||
````text
|
||||
The offset method creates a new Rect that is moved by the amount
|
||||
specified in the x and y direction. These values can be positive or
|
||||
negative. This is useful for manual layout tasks.
|
||||
|
||||
```rust
|
||||
let rect = area.offset(Offset { x: 10, y -10 });
|
||||
```
|
||||
````
|
||||
|
||||
- [edacaf7](https://github.com/ratatui-org/ratatui/commit/edacaf7ff4e4b14702f6361af5a6da713b7dc564)
|
||||
*(buffer)* Deprecate `Cell::symbol` field ([#624](https://github.com/ratatui-org/ratatui/issues/624))
|
||||
|
||||
````text
|
||||
The Cell::symbol field is now accessible via a getter method (`symbol()`). This will
|
||||
allow us to make future changes to the Cell internals such as replacing `String` with
|
||||
`compact_str`.
|
||||
````
|
||||
|
||||
- [6b2efd0](https://github.com/ratatui-org/ratatui/commit/6b2efd0f6c3bf56dc06bbf042db40c0c66de577e)
|
||||
*(layout)* Accept IntoIterator for constraints ([#663](https://github.com/ratatui-org/ratatui/issues/663))
|
||||
|
||||
````text
|
||||
Layout and Table now accept IntoIterator for constraints with an Item
|
||||
that is AsRef<Constraint>. This allows pretty much any collection of
|
||||
constraints to be passed to the layout functions including arrays,
|
||||
vectors, slices, and iterators (without having to call collect() on
|
||||
them).
|
||||
````
|
||||
|
||||
- [753e246](https://github.com/ratatui-org/ratatui/commit/753e246531e1e9e2ea558911f8d03e738901d85f)
|
||||
*(layout)* Allow configuring layout fill ([#633](https://github.com/ratatui-org/ratatui/issues/633))
|
||||
|
||||
````text
|
||||
The layout split will generally fill the remaining area when `split()`
|
||||
is called. This change allows the caller to configure how any extra
|
||||
space is allocated to the `Rect`s. This is useful for cases where the
|
||||
caller wants to have a fixed size for one of the `Rect`s, and have the
|
||||
other `Rect`s fill the remaining space.
|
||||
|
||||
For now, the method and enum are marked as unstable because the exact
|
||||
name is still being bikeshedded. To enable this functionality, add the
|
||||
`unstable-segment-size` feature flag in your `Cargo.toml`.
|
||||
|
||||
To configure the layout to fill the remaining space evenly, use
|
||||
`Layout::segment_size(SegmentSize::EvenDistribution)`. The default
|
||||
behavior is `SegmentSize::LastTakesRemainder`, which gives the last
|
||||
segment the remaining space. `SegmentSize::None` will disable this
|
||||
behavior. See the docs for `Layout::segment_size()` and
|
||||
`layout::SegmentSize` for more information.
|
||||
|
||||
Fixes https://github.com/ratatui-org/ratatui/issues/536
|
||||
````
|
||||
|
||||
- [1e2f0be](https://github.com/ratatui-org/ratatui/commit/1e2f0be75ac3fb3d6500c1de291bd49972b808e4)
|
||||
*(layout)* Add parameters to Layout::new() ([#557](https://github.com/ratatui-org/ratatui/issues/557)) [**breaking**]
|
||||
|
||||
````text
|
||||
Adds a convenience function to create a layout with a direction and a
|
||||
list of constraints which are the most common parameters that would be
|
||||
generally configured using the builder pattern. The constraints can be
|
||||
passed in as any iterator of constraints.
|
||||
|
||||
```rust
|
||||
let layout = Layout::new(Direction::Horizontal, [
|
||||
Constraint::Percentage(50),
|
||||
Constraint::Percentage(50),
|
||||
]);
|
||||
```
|
||||
````
|
||||
|
||||
- [c862aa5](https://github.com/ratatui-org/ratatui/commit/c862aa5e9ef4dbf494b5151214ac87f5c71e76d4)
|
||||
*(list)* Support line alignment ([#599](https://github.com/ratatui-org/ratatui/issues/599))
|
||||
|
||||
````text
|
||||
The `List` widget now respects the alignment of `Line`s and renders them as expected.
|
||||
````
|
||||
|
||||
- [4424637](https://github.com/ratatui-org/ratatui/commit/4424637af252dc2f227fe4956eac71135e60fb02)
|
||||
*(span)* Add setters for content and style ([#647](https://github.com/ratatui-org/ratatui/issues/647))
|
||||
|
||||
- [ebf1f42](https://github.com/ratatui-org/ratatui/commit/ebf1f4294211d478b8633a06576ec269a50db588)
|
||||
*(style)* Implement `From` trait for crossterm to `Style` related structs ([#686](https://github.com/ratatui-org/ratatui/issues/686))
|
||||
|
||||
- [e49385b](https://github.com/ratatui-org/ratatui/commit/e49385b78c8e01fe6381b19d15137346bc6eb8a1)
|
||||
*(table)* Add a Table::segment_size method ([#660](https://github.com/ratatui-org/ratatui/issues/660))
|
||||
|
||||
````text
|
||||
It controls how to distribute extra space to an underconstrained table.
|
||||
The default, legacy behavior is to leave the extra space unused. The
|
||||
new options are LastTakesRemainder which gets all space to the rightmost
|
||||
column that can used it, and EvenDistribution which divides it amongst
|
||||
all columns.
|
||||
````
|
||||
|
||||
- [b8f71c0](https://github.com/ratatui-org/ratatui/commit/b8f71c0d6eda3da272d29c7a9b3c47181049f76a)
|
||||
*(widgets/chart)* Add option to set the position of legend ([#378](https://github.com/ratatui-org/ratatui/issues/378))
|
||||
|
||||
- [5bf4f52](https://github.com/ratatui-org/ratatui/commit/5bf4f52119ab3e0e3a266af196058179dc1d18c3)
|
||||
*(uncategorized)* Implement `From` trait for termion to `Style` related structs ([#692](https://github.com/ratatui-org/ratatui/issues/692))
|
||||
|
||||
````text
|
||||
* feat(termion): implement from termion color
|
||||
|
||||
* feat(termion): implement from termion style
|
||||
|
||||
* feat(termion): implement from termion `Bg` and `Fg`
|
||||
````
|
||||
|
||||
- [d19b266](https://github.com/ratatui-org/ratatui/commit/d19b266e0eabdb0fb00660439a1818239c94024b)
|
||||
*(uncategorized)* Add Constraint helpers (e.g. from_lengths) ([#641](https://github.com/ratatui-org/ratatui/issues/641))
|
||||
|
||||
````text
|
||||
Adds helper methods that convert from iterators of u16 values to the
|
||||
specific Constraint type. This makes it easy to create constraints like:
|
||||
|
||||
```rust
|
||||
// a fixed layout
|
||||
let constraints = Constraint::from_lengths([10, 20, 10]);
|
||||
|
||||
// a centered layout
|
||||
let constraints = Constraint::from_ratios([(1, 4), (1, 2), (1, 4)]);
|
||||
let constraints = Constraint::from_percentages([25, 50, 25]);
|
||||
|
||||
// a centered layout with a minimum size
|
||||
let constraints = Constraint::from_mins([0, 100, 0]);
|
||||
|
||||
// a sidebar / main layout with maximum sizes
|
||||
let constraints = Constraint::from_maxes([30, 200]);
|
||||
```
|
||||
````
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- [f69d57c](https://github.com/ratatui-org/ratatui/commit/f69d57c3b59e27b517a5ca1a002af808fee47970)
|
||||
*(rect)* Fix underflow in the `Rect::intersection` method ([#678](https://github.com/ratatui-org/ratatui/issues/678))
|
||||
|
||||
- [56fc410](https://github.com/ratatui-org/ratatui/commit/56fc4101056e0f631f563f8f2c07646063e650d3)
|
||||
*(block)* Make `inner` aware of title positions ([#657](https://github.com/ratatui-org/ratatui/issues/657))
|
||||
|
||||
````text
|
||||
Previously, when computing the inner rendering area of a block, all
|
||||
titles were assumed to be positioned at the top, which caused the
|
||||
height of the inner area to be miscalculated.
|
||||
````
|
||||
|
||||
- [ec7b387](https://github.com/ratatui-org/ratatui/commit/ec7b3872b46c6828c88ce7f72308dc67731fca25)
|
||||
*(doc)* Do not access deprecated `Cell::symbol` field in doc example ([#626](https://github.com/ratatui-org/ratatui/issues/626))
|
||||
|
||||
- [37c70db](https://github.com/ratatui-org/ratatui/commit/37c70dbb8e19c0fb35ced16b29751933514a441e)
|
||||
*(table)* Add widths parameter to new() ([#664](https://github.com/ratatui-org/ratatui/issues/664)) [**breaking**]
|
||||
|
||||
````text
|
||||
This prevents creating a table that doesn't actually render anything.
|
||||
````
|
||||
|
||||
- [1f88da7](https://github.com/ratatui-org/ratatui/commit/1f88da75383f6de76e64e9258fbf38d02ec77af9)
|
||||
*(table)* Fix new clippy lint which triggers on table widths tests ([#630](https://github.com/ratatui-org/ratatui/issues/630))
|
||||
|
||||
````text
|
||||
* fix(table): new clippy lint in 1.74.0 triggers on table widths tests
|
||||
````
|
||||
|
||||
- [36d8c53](https://github.com/ratatui-org/ratatui/commit/36d8c5364590a559913c40ee5f021b5d8e3466e6)
|
||||
*(table)* Widths() now accepts AsRef<[Constraint]> ([#628](https://github.com/ratatui-org/ratatui/issues/628))
|
||||
|
||||
````text
|
||||
This allows passing an array, slice or Vec of constraints, which is more
|
||||
ergonomic than requiring this to always be a slice.
|
||||
|
||||
The following calls now all succeed:
|
||||
|
||||
```rust
|
||||
Table::new(rows).widths([Constraint::Length(5), Constraint::Length(5)]);
|
||||
Table::new(rows).widths(&[Constraint::Length(5), Constraint::Length(5)]);
|
||||
|
||||
// widths could also be computed at runtime
|
||||
let widths = vec![Constraint::Length(5), Constraint::Length(5)];
|
||||
Table::new(rows).widths(widths.clone());
|
||||
Table::new(rows).widths(&widths);
|
||||
```
|
||||
````
|
||||
|
||||
- [34d099c](https://github.com/ratatui-org/ratatui/commit/34d099c99af27eacfdde71f9ced255c29e1e001a)
|
||||
*(tabs)* Fixup tests broken by semantic merge conflict ([#665](https://github.com/ratatui-org/ratatui/issues/665))
|
||||
|
||||
````text
|
||||
Two changes without any line overlap caused the tabs tests to break
|
||||
````
|
||||
|
||||
- [e4579f0](https://github.com/ratatui-org/ratatui/commit/e4579f0db2b70b59590cae02e994e3736b19a1b3)
|
||||
*(tabs)* Set the default highlight_style ([#635](https://github.com/ratatui-org/ratatui/issues/635)) [**breaking**]
|
||||
|
||||
````text
|
||||
Previously the default highlight_style was set to `Style::default()`,
|
||||
which meant that the highlight style was the same as the normal style.
|
||||
This change sets the default highlight_style to reversed text.
|
||||
````
|
||||
|
||||
- [28ac55b](https://github.com/ratatui-org/ratatui/commit/28ac55bc62e4e14e3ace300633d56791a1d3dea0)
|
||||
*(tabs)* Tab widget now supports custom padding ([#629](https://github.com/ratatui-org/ratatui/issues/629))
|
||||
|
||||
````text
|
||||
The Tab widget now contains padding_left and and padding_right
|
||||
properties. Those values can be set with functions `padding_left()`,
|
||||
`padding_right()`, and `padding()` which all accept `Into<Line>`.
|
||||
|
||||
Fixes issue https://github.com/ratatui-org/ratatui/issues/502
|
||||
````
|
||||
|
||||
- [df0eb1f](https://github.com/ratatui-org/ratatui/commit/df0eb1f8e94752db542ff58e1453f4f8beab17e2)
|
||||
*(terminal)* Insert_before() now accepts lines > terminal height and doesn't add an extra blank line ([#596](https://github.com/ratatui-org/ratatui/issues/596))
|
||||
|
||||
````text
|
||||
Fixes issue with inserting content with height>viewport_area.height and adds
|
||||
the ability to insert content of height>terminal_height
|
||||
|
||||
- Adds TestBackend::append_lines() and TestBackend::clear_region() methods to
|
||||
support testing the changes
|
||||
````
|
||||
|
||||
- [aaeba27](https://github.com/ratatui-org/ratatui/commit/aaeba2709c09b7373f3781ecd4b0a96b22fc2764)
|
||||
*(uncategorized)* Truncate table when overflow ([#685](https://github.com/ratatui-org/ratatui/issues/685))
|
||||
|
||||
````text
|
||||
This prevents a panic when rendering an empty right aligned and rightmost table cell
|
||||
````
|
||||
|
||||
- [ffa78aa](https://github.com/ratatui-org/ratatui/commit/ffa78aa67ccd79b9aa1af0d7ccf56a2059d0f519)
|
||||
*(uncategorized)* Add #[must_use] to Style-moving methods ([#600](https://github.com/ratatui-org/ratatui/issues/600))
|
||||
|
||||
- [a2f2bd5](https://github.com/ratatui-org/ratatui/commit/a2f2bd5df53a796c0f2a57bb1b22151e52b5ef03)
|
||||
*(uncategorized)* MSRV is now `1.70.0` ([#593](https://github.com/ratatui-org/ratatui/issues/593))
|
||||
|
||||
### Refactor
|
||||
|
||||
- [f767ea7](https://github.com/ratatui-org/ratatui/commit/f767ea7d3766887cb79145103b5aa92e0eabf8f6)
|
||||
*(list)* `start_corner` is now `direction` ([#673](https://github.com/ratatui-org/ratatui/issues/673))
|
||||
|
||||
````text
|
||||
The previous name `start_corner` did not communicate clearly the intent of the method.
|
||||
A new method `direction` and a new enum `ListDirection` were added.
|
||||
|
||||
`start_corner` is now deprecated
|
||||
````
|
||||
|
||||
- [b82451f](https://github.com/ratatui-org/ratatui/commit/b82451fb33f35ae0323a56bb6f962404b076a262)
|
||||
*(examples)* Add vim binding ([#688](https://github.com/ratatui-org/ratatui/issues/688))
|
||||
|
||||
- [0576a8a](https://github.com/ratatui-org/ratatui/commit/0576a8aa3212c57d288c67592337a3870ae6dafc)
|
||||
*(layout)* To natural reading order ([#681](https://github.com/ratatui-org/ratatui/issues/681))
|
||||
|
||||
````text
|
||||
Structs and enums at the top of the file helps show the interaction
|
||||
between the types without having to find each type in between longer
|
||||
impl sections.
|
||||
|
||||
Also moved the try_split function into the Layout impl as an associated
|
||||
function and inlined the `layout::split()` which just called try_split.
|
||||
This makes the code a bit more contained.
|
||||
````
|
||||
|
||||
- [4be18ab](https://github.com/ratatui-org/ratatui/commit/4be18aba8b535165f03d15450276b2e95a7970eb)
|
||||
*(readme)* Reference awesome-ratatui instead of wiki ([#689](https://github.com/ratatui-org/ratatui/issues/689))
|
||||
|
||||
````text
|
||||
* refactor(readme): link awesome-ratatui instead of wiki
|
||||
|
||||
The apps wiki moved to awesome-ratatui
|
||||
|
||||
* docs(readme): Update README.md
|
||||
````
|
||||
|
||||
- [7ef0afc](https://github.com/ratatui-org/ratatui/commit/7ef0afcb62198f76321e84d9bb19a8a590a3b649)
|
||||
*(widgets)* Remove unnecessary dynamic dispatch and heap allocation ([#597](https://github.com/ratatui-org/ratatui/issues/597))
|
||||
|
||||
- [b282a06](https://github.com/ratatui-org/ratatui/commit/b282a0693289d9d2602b54b639d3701d8c8cc8a8)
|
||||
*(uncategorized)* Remove items deprecated since 0.10 ([#691](https://github.com/ratatui-org/ratatui/issues/691)) [**breaking**]
|
||||
|
||||
````text
|
||||
Remove `Axis::title_style` and `Buffer::set_background` which are deprecated since 0.10
|
||||
````
|
||||
|
||||
- [7ced7c0](https://github.com/ratatui-org/ratatui/commit/7ced7c0aa3acdaa63ed6add59711614993210ba3)
|
||||
*(uncategorized)* Define struct WrappedLine instead of anonymous tuple ([#608](https://github.com/ratatui-org/ratatui/issues/608))
|
||||
|
||||
````text
|
||||
It makes the type easier to document, and more obvious for users
|
||||
````
|
||||
|
||||
### Documentation
|
||||
|
||||
- [fe632d7](https://github.com/ratatui-org/ratatui/commit/fe632d70cb150264d9af2f79145a1d14a3637f3e)
|
||||
*(sparkline)* Add documentation ([#648](https://github.com/ratatui-org/ratatui/issues/648))
|
||||
|
||||
- [f4c8de0](https://github.com/ratatui-org/ratatui/commit/f4c8de041d48cec5ea9b3e1f540f57af5a09d7a4)
|
||||
*(chart)* Document chart module ([#696](https://github.com/ratatui-org/ratatui/issues/696))
|
||||
|
||||
- [1b8b626](https://github.com/ratatui-org/ratatui/commit/1b8b6261e2de29a37b2cd7d6ee8659fb46d3beff)
|
||||
*(examples)* Add animation and FPS counter to colors_rgb ([#583](https://github.com/ratatui-org/ratatui/issues/583))
|
||||
|
||||
- [2169a0d](https://github.com/ratatui-org/ratatui/commit/2169a0da01e3bd6272e33b9de26a033fcb5f55f2)
|
||||
*(examples)* Add example of half block rendering ([#687](https://github.com/ratatui-org/ratatui/issues/687))
|
||||
|
||||
````text
|
||||
This is a fun example of how to render big text using half blocks
|
||||
````
|
||||
|
||||
- [41c44a4](https://github.com/ratatui-org/ratatui/commit/41c44a4af66ba791959f3a298d1b544330b9a164)
|
||||
*(frame)* Add docs about resize events ([#697](https://github.com/ratatui-org/ratatui/issues/697))
|
||||
|
||||
- [91c67eb](https://github.com/ratatui-org/ratatui/commit/91c67eb1009449e0dfdd29e6ef0132c5254cfbde)
|
||||
*(github)* Update code owners ([#666](https://github.com/ratatui-org/ratatui/issues/666))
|
||||
|
||||
````text
|
||||
onboard @Valentin271 as maintainer
|
||||
````
|
||||
|
||||
- [458fa90](https://github.com/ratatui-org/ratatui/commit/458fa9036281e0e6e88bd2ec90c633e499ce547c)
|
||||
*(lib)* Tweak the crate documentation ([#659](https://github.com/ratatui-org/ratatui/issues/659))
|
||||
|
||||
- [3ec4e24](https://github.com/ratatui-org/ratatui/commit/3ec4e24d00e118a12c8fea888e16ce19b75cf45f)
|
||||
*(list)* Add documentation to the List widget ([#669](https://github.com/ratatui-org/ratatui/issues/669))
|
||||
|
||||
````text
|
||||
Adds documentation to the List widget and all its sub components like `ListState` and `ListItem`
|
||||
````
|
||||
|
||||
- [9f37100](https://github.com/ratatui-org/ratatui/commit/9f371000968044e09545d66068c4ed4ea4b35d8a)
|
||||
*(readme)* Update README.md and fix the bug that demo2 cannot run ([#595](https://github.com/ratatui-org/ratatui/issues/595))
|
||||
|
||||
````text
|
||||
Fixes https://github.com/ratatui-org/ratatui/issues/594
|
||||
````
|
||||
|
||||
- [2a87251](https://github.com/ratatui-org/ratatui/commit/2a87251152432fd99c18864f32874fed2cab2f99)
|
||||
*(security)* Add security policy ([#676](https://github.com/ratatui-org/ratatui/issues/676))
|
||||
|
||||
````text
|
||||
* docs: Create SECURITY.md
|
||||
|
||||
* Update SECURITY.md
|
||||
````
|
||||
|
||||
- [987f7ee](https://github.com/ratatui-org/ratatui/commit/987f7eed4c8bd09e319b504e587eb1f3667ee64b)
|
||||
*(website)* Rename book to website ([#661](https://github.com/ratatui-org/ratatui/issues/661))
|
||||
|
||||
- [a15c3b2](https://github.com/ratatui-org/ratatui/commit/a15c3b2660bf4102bc881a5bc11959bc136f4a17)
|
||||
*(uncategorized)* Remove deprecated table constructor from breaking changes ([#698](https://github.com/ratatui-org/ratatui/issues/698))
|
||||
|
||||
- [113b4b7](https://github.com/ratatui-org/ratatui/commit/113b4b7a4ea841fe2ca7b1c153243fec781c3cc0)
|
||||
*(uncategorized)* Rename template links to remove ratatui from name 📚 ([#690](https://github.com/ratatui-org/ratatui/issues/690))
|
||||
|
||||
- [211160c](https://github.com/ratatui-org/ratatui/commit/211160ca165e2ad23b3d4cd9382c6e4869644a9c)
|
||||
*(uncategorized)* Remove simple-tui-rs ([#651](https://github.com/ratatui-org/ratatui/issues/651))
|
||||
|
||||
````text
|
||||
This has not been recently and doesn't lead to good code
|
||||
````
|
||||
|
||||
### Styling
|
||||
|
||||
- [6a6e9dd](https://github.com/ratatui-org/ratatui/commit/6a6e9dde9dc66ecb6f47f858fd0a67d7dc9eb7d1)
|
||||
*(tabs)* Fix doc formatting ([#662](https://github.com/ratatui-org/ratatui/issues/662))
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- [910ad00](https://github.com/ratatui-org/ratatui/commit/910ad00059c3603ba6b1751c95783f974fde88a1)
|
||||
*(rustfmt)* Enable format_code_in_doc_comments ([#695](https://github.com/ratatui-org/ratatui/issues/695))
|
||||
|
||||
````text
|
||||
This enables more consistently formatted code in doc comments,
|
||||
especially since ratatui heavily uses fluent setters.
|
||||
|
||||
See https://rust-lang.github.io/rustfmt/?version=v1.6.0#format_code_in_doc_comments
|
||||
````
|
||||
|
||||
- [d118565](https://github.com/ratatui-org/ratatui/commit/d118565ef60480fba8f2906ede81f875a562cb61)
|
||||
*(table)* Cleanup docs and builder methods ([#638](https://github.com/ratatui-org/ratatui/issues/638))
|
||||
|
||||
````text
|
||||
- Refactor the `table` module for better top to bottom readability by
|
||||
putting types first and arranging them in a logical order (Table, Row,
|
||||
Cell, other).
|
||||
|
||||
- Adds new methods for:
|
||||
- `Table::rows`
|
||||
- `Row::cells`
|
||||
- `Cell::new`
|
||||
- `Cell::content`
|
||||
- `TableState::new`
|
||||
- `TableState::selected_mut`
|
||||
|
||||
- Makes `HighlightSpacing::should_add` pub(crate) since it's an internal
|
||||
detail.
|
||||
|
||||
- Adds tests for all the new methods and simple property tests for all
|
||||
the other setter methods.
|
||||
````
|
||||
|
||||
- [dd22e72](https://github.com/ratatui-org/ratatui/commit/dd22e721e3aed24538eb08e46e40339cec636bcb)
|
||||
*(uncategorized)* Correct "builder methods" in docs and add `must_use` on widgets setters ([#655](https://github.com/ratatui-org/ratatui/issues/655))
|
||||
|
||||
- [18e19f6](https://github.com/ratatui-org/ratatui/commit/18e19f6ce6ae3ce9bd52110ab6cbd4ed4bcca5e6)
|
||||
*(uncategorized)* Fix breaking changes doc versions ([#639](https://github.com/ratatui-org/ratatui/issues/639))
|
||||
|
||||
````text
|
||||
Moves the layout::new change to unreleasedd section and adds the table change
|
||||
````
|
||||
|
||||
- [a58cce2](https://github.com/ratatui-org/ratatui/commit/a58cce2dba404fe394bbb298645bf3c40518fe1f)
|
||||
*(uncategorized)* Disable default benchmarking ([#598](https://github.com/ratatui-org/ratatui/issues/598))
|
||||
|
||||
````text
|
||||
Disables the default benchmarking behaviour for the lib target to fix unrecognized
|
||||
criterion benchmark arguments.
|
||||
|
||||
See https://bheisler.github.io/criterion.rs/book/faq.html#cargo-bench-gives-unrecognized-option-errors-for-valid-command-line-options for details
|
||||
````
|
||||
|
||||
### Continuous Integration
|
||||
|
||||
- [59b9c32](https://github.com/ratatui-org/ratatui/commit/59b9c32fbc2bc6725bdec42e63216024fab71493)
|
||||
*(codecov)* Adjust threshold and noise settings ([#615](https://github.com/ratatui-org/ratatui/issues/615))
|
||||
|
||||
````text
|
||||
Fixes https://github.com/ratatui-org/ratatui/issues/612
|
||||
````
|
||||
|
||||
- [03401cd](https://github.com/ratatui-org/ratatui/commit/03401cd46e6566af4d063bac11efc30f28b5358a)
|
||||
*(uncategorized)* Fix untrusted input in pr check workflow ([#680](https://github.com/ratatui-org/ratatui/issues/680))
|
||||
|
||||
### Contributors
|
||||
|
||||
Thank you so much to everyone that contributed to this release!
|
||||
|
||||
Here is the list of contributors who have contributed to `ratatui` for the first time!
|
||||
|
||||
* @rikonaka
|
||||
* @danny-burrows
|
||||
* @SOF3
|
||||
* @jan-ferdinand
|
||||
* @rhaskia
|
||||
* @asomers
|
||||
* @progval
|
||||
* @TylerBloom
|
||||
* @YeungKC
|
||||
* @lyuha
|
||||
|
||||
## [0.24.0](https://github.com/ratatui-org/ratatui/releases/tag/0.24.0) - 2023-10-23
|
||||
|
||||
We are excited to announce the new version of `ratatui` - a Rust library that's all about cooking up TUIs 🐭
|
||||
@@ -10,7 +495,7 @@ In this version, we've introduced features like window size API, enhanced chart
|
||||
The list of \*breaking changes\* can be found [here](https://github.com/ratatui-org/ratatui/blob/main/BREAKING-CHANGES.md) ⚠️.
|
||||
Also, we created various tutorials and walkthroughs in [Ratatui Book](https://github.com/ratatui-org/ratatui-book) which is available at <https://ratatui.rs> 🚀
|
||||
|
||||
✨ **Release highlights**: <https://ratatui.rs/highlights/v0.24.html>
|
||||
✨ **Release highlights**: <https://ratatui.rs/highlights/v024>
|
||||
|
||||
### Features
|
||||
|
||||
|
||||
33
Cargo.toml
33
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ratatui"
|
||||
version = "0.24.0" # crate version
|
||||
version = "0.25.0" # crate version
|
||||
authors = ["Florian Dehau <work@fdehau.com>", "The Ratatui Developers"]
|
||||
description = "A library that's all about cooking up terminal user interfaces"
|
||||
documentation = "https://docs.rs/ratatui/latest/ratatui/"
|
||||
@@ -18,7 +18,7 @@ exclude = [
|
||||
]
|
||||
autoexamples = true
|
||||
edition = "2021"
|
||||
rust-version = "1.67.0"
|
||||
rust-version = "1.70.0"
|
||||
|
||||
[badges]
|
||||
|
||||
@@ -31,7 +31,7 @@ serde = { version = "1", optional = true, features = ["derive"] }
|
||||
bitflags = "2.3"
|
||||
cassowary = "0.3"
|
||||
indoc = "2.0"
|
||||
itertools = "0.11"
|
||||
itertools = "0.12"
|
||||
paste = "1.0.2"
|
||||
strum = { version = "0.25", features = ["derive"] }
|
||||
time = { version = "0.3.11", optional = true, features = ["local-offset"] }
|
||||
@@ -39,6 +39,7 @@ unicode-segmentation = "1.10"
|
||||
unicode-width = "0.1"
|
||||
document-features = { version = "0.2.7", optional = true }
|
||||
lru = "0.12.0"
|
||||
stability = "0.1.1"
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = "1.0.71"
|
||||
@@ -47,11 +48,12 @@ better-panic = "0.3.0"
|
||||
cargo-husky = { version = "1.5.0", default-features = false, features = [
|
||||
"user-hooks",
|
||||
] }
|
||||
color-eyre = "0.6.2"
|
||||
criterion = { version = "0.5.1", features = ["html_reports"] }
|
||||
fakeit = "1.1"
|
||||
rand = "0.8.5"
|
||||
palette = "0.7.3"
|
||||
pretty_assertions = "1.4.0"
|
||||
rand = "0.8.5"
|
||||
|
||||
[features]
|
||||
#! The crate provides a set of optional features that can be enabled in your `cargo.toml` file.
|
||||
@@ -89,6 +91,21 @@ widget-calendar = ["dep:time"]
|
||||
## enables the backend code that sets the underline color.
|
||||
underline-color = ["dep:crossterm"]
|
||||
|
||||
#! The following features are unstable and may change in the future:
|
||||
|
||||
## Enable all unstable features.
|
||||
unstable = ["unstable-segment-size", "unstable-rendered-line-info"]
|
||||
|
||||
## Enables the [`Layout::segment_size`](crate::layout::Layout::segment_size) method which is experimental and may change in the
|
||||
## future. See [Issue #536](https://github.com/ratatui-org/ratatui/issues/536) for more details.
|
||||
unstable-segment-size = []
|
||||
|
||||
## Enables the [`Paragraph::line_count`](crate::widgets::Paragraph::line_count)
|
||||
## [`Paragraph::line_width`](crate::widgets::Paragraph::line_width) methods
|
||||
## which are experimental and may change in the future.
|
||||
## See [Issue 293](https://github.com/ratatui-org/ratatui/issues/293) for more details.
|
||||
unstable-rendered-line-info = []
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
# see https://doc.rust-lang.org/nightly/rustdoc/scraped-examples.html
|
||||
@@ -107,6 +124,9 @@ harness = false
|
||||
name = "list"
|
||||
harness = false
|
||||
|
||||
[lib]
|
||||
bench = false
|
||||
|
||||
[[bench]]
|
||||
name = "paragraph"
|
||||
harness = false
|
||||
@@ -213,6 +233,11 @@ name = "popup"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "ratatui-logo"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "scrollbar"
|
||||
required-features = ["crossterm"]
|
||||
|
||||
@@ -11,7 +11,7 @@ ALL_FEATURES = "all-widgets,macros,serde"
|
||||
# sets of flags, one for Windows and one for other platforms.
|
||||
# Windows: --features=all-widgets,macros,serde,crossterm,termwiz,underline-color
|
||||
# Other: --features=all-widgets,macros,serde,crossterm,termion,termwiz,underline-color
|
||||
ALL_FEATURES_FLAG = { source = "${CARGO_MAKE_RUST_TARGET_OS}", default_value = "--features=all-widgets,macros,serde,crossterm,termion,termwiz", mapping = { "windows" = "--features=all-widgets,macros,serde,crossterm,termwiz" } }
|
||||
ALL_FEATURES_FLAG = { source = "${CARGO_MAKE_RUST_TARGET_OS}", default_value = "--features=all-widgets,macros,serde,crossterm,termion,termwiz,unstable", mapping = { "windows" = "--features=all-widgets,macros,serde,crossterm,termwiz,unstable" } }
|
||||
|
||||
[tasks.default]
|
||||
alias = "ci"
|
||||
@@ -90,12 +90,21 @@ args = [
|
||||
"warnings",
|
||||
]
|
||||
|
||||
[tasks.install-nextest]
|
||||
description = "Install cargo-nextest"
|
||||
install_crate = { crate_name = "cargo-nextest", binary = "cargo-nextest", test_arg = "--help" }
|
||||
|
||||
[tasks.test]
|
||||
description = "Run tests"
|
||||
dependencies = ["test-doc"]
|
||||
run_task = { name = ["test-lib", "test-doc"] }
|
||||
|
||||
[tasks.test-lib]
|
||||
description = "Run default tests"
|
||||
dependencies = ["install-nextest"]
|
||||
command = "cargo"
|
||||
args = [
|
||||
"test",
|
||||
"nextest",
|
||||
"run",
|
||||
"--all-targets",
|
||||
"--no-default-features",
|
||||
"${ALL_FEATURES_FLAG}",
|
||||
@@ -109,9 +118,11 @@ args = ["test", "--doc", "--no-default-features", "${ALL_FEATURES_FLAG}"]
|
||||
[tasks.test-backend]
|
||||
# takes a command line parameter to specify the backend to test (e.g. "crossterm")
|
||||
description = "Run backend-specific tests"
|
||||
dependencies = ["install-nextest"]
|
||||
command = "cargo"
|
||||
args = [
|
||||
"test",
|
||||
"nextest",
|
||||
"run",
|
||||
"--all-targets",
|
||||
"--no-default-features",
|
||||
"--features",
|
||||
|
||||
120
README.md
120
README.md
@@ -25,27 +25,29 @@
|
||||
|
||||
<div align="center">
|
||||
|
||||
[![Crate Badge]](https://crates.io/crates/ratatui) [![License Badge]](./LICENSE) [![CI
|
||||
Badge]](https://github.com/ratatui-org/ratatui/actions?query=workflow%3ACI+) [![Docs
|
||||
Badge]](https://docs.rs/crate/ratatui/)<br>
|
||||
[![Dependencies Badge]](https://deps.rs/repo/github/ratatui-org/ratatui) [![Codecov
|
||||
Badge]](https://app.codecov.io/gh/ratatui-org/ratatui) [![Discord
|
||||
Badge]](https://discord.gg/pMCEU9hNEj) [![Matrix
|
||||
Badge]](https://matrix.to/#/#ratatui:matrix.org)<br>
|
||||
[Documentation](https://docs.rs/ratatui) · [Ratatui Book](https://ratatui.rs) ·
|
||||
[Examples](https://github.com/ratatui-org/ratatui/tree/main/examples) · [Report a
|
||||
bug](https://github.com/ratatui-org/ratatui/issues/new?labels=bug&projects=&template=bug_report.md)
|
||||
· [Request a
|
||||
Feature](https://github.com/ratatui-org/ratatui/issues/new?labels=enhancement&projects=&template=feature_request.md)
|
||||
[![Crate Badge]](https://crates.io/crates/ratatui)
|
||||
[![License Badge]](./LICENSE)
|
||||
[![CI Badge]](https://github.com/ratatui-org/ratatui/actions?query=workflow%3ACI+)
|
||||
[![Docs Badge]](https://docs.rs/crate/ratatui/)<br>
|
||||
[![Dependencies Badge]](https://deps.rs/repo/github/ratatui-org/ratatui)
|
||||
[![Codecov Badge]](https://app.codecov.io/gh/ratatui-org/ratatui)
|
||||
[![Discord Badge]](https://discord.gg/pMCEU9hNEj)
|
||||
[![Matrix Badge]](https://matrix.to/#/#ratatui:matrix.org)<br>
|
||||
|
||||
[Documentation](https://docs.rs/ratatui)
|
||||
· [Ratatui Website](https://ratatui.rs)
|
||||
· [Examples](https://github.com/ratatui-org/ratatui/tree/main/examples)
|
||||
· [Report a bug](https://github.com/ratatui-org/ratatui/issues/new?labels=bug&projects=&template=bug_report.md)
|
||||
· [Request a Feature](https://github.com/ratatui-org/ratatui/issues/new?labels=enhancement&projects=&template=feature_request.md)
|
||||
· [Send a Pull Request](https://github.com/ratatui-org/ratatui/compare)
|
||||
|
||||
</div>
|
||||
|
||||
# Ratatui
|
||||
|
||||
[Ratatui] is a crate for cooking up terminal user interfaces in rust. It is a lightweight
|
||||
library that provides a set of widgets and utilities to build complex rust TUIs. Ratatui was
|
||||
forked from the [Tui-rs crate] in 2023 in order to continue its development.
|
||||
[Ratatui] is a crate for cooking up terminal user interfaces in Rust. It is a lightweight
|
||||
library that provides a set of widgets and utilities to build complex Rust TUIs. Ratatui was
|
||||
forked from the [tui-rs] crate in 2023 in order to continue its development.
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -56,7 +58,7 @@ cargo add ratatui crossterm
|
||||
```
|
||||
|
||||
Ratatui uses [Crossterm] by default as it works on most platforms. See the [Installation]
|
||||
section of the [Ratatui Book] for more details on how to use other backends ([Termion] /
|
||||
section of the [Ratatui Website] for more details on how to use other backends ([Termion] /
|
||||
[Termwiz]).
|
||||
|
||||
## Introduction
|
||||
@@ -64,12 +66,12 @@ section of the [Ratatui Book] for more details on how to use other backends ([Te
|
||||
Ratatui is based on the principle of immediate rendering with intermediate buffers. This means
|
||||
that for each frame, your app must render all widgets that are supposed to be part of the UI.
|
||||
This is in contrast to the retained mode style of rendering where widgets are updated and then
|
||||
automatically redrawn on the next frame. See the [Rendering] section of the [Ratatui Book] for
|
||||
automatically redrawn on the next frame. See the [Rendering] section of the [Ratatui Website] for
|
||||
more info.
|
||||
|
||||
## Other documentation
|
||||
|
||||
- [Ratatui Book] - explains the library's concepts and provides step-by-step tutorials
|
||||
- [Ratatui Website] - explains the library's concepts and provides step-by-step tutorials
|
||||
- [Examples] - a collection of examples that demonstrate how to use the library.
|
||||
- [API Documentation] - the full API documentation for the library on docs.rs.
|
||||
- [Changelog] - generated by [git-cliff] utilizing [Conventional Commits].
|
||||
@@ -81,12 +83,11 @@ more info.
|
||||
The following example demonstrates the minimal amount of code necessary to setup a terminal and
|
||||
render "Hello World!". The full code for this example which contains a little more detail is in
|
||||
[hello_world.rs]. For more guidance on different ways to structure your application see the
|
||||
[Application Patterns] and [Hello World tutorial] sections in the [Ratatui Book] and the various
|
||||
[Application Patterns] and [Hello World tutorial] sections in the [Ratatui Website] and the various
|
||||
[Examples]. There are also several starter templates available:
|
||||
|
||||
- [rust-tui-template]
|
||||
- [ratatui-async-template] (book and template)
|
||||
- [simple-tui-rs]
|
||||
- [template]
|
||||
- [async-template] (book and template)
|
||||
|
||||
Every application built with `ratatui` needs to implement the following steps:
|
||||
|
||||
@@ -110,20 +111,20 @@ implements the [`Backend`] trait which has implementations for [Crossterm], [Ter
|
||||
|
||||
Most applications should enter the Alternate Screen when starting and leave it when exiting and
|
||||
also enable raw mode to disable line buffering and enable reading key events. See the [`backend`
|
||||
module] and the [Backends] section of the [Ratatui Book] for more info.
|
||||
module] and the [Backends] section of the [Ratatui Website] for more info.
|
||||
|
||||
### Drawing the UI
|
||||
|
||||
The drawing logic is delegated to a closure that takes a [`Frame`] instance as argument. The
|
||||
[`Frame`] provides the size of the area to draw to and allows the app to render any [`Widget`]
|
||||
using the provided [`render_widget`] method. See the [Widgets] section of the [Ratatui Book] for
|
||||
using the provided [`render_widget`] method. See the [Widgets] section of the [Ratatui Website] for
|
||||
more info.
|
||||
|
||||
### Handling events
|
||||
|
||||
Ratatui does not include any input handling. Instead event handling can be implemented by
|
||||
calling backend library methods directly. See the [Handling Events] section of the [Ratatui
|
||||
Book] for more info. For example, if you are using [Crossterm], you can use the
|
||||
Website] for more info. For example, if you are using [Crossterm], you can use the
|
||||
[`crossterm::event`] module to handle events.
|
||||
|
||||
### Example
|
||||
@@ -182,20 +183,21 @@ Running this example produces the following output:
|
||||
The library comes with a basic yet useful layout management object called [`Layout`] which
|
||||
allows you to split the available space into multiple areas and then render widgets in each
|
||||
area. This lets you describe a responsive terminal UI by nesting layouts. See the [Layout]
|
||||
section of the [Ratatui Book] for more info.
|
||||
section of the [Ratatui Website] for more info.
|
||||
|
||||
```rust
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
fn ui(frame: &mut Frame) {
|
||||
let main_layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
let main_layout = Layout::new(
|
||||
Direction::Vertical,
|
||||
[
|
||||
Constraint::Length(1),
|
||||
Constraint::Min(0),
|
||||
Constraint::Length(1),
|
||||
])
|
||||
.split(frame.size());
|
||||
]
|
||||
)
|
||||
.split(frame.size());
|
||||
frame.render_widget(
|
||||
Block::new().borders(Borders::TOP).title("Title Bar"),
|
||||
main_layout[0],
|
||||
@@ -205,10 +207,11 @@ fn ui(frame: &mut Frame) {
|
||||
main_layout[2],
|
||||
);
|
||||
|
||||
let inner_layout = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
.split(main_layout[1]);
|
||||
let inner_layout = Layout::new(
|
||||
Direction::Horizontal,
|
||||
[Constraint::Percentage(50), Constraint::Percentage(50)]
|
||||
)
|
||||
.split(main_layout[1]);
|
||||
frame.render_widget(
|
||||
Block::default().borders(Borders::ALL).title("Left"),
|
||||
inner_layout[0],
|
||||
@@ -234,22 +237,23 @@ The [`style` module] provides types that represent the various styling options.
|
||||
important one is [`Style`] which represents the foreground and background colors and the text
|
||||
attributes of a [`Span`]. The [`style` module] also provides a [`Stylize`] trait that allows
|
||||
short-hand syntax to apply a style to widgets and text. See the [Styling Text] section of the
|
||||
[Ratatui Book] for more info.
|
||||
[Ratatui Website] for more info.
|
||||
|
||||
```rust
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
fn ui(frame: &mut Frame) {
|
||||
let areas = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
let areas = Layout::new(
|
||||
Direction::Vertical,
|
||||
[
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
Constraint::Min(0),
|
||||
])
|
||||
.split(frame.size());
|
||||
]
|
||||
)
|
||||
.split(frame.size());
|
||||
|
||||
let span1 = Span::raw("Hello ");
|
||||
let span2 = Span::styled(
|
||||
@@ -285,21 +289,20 @@ Running this example produces the following output:
|
||||
|
||||
![docsrs-styling]
|
||||
|
||||
[Ratatui Book]: https://ratatui.rs
|
||||
[Installation]: https://ratatui.rs/installation.html
|
||||
[Rendering]: https://ratatui.rs/concepts/rendering/index.html
|
||||
[Application Patterns]: https://ratatui.rs/concepts/application_patterns/index.html
|
||||
[Hello World tutorial]: https://ratatui.rs/tutorial/hello_world.html
|
||||
[Backends]: https://ratatui.rs/concepts/backends/index.html
|
||||
[Widgets]: https://ratatui.rs/how-to/widgets/index.html
|
||||
[Handling Events]: https://ratatui.rs/concepts/event_handling.html
|
||||
[Layout]: https://ratatui.rs/how-to/layout/index.html
|
||||
[Styling Text]: https://ratatui.rs/how-to/render/style-text.html
|
||||
[rust-tui-template]: https://github.com/ratatui-org/rust-tui-template
|
||||
[ratatui-async-template]: https://ratatui-org.github.io/ratatui-async-template/
|
||||
[simple-tui-rs]: https://github.com/pmsanford/simple-tui-rs
|
||||
[Ratatui Website]: https://ratatui.rs/
|
||||
[Installation]: https://ratatui.rs/installation/
|
||||
[Rendering]: https://ratatui.rs/concepts/rendering/
|
||||
[Application Patterns]: https://ratatui.rs/concepts/application-patterns/
|
||||
[Hello World tutorial]: https://ratatui.rs/tutorials/hello-world/
|
||||
[Backends]: https://ratatui.rs/concepts/backends/
|
||||
[Widgets]: https://ratatui.rs/how-to/widgets/
|
||||
[Handling Events]: https://ratatui.rs/concepts/event-handling/
|
||||
[Layout]: https://ratatui.rs/how-to/layout/
|
||||
[Styling Text]: https://ratatui.rs/how-to/render/style-text/
|
||||
[template]: https://github.com/ratatui-org/template
|
||||
[async-template]: https://ratatui-org.github.io/async-template
|
||||
[Examples]: https://github.com/ratatui-org/ratatui/tree/main/examples
|
||||
[git-cliff]: https://github.com/orhun/git-cliff
|
||||
[git-cliff]: https://git-cliff.org
|
||||
[Conventional Commits]: https://www.conventionalcommits.org
|
||||
[API Documentation]: https://docs.rs/ratatui
|
||||
[Changelog]: https://github.com/ratatui-org/ratatui/blob/main/CHANGELOG.md
|
||||
@@ -325,7 +328,7 @@ Running this example produces the following output:
|
||||
[Crossterm]: https://crates.io/crates/crossterm
|
||||
[Termion]: https://crates.io/crates/termion
|
||||
[Termwiz]: https://crates.io/crates/termwiz
|
||||
[Tui-rs crate]: https://crates.io/crates/tui
|
||||
[tui-rs]: https://crates.io/crates/tui
|
||||
[hello_world.rs]: https://github.com/ratatui-org/ratatui/blob/main/examples/hello_world.rs
|
||||
[Crate Badge]: https://img.shields.io/crates/v/ratatui?logo=rust&style=flat-square
|
||||
[CI Badge]:
|
||||
@@ -402,7 +405,6 @@ be installed with `cargo install cargo-make`).
|
||||
`ratatui::style::Color`
|
||||
- [rust-tui-template](https://github.com/ratatui-org/rust-tui-template) — A template for
|
||||
bootstrapping a Rust TUI application with Tui-rs & crossterm
|
||||
- [simple-tui-rs](https://github.com/pmsanford/simple-tui-rs) — A simple example tui-rs app
|
||||
- [tui-builder](https://github.com/jkelleyrtp/tui-builder) — Batteries-included MVC framework for
|
||||
Tui-rs + Crossterm apps
|
||||
- [tui-clap](https://github.com/kegesch/tui-clap-rs) — Use clap-rs together with Tui-rs
|
||||
@@ -425,8 +427,8 @@ be installed with `cargo install cargo-make`).
|
||||
|
||||
## Apps
|
||||
|
||||
Check out the list of more than 50 [Apps using
|
||||
`Ratatui`](https://github.com/ratatui-org/ratatui/wiki/Apps-using-Ratatui)!
|
||||
Check out [awesome-ratatui](https://github.com/ratatui-org/awesome-ratatui) for a curated list of
|
||||
awesome apps/libraries built with `ratatui`!
|
||||
|
||||
## Alternatives
|
||||
|
||||
|
||||
9
SECURITY.md
Normal file
9
SECURITY.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
We only support the latest version of this crate.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
To report secuirity vulnerability, please use the form at https://github.com/ratatui-org/ratatui/security/advisories/new
|
||||
11
codecov.yml
11
codecov.yml
@@ -1,3 +1,14 @@
|
||||
coverage: # https://docs.codecov.com/docs/codecovyml-reference#coverage
|
||||
precision: 1 # e.g. 89.1%
|
||||
round: down
|
||||
range: 85..100 # https://docs.codecov.com/docs/coverage-configuration#section-range
|
||||
status: # https://docs.codecov.com/docs/commit-status
|
||||
project:
|
||||
default:
|
||||
threshold: 1% # Avoid false negatives
|
||||
ignore:
|
||||
- "examples"
|
||||
- "benches"
|
||||
comment: # https://docs.codecov.com/docs/pull-request-comments
|
||||
# make the comments less noisy
|
||||
require_changes: true
|
||||
|
||||
@@ -9,7 +9,7 @@ images themselves are stored in a separate git branch to avoid bloating the main
|
||||
This is the demo example from the main README and crate page. Source: [demo2](./demo2/).
|
||||
|
||||
```shell
|
||||
cargo run --example=demo2 --features=crossterm
|
||||
cargo run --example=demo2 --features="crossterm widget-calendar"
|
||||
```
|
||||
|
||||
![Demo2][demo2.gif]
|
||||
@@ -117,7 +117,10 @@ two square-ish pixels in the space of a single rectangular terminal cell.
|
||||
cargo run --example=colors_rgb --features=crossterm
|
||||
```
|
||||
|
||||
![Colors RGB][colors_rgb.png]
|
||||
Note: VHs renders full screen animations poorly, so this is a screen capture rather than the output
|
||||
of the VHS tape.
|
||||
|
||||
<https://github.com/ratatui-org/ratatui/assets/381361/485e775a-e0b5-4133-899b-1e8aeb56e774>
|
||||
|
||||
## Custom Widget
|
||||
|
||||
@@ -223,6 +226,18 @@ cargo run --example=popup --features=crossterm
|
||||
|
||||
![Popup][popup.gif]
|
||||
|
||||
## Ratatui-logo
|
||||
|
||||
A fun example of using half blocks to render graphics Source:
|
||||
[ratatui-logo.rs](./ratatui-logo.rs).
|
||||
|
||||
>
|
||||
```shell
|
||||
cargo run --example=ratatui-logo --features=crossterm
|
||||
```
|
||||
|
||||
![Ratatui Logo][ratatui-logo.gif]
|
||||
|
||||
## Scrollbar
|
||||
|
||||
Demonstrates the [`Scrollbar`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Scrollbar.html)
|
||||
@@ -271,7 +286,8 @@ cargo run --example=tabs --features=crossterm
|
||||
|
||||
Demonstrates one approach to accepting user input. Source [user_input.rs](./user_input.rs).
|
||||
|
||||
> [!NOTE] Consider using [`tui-textarea`](https://crates.io/crates/tui-textarea) or
|
||||
> [!NOTE]
|
||||
> Consider using [`tui-textarea`](https://crates.io/crates/tui-textarea) or
|
||||
> [`tui-input`](https://crates.io/crates/tui-input) crates for more functional text entry UIs.
|
||||
|
||||
```shell
|
||||
@@ -296,7 +312,6 @@ examples/generate.bash
|
||||
[canvas.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/canvas.gif?raw=true
|
||||
[chart.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/chart.gif?raw=true
|
||||
[colors.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/colors.gif?raw=true
|
||||
[colors_rgb.png]: https://github.com/ratatui-org/ratatui/blob/images/examples/colors_rgb.png?raw=true
|
||||
[custom_widget.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/custom_widget.gif?raw=true
|
||||
[demo.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/demo.gif?raw=true
|
||||
[demo2.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/demo2.gif?raw=true
|
||||
@@ -309,6 +324,7 @@ examples/generate.bash
|
||||
[panic.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/panic.gif?raw=true
|
||||
[paragraph.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/paragraph.gif?raw=true
|
||||
[popup.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/popup.gif?raw=true
|
||||
[ratatui-logo.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/ratatui-logo.gif?raw=true
|
||||
[scrollbar.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/scrollbar.gif?raw=true
|
||||
[sparkline.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/sparkline.gif?raw=true
|
||||
[table.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/table.gif?raw=true
|
||||
|
||||
@@ -175,7 +175,7 @@ fn make_dates(current_year: i32) -> CalendarEventStore {
|
||||
mod cals {
|
||||
use super::*;
|
||||
|
||||
pub(super) fn get_cal<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
|
||||
pub(super) fn get_cal<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
|
||||
use Month::*;
|
||||
match m {
|
||||
May => example1(m, y, es),
|
||||
@@ -188,7 +188,7 @@ mod cals {
|
||||
}
|
||||
}
|
||||
|
||||
fn default<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
|
||||
fn default<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
|
||||
let default_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.bg(Color::Rgb(50, 50, 50));
|
||||
@@ -198,7 +198,7 @@ mod cals {
|
||||
.default_style(default_style)
|
||||
}
|
||||
|
||||
fn example1<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
|
||||
fn example1<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
|
||||
let default_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.bg(Color::Rgb(50, 50, 50));
|
||||
@@ -209,7 +209,7 @@ mod cals {
|
||||
.show_month_header(Style::default())
|
||||
}
|
||||
|
||||
fn example2<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
|
||||
fn example2<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
|
||||
let header_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.add_modifier(Modifier::DIM)
|
||||
@@ -225,7 +225,7 @@ mod cals {
|
||||
.show_month_header(Style::default())
|
||||
}
|
||||
|
||||
fn example3<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
|
||||
fn example3<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
|
||||
let header_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.fg(Color::Green);
|
||||
@@ -241,7 +241,7 @@ mod cals {
|
||||
.show_month_header(Style::default())
|
||||
}
|
||||
|
||||
fn example4<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
|
||||
fn example4<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
|
||||
let header_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.fg(Color::Green);
|
||||
@@ -255,7 +255,7 @@ mod cals {
|
||||
.default_style(default_style)
|
||||
}
|
||||
|
||||
fn example5<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
|
||||
fn example5<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
|
||||
let header_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.fg(Color::Green);
|
||||
|
||||
@@ -59,10 +59,10 @@ impl App {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => break,
|
||||
KeyCode::Down => app.y += 1.0,
|
||||
KeyCode::Up => app.y -= 1.0,
|
||||
KeyCode::Right => app.x += 1.0,
|
||||
KeyCode::Left => app.x -= 1.0,
|
||||
KeyCode::Down | KeyCode::Char('j') => app.y += 1.0,
|
||||
KeyCode::Up | KeyCode::Char('k') => app.y -= 1.0,
|
||||
KeyCode::Right | KeyCode::Char('l') => app.x += 1.0,
|
||||
KeyCode::Left | KeyCode::Char('h') => app.x -= 1.0,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,18 +9,10 @@ use crossterm::{
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
const DATA: [(f64, f64); 5] = [(0.0, 0.0), (1.0, 1.0), (2.0, 2.0), (3.0, 3.0), (4.0, 4.0)];
|
||||
const DATA2: [(f64, f64); 7] = [
|
||||
(0.0, 0.0),
|
||||
(10.0, 1.0),
|
||||
(20.0, 0.5),
|
||||
(30.0, 1.5),
|
||||
(40.0, 1.0),
|
||||
(50.0, 2.5),
|
||||
(60.0, 3.0),
|
||||
];
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{block::Title, *},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SinSignal {
|
||||
@@ -142,14 +134,28 @@ fn run_app<B: Backend>(
|
||||
|
||||
fn ui(f: &mut Frame, app: &App) {
|
||||
let size = f.size();
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Ratio(1, 3),
|
||||
Constraint::Ratio(1, 3),
|
||||
Constraint::Ratio(1, 3),
|
||||
])
|
||||
.split(size);
|
||||
let vertical_chunks = Layout::new(
|
||||
Direction::Vertical,
|
||||
[Constraint::Percentage(40), Constraint::Percentage(60)],
|
||||
)
|
||||
.split(size);
|
||||
|
||||
// top chart
|
||||
render_chart1(f, vertical_chunks[0], app);
|
||||
|
||||
let horizontal_chunks = Layout::new(
|
||||
Direction::Horizontal,
|
||||
[Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)],
|
||||
)
|
||||
.split(vertical_chunks[1]);
|
||||
|
||||
// bottom left
|
||||
render_line_chart(f, horizontal_chunks[0]);
|
||||
// bottom right
|
||||
render_scatter(f, horizontal_chunks[1]);
|
||||
}
|
||||
|
||||
fn render_chart1(f: &mut Frame, area: Rect, app: &App) {
|
||||
let x_labels = vec![
|
||||
Span::styled(
|
||||
format!("{}", app.window[0]),
|
||||
@@ -194,61 +200,164 @@ fn ui(f: &mut Frame, app: &App) {
|
||||
.labels(vec!["-20".bold(), "0".into(), "20".bold()])
|
||||
.bounds([-20.0, 20.0]),
|
||||
);
|
||||
f.render_widget(chart, chunks[0]);
|
||||
|
||||
let datasets = vec![Dataset::default()
|
||||
.name("data")
|
||||
.marker(symbols::Marker::Braille)
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.graph_type(GraphType::Line)
|
||||
.data(&DATA)];
|
||||
let chart = Chart::new(datasets)
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Chart 2".cyan().bold())
|
||||
.borders(Borders::ALL),
|
||||
)
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
.title("X Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.bounds([0.0, 5.0])
|
||||
.labels(vec!["0".bold(), "2.5".into(), "5.0".bold()]),
|
||||
)
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.title("Y Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.bounds([0.0, 5.0])
|
||||
.labels(vec!["0".bold(), "2.5".into(), "5.0".bold()]),
|
||||
);
|
||||
f.render_widget(chart, chunks[1]);
|
||||
|
||||
let datasets = vec![Dataset::default()
|
||||
.name("data")
|
||||
.marker(symbols::Marker::Braille)
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.graph_type(GraphType::Line)
|
||||
.data(&DATA2)];
|
||||
let chart = Chart::new(datasets)
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Chart 3".cyan().bold())
|
||||
.borders(Borders::ALL),
|
||||
)
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
.title("X Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.bounds([0.0, 50.0])
|
||||
.labels(vec!["0".bold(), "25".into(), "50".bold()]),
|
||||
)
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.title("Y Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.bounds([0.0, 5.0])
|
||||
.labels(vec!["0".bold(), "2.5".into(), "5".bold()]),
|
||||
);
|
||||
f.render_widget(chart, chunks[2]);
|
||||
f.render_widget(chart, area);
|
||||
}
|
||||
|
||||
fn render_line_chart(f: &mut Frame, area: Rect) {
|
||||
let datasets = vec![Dataset::default()
|
||||
.name("Line from only 2 points")
|
||||
.marker(symbols::Marker::Braille)
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.graph_type(GraphType::Line)
|
||||
.data(&[(1., 1.), (4., 4.)])];
|
||||
|
||||
let chart = Chart::new(datasets)
|
||||
.block(
|
||||
Block::default()
|
||||
.title(
|
||||
Title::default()
|
||||
.content("Line chart".cyan().bold())
|
||||
.alignment(Alignment::Center),
|
||||
)
|
||||
.borders(Borders::ALL),
|
||||
)
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
.title("X Axis")
|
||||
.style(Style::default().gray())
|
||||
.bounds([0.0, 5.0])
|
||||
.labels(vec!["0".bold(), "2.5".into(), "5.0".bold()]),
|
||||
)
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.title("Y Axis")
|
||||
.style(Style::default().gray())
|
||||
.bounds([0.0, 5.0])
|
||||
.labels(vec!["0".bold(), "2.5".into(), "5.0".bold()]),
|
||||
)
|
||||
.legend_position(Some(LegendPosition::TopLeft))
|
||||
.hidden_legend_constraints((Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)));
|
||||
|
||||
f.render_widget(chart, area)
|
||||
}
|
||||
|
||||
fn render_scatter(f: &mut Frame, area: Rect) {
|
||||
let datasets = vec![
|
||||
Dataset::default()
|
||||
.name("Heavy")
|
||||
.marker(Marker::Dot)
|
||||
.graph_type(GraphType::Scatter)
|
||||
.style(Style::new().yellow())
|
||||
.data(&HEAVY_PAYLOAD_DATA),
|
||||
Dataset::default()
|
||||
.name("Medium")
|
||||
.marker(Marker::Braille)
|
||||
.graph_type(GraphType::Scatter)
|
||||
.style(Style::new().magenta())
|
||||
.data(&MEDIUM_PAYLOAD_DATA),
|
||||
Dataset::default()
|
||||
.name("Small")
|
||||
.marker(Marker::Dot)
|
||||
.graph_type(GraphType::Scatter)
|
||||
.style(Style::new().cyan())
|
||||
.data(&SMALL_PAYLOAD_DATA),
|
||||
];
|
||||
|
||||
let chart = Chart::new(datasets)
|
||||
.block(
|
||||
Block::new().borders(Borders::all()).title(
|
||||
Title::default()
|
||||
.content("Scatter chart".cyan().bold())
|
||||
.alignment(Alignment::Center),
|
||||
),
|
||||
)
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
.title("Year")
|
||||
.bounds([1960., 2020.])
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.labels(vec!["1960".into(), "1990".into(), "2020".into()]),
|
||||
)
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.title("Cost")
|
||||
.bounds([0., 75000.])
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.labels(vec!["0".into(), "37 500".into(), "75 000".into()]),
|
||||
)
|
||||
.hidden_legend_constraints((Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)));
|
||||
|
||||
f.render_widget(chart, area);
|
||||
}
|
||||
|
||||
// Data from https://ourworldindata.org/space-exploration-satellites
|
||||
const HEAVY_PAYLOAD_DATA: [(f64, f64); 9] = [
|
||||
(1965., 8200.),
|
||||
(1967., 5400.),
|
||||
(1981., 65400.),
|
||||
(1989., 30800.),
|
||||
(1997., 10200.),
|
||||
(2004., 11600.),
|
||||
(2014., 4500.),
|
||||
(2016., 7900.),
|
||||
(2018., 1500.),
|
||||
];
|
||||
|
||||
const MEDIUM_PAYLOAD_DATA: [(f64, f64); 29] = [
|
||||
(1963., 29500.),
|
||||
(1964., 30600.),
|
||||
(1965., 177900.),
|
||||
(1965., 21000.),
|
||||
(1966., 17900.),
|
||||
(1966., 8400.),
|
||||
(1975., 17500.),
|
||||
(1982., 8300.),
|
||||
(1985., 5100.),
|
||||
(1988., 18300.),
|
||||
(1990., 38800.),
|
||||
(1990., 9900.),
|
||||
(1991., 18700.),
|
||||
(1992., 9100.),
|
||||
(1994., 10500.),
|
||||
(1994., 8500.),
|
||||
(1994., 8700.),
|
||||
(1997., 6200.),
|
||||
(1999., 18000.),
|
||||
(1999., 7600.),
|
||||
(1999., 8900.),
|
||||
(1999., 9600.),
|
||||
(2000., 16000.),
|
||||
(2001., 10000.),
|
||||
(2002., 10400.),
|
||||
(2002., 8100.),
|
||||
(2010., 2600.),
|
||||
(2013., 13600.),
|
||||
(2017., 8000.),
|
||||
];
|
||||
|
||||
const SMALL_PAYLOAD_DATA: [(f64, f64); 23] = [
|
||||
(1961., 118500.),
|
||||
(1962., 14900.),
|
||||
(1975., 21400.),
|
||||
(1980., 32800.),
|
||||
(1988., 31100.),
|
||||
(1990., 41100.),
|
||||
(1993., 23600.),
|
||||
(1994., 20600.),
|
||||
(1994., 34600.),
|
||||
(1996., 50600.),
|
||||
(1997., 19200.),
|
||||
(1997., 45800.),
|
||||
(1998., 19100.),
|
||||
(2000., 73100.),
|
||||
(2003., 11200.),
|
||||
(2008., 12600.),
|
||||
(2010., 30500.),
|
||||
(2012., 20000.),
|
||||
(2013., 10600.),
|
||||
(2013., 34500.),
|
||||
(2015., 10600.),
|
||||
(2018., 23100.),
|
||||
(2019., 17300.),
|
||||
];
|
||||
|
||||
@@ -2,63 +2,85 @@
|
||||
///
|
||||
/// Requires a terminal that supports 24-bit color (true color) and unicode.
|
||||
use std::{
|
||||
error::Error,
|
||||
io::{stdout, Stdout},
|
||||
rc::Rc,
|
||||
time::Duration,
|
||||
io::stdout,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use color_eyre::config::HookBuilder;
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode, KeyEventKind},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
};
|
||||
use palette::{
|
||||
convert::{FromColorUnclamped, IntoColorUnclamped},
|
||||
Okhsv, Srgb,
|
||||
};
|
||||
use palette::{convert::FromColorUnclamped, Okhsv, Srgb};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
type Result<T> = std::result::Result<T, Box<dyn Error>>;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
install_panic_hook();
|
||||
App::new()?.run()
|
||||
fn main() -> color_eyre::Result<()> {
|
||||
App::run()
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct App {
|
||||
terminal: Terminal<CrosstermBackend<Stdout>>,
|
||||
should_quit: bool,
|
||||
// a 2d vec of the colors to render, calculated when the size changes as this is expensive
|
||||
// to calculate every frame
|
||||
colors: Vec<Vec<Color>>,
|
||||
last_size: Rect,
|
||||
fps: Fps,
|
||||
frame_count: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Fps {
|
||||
frame_count: usize,
|
||||
last_instant: Instant,
|
||||
fps: Option<f32>,
|
||||
}
|
||||
|
||||
struct AppWidget<'a> {
|
||||
title: Paragraph<'a>,
|
||||
fps_widget: FpsWidget<'a>,
|
||||
rgb_colors_widget: RgbColorsWidget<'a>,
|
||||
}
|
||||
|
||||
struct FpsWidget<'a> {
|
||||
fps: &'a Fps,
|
||||
}
|
||||
|
||||
struct RgbColorsWidget<'a> {
|
||||
/// The colors to render - should be double the height of the area
|
||||
colors: &'a Vec<Vec<Color>>,
|
||||
/// the number of elapsed frames that have passed - used to animate the colors
|
||||
frame_count: usize,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new() -> Result<Self> {
|
||||
Ok(Self {
|
||||
terminal: Terminal::new(CrosstermBackend::new(stdout()))?,
|
||||
should_quit: false,
|
||||
})
|
||||
}
|
||||
pub fn run() -> color_eyre::Result<()> {
|
||||
install_panic_hook()?;
|
||||
|
||||
pub fn run(mut self) -> Result<()> {
|
||||
init_terminal()?;
|
||||
self.terminal.clear()?;
|
||||
while !self.should_quit {
|
||||
self.draw()?;
|
||||
self.handle_events()?;
|
||||
let mut terminal = init_terminal()?;
|
||||
let mut app = Self::default();
|
||||
|
||||
while !app.should_quit {
|
||||
app.tick();
|
||||
terminal.draw(|frame| {
|
||||
let size = frame.size();
|
||||
app.setup_colors(size);
|
||||
frame.render_widget(AppWidget::new(&app), size);
|
||||
})?;
|
||||
app.handle_events()?;
|
||||
}
|
||||
restore_terminal()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw(&mut self) -> Result<()> {
|
||||
self.terminal.draw(|frame| {
|
||||
frame.render_widget(RgbColors, frame.size());
|
||||
})?;
|
||||
Ok(())
|
||||
fn tick(&mut self) {
|
||||
self.frame_count += 1;
|
||||
self.fps.tick();
|
||||
}
|
||||
|
||||
fn handle_events(&mut self) -> Result<()> {
|
||||
if event::poll(Duration::from_millis(100))? {
|
||||
fn handle_events(&mut self) -> color_eyre::Result<()> {
|
||||
if event::poll(Duration::from_secs_f32(1.0 / 60.0))? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
|
||||
self.should_quit = true;
|
||||
@@ -67,80 +89,140 @@ impl App {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for App {
|
||||
fn drop(&mut self) {
|
||||
let _ = restore_terminal();
|
||||
fn setup_colors(&mut self, size: Rect) {
|
||||
// only update the colors if the size has changed since the last time we rendered
|
||||
if self.last_size.width == size.width && self.last_size.height == size.height {
|
||||
return;
|
||||
}
|
||||
self.last_size = size;
|
||||
let Rect { width, height, .. } = size;
|
||||
// double the height because each screen row has two rows of half block pixels
|
||||
let height = height * 2;
|
||||
self.colors.clear();
|
||||
for y in 0..height {
|
||||
let mut row = Vec::new();
|
||||
for x in 0..width {
|
||||
let hue = x as f32 * 360.0 / width as f32;
|
||||
let value = (height - y) as f32 / height as f32;
|
||||
let saturation = Okhsv::max_saturation();
|
||||
let color = Okhsv::new(hue, saturation, value);
|
||||
let color = Srgb::<f32>::from_color_unclamped(color);
|
||||
let color: Srgb<u8> = color.into_format();
|
||||
let color = Color::Rgb(color.red, color.green, color.blue);
|
||||
row.push(color);
|
||||
}
|
||||
self.colors.push(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct RgbColors;
|
||||
impl Fps {
|
||||
fn tick(&mut self) {
|
||||
self.frame_count += 1;
|
||||
let elapsed = self.last_instant.elapsed();
|
||||
// update the fps every second, but only if we've rendered at least 2 frames (to avoid
|
||||
// noise in the fps calculation)
|
||||
if elapsed > Duration::from_secs(1) && self.frame_count > 2 {
|
||||
self.fps = Some(self.frame_count as f32 / elapsed.as_secs_f32());
|
||||
self.frame_count = 0;
|
||||
self.last_instant = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for RgbColors {
|
||||
impl Default for Fps {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
frame_count: 0,
|
||||
last_instant: Instant::now(),
|
||||
fps: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> AppWidget<'a> {
|
||||
fn new(app: &'a App) -> Self {
|
||||
let title =
|
||||
Paragraph::new("colors_rgb example. Press q to quit").alignment(Alignment::Center);
|
||||
Self {
|
||||
title,
|
||||
fps_widget: FpsWidget { fps: &app.fps },
|
||||
rgb_colors_widget: RgbColorsWidget {
|
||||
colors: &app.colors,
|
||||
frame_count: app.frame_count,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for AppWidget<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let layout = Self::layout(area);
|
||||
Self::render_title(layout[0], buf);
|
||||
Self::render_colors(layout[1], buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl RgbColors {
|
||||
fn layout(area: Rect) -> Rc<[Rect]> {
|
||||
Layout::default()
|
||||
let main_layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Length(1), Constraint::Min(0)])
|
||||
.split(area)
|
||||
}
|
||||
.split(area);
|
||||
let title_layout = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Min(0), Constraint::Length(8)])
|
||||
.split(main_layout[0]);
|
||||
|
||||
fn render_title(area: Rect, buf: &mut Buffer) {
|
||||
Paragraph::new("colors_rgb example. Press q to quit")
|
||||
.dark_gray()
|
||||
.alignment(Alignment::Center)
|
||||
.render(area, buf);
|
||||
self.title.render(title_layout[0], buf);
|
||||
self.fps_widget.render(title_layout[1], buf);
|
||||
self.rgb_colors_widget.render(main_layout[1], buf);
|
||||
}
|
||||
}
|
||||
|
||||
/// Render a colored grid of half block characters (`"▀"`) each with a different RGB color.
|
||||
fn render_colors(area: Rect, buf: &mut Buffer) {
|
||||
impl Widget for RgbColorsWidget<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let colors = self.colors;
|
||||
for (xi, x) in (area.left()..area.right()).enumerate() {
|
||||
// animate the colors by shifting the x index by the frame number
|
||||
let xi = (xi + self.frame_count) % (area.width as usize);
|
||||
for (yi, y) in (area.top()..area.bottom()).enumerate() {
|
||||
let hue = xi as f32 * 360.0 / area.width as f32;
|
||||
|
||||
let value_fg = (yi as f32) / (area.height as f32 - 0.5);
|
||||
let fg = Okhsv::<f32>::new(hue, Okhsv::max_saturation(), value_fg);
|
||||
let fg: Srgb = fg.into_color_unclamped();
|
||||
let fg: Srgb<u8> = fg.into_format();
|
||||
let fg = Color::Rgb(fg.red, fg.green, fg.blue);
|
||||
|
||||
let value_bg = (yi as f32 + 0.5) / (area.height as f32 - 0.5);
|
||||
let bg = Okhsv::new(hue, Okhsv::max_saturation(), value_bg);
|
||||
let bg = Srgb::<f32>::from_color_unclamped(bg);
|
||||
let bg: Srgb<u8> = bg.into_format();
|
||||
let bg = Color::Rgb(bg.red, bg.green, bg.blue);
|
||||
|
||||
let fg = colors[yi * 2][xi];
|
||||
let bg = colors[yi * 2 + 1][xi];
|
||||
buf.get_mut(x, y).set_char('▀').set_fg(fg).set_bg(bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Install a panic hook that restores the terminal before panicking.
|
||||
fn install_panic_hook() {
|
||||
better_panic::install();
|
||||
let prev_hook = std::panic::take_hook();
|
||||
std::panic::set_hook(Box::new(move |info| {
|
||||
let _ = restore_terminal();
|
||||
prev_hook(info);
|
||||
}));
|
||||
impl<'a> Widget for FpsWidget<'a> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
if let Some(fps) = self.fps.fps {
|
||||
let text = format!("{:.1} fps", fps);
|
||||
Paragraph::new(text).render(area, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn init_terminal() -> Result<()> {
|
||||
enable_raw_mode()?;
|
||||
stdout().execute(EnterAlternateScreen)?;
|
||||
/// Install a panic hook that restores the terminal before panicking.
|
||||
fn install_panic_hook() -> color_eyre::Result<()> {
|
||||
let (panic, error) = HookBuilder::default().into_hooks();
|
||||
let panic = panic.into_panic_hook();
|
||||
let error = error.into_eyre_hook();
|
||||
color_eyre::eyre::set_hook(Box::new(move |e| {
|
||||
let _ = restore_terminal();
|
||||
error(e)
|
||||
}))?;
|
||||
std::panic::set_hook(Box::new(move |info| {
|
||||
let _ = restore_terminal();
|
||||
panic(info)
|
||||
}));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn restore_terminal() -> Result<()> {
|
||||
fn init_terminal() -> color_eyre::Result<Terminal<impl Backend>> {
|
||||
enable_raw_mode()?;
|
||||
stdout().execute(EnterAlternateScreen)?;
|
||||
let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
|
||||
terminal.clear()?;
|
||||
terminal.hide_cursor()?;
|
||||
Ok(terminal)
|
||||
}
|
||||
|
||||
fn restore_terminal() -> color_eyre::Result<()> {
|
||||
disable_raw_mode()?;
|
||||
stdout().execute(LeaveAlternateScreen)?;
|
||||
Ok(())
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/colors_rgb.tape`
|
||||
|
||||
# note that this script sometimes results in the gif having screen tearing
|
||||
# issues. I'm not sure why, but it's not a problem with the library.
|
||||
Output "target/colors_rgb.gif"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 1200
|
||||
Set Height 800
|
||||
Set Height 1200
|
||||
|
||||
# unsure if these help the screen tearing issue, but they don't hurt
|
||||
Set Framerate 60
|
||||
Set CursorBlink false
|
||||
|
||||
Hide
|
||||
Type "cargo run --example=colors_rgb --features=crossterm"
|
||||
Type "cargo run --example=colors_rgb --features=crossterm --release"
|
||||
Enter
|
||||
Sleep 2s
|
||||
Screenshot "target/colors_rgb.png"
|
||||
# Screenshot "target/colors_rgb.png"
|
||||
Show
|
||||
Sleep 1s
|
||||
Sleep 10s
|
||||
|
||||
@@ -216,12 +216,12 @@ fn handle_key_event(
|
||||
) -> ControlFlow<()> {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return ControlFlow::Break(()),
|
||||
KeyCode::Left => {
|
||||
KeyCode::Left | KeyCode::Char('h') => {
|
||||
button_states[*selected_button] = State::Normal;
|
||||
*selected_button = selected_button.saturating_sub(1);
|
||||
button_states[*selected_button] = State::Selected;
|
||||
}
|
||||
KeyCode::Right => {
|
||||
KeyCode::Right | KeyCode::Char('l') => {
|
||||
button_states[*selected_button] = State::Normal;
|
||||
*selected_button = selected_button.saturating_add(1).min(2);
|
||||
button_states[*selected_button] = State::Selected;
|
||||
|
||||
@@ -55,11 +55,11 @@ fn run_app<B: Backend>(
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
KeyCode::Left | KeyCode::Char('h') => app.on_left(),
|
||||
KeyCode::Up | KeyCode::Char('k') => app.on_up(),
|
||||
KeyCode::Right | KeyCode::Char('l') => app.on_right(),
|
||||
KeyCode::Down | KeyCode::Char('j') => app.on_down(),
|
||||
KeyCode::Char(c) => app.on_key(c),
|
||||
KeyCode::Left => app.on_left(),
|
||||
KeyCode::Up => app.on_up(),
|
||||
KeyCode::Right => app.on_right(),
|
||||
KeyCode::Down => app.on_down(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,11 +39,11 @@ fn run_app<B: Backend>(
|
||||
|
||||
match events.recv()? {
|
||||
Event::Input(key) => match key {
|
||||
Key::Up | Key::Char('k') => app.on_up(),
|
||||
Key::Down | Key::Char('j') => app.on_down(),
|
||||
Key::Left | Key::Char('h') => app.on_left(),
|
||||
Key::Right | Key::Char('l') => app.on_right(),
|
||||
Key::Char(c) => app.on_key(c),
|
||||
Key::Up => app.on_up(),
|
||||
Key::Down => app.on_down(),
|
||||
Key::Left => app.on_left(),
|
||||
Key::Right => app.on_right(),
|
||||
_ => {}
|
||||
},
|
||||
Event::Tick => app.on_tick(),
|
||||
|
||||
@@ -45,10 +45,10 @@ fn run_app(
|
||||
{
|
||||
match input {
|
||||
InputEvent::Key(key_code) => match key_code.key {
|
||||
KeyCode::UpArrow => app.on_up(),
|
||||
KeyCode::DownArrow => app.on_down(),
|
||||
KeyCode::LeftArrow => app.on_left(),
|
||||
KeyCode::RightArrow => app.on_right(),
|
||||
KeyCode::UpArrow | KeyCode::Char('k') => app.on_up(),
|
||||
KeyCode::DownArrow | KeyCode::Char('j') => app.on_down(),
|
||||
KeyCode::LeftArrow | KeyCode::Char('h') => app.on_left(),
|
||||
KeyCode::RightArrow | KeyCode::Char('l') => app.on_right(),
|
||||
KeyCode::Char(c) => app.on_key(c),
|
||||
_ => {}
|
||||
},
|
||||
|
||||
@@ -289,18 +289,20 @@ fn draw_second_tab(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
};
|
||||
Row::new(vec![s.name, s.location, s.status]).style(style)
|
||||
});
|
||||
let table = Table::new(rows)
|
||||
.header(
|
||||
Row::new(vec!["Server", "Location", "Status"])
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.bottom_margin(1),
|
||||
)
|
||||
.block(Block::default().title("Servers").borders(Borders::ALL))
|
||||
.widths(&[
|
||||
let table = Table::new(
|
||||
rows,
|
||||
[
|
||||
Constraint::Length(15),
|
||||
Constraint::Length(15),
|
||||
Constraint::Length(10),
|
||||
]);
|
||||
],
|
||||
)
|
||||
.header(
|
||||
Row::new(vec!["Server", "Location", "Status"])
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.bottom_margin(1),
|
||||
)
|
||||
.block(Block::default().title("Servers").borders(Borders::ALL));
|
||||
f.render_widget(table, chunks[0]);
|
||||
|
||||
let map = Canvas::default()
|
||||
@@ -393,12 +395,14 @@ fn draw_third_tab(f: &mut Frame, _app: &mut App, area: Rect) {
|
||||
Row::new(cells)
|
||||
})
|
||||
.collect();
|
||||
let table = Table::new(items)
|
||||
.block(Block::default().title("Colors").borders(Borders::ALL))
|
||||
.widths(&[
|
||||
let table = Table::new(
|
||||
items,
|
||||
[
|
||||
Constraint::Ratio(1, 3),
|
||||
Constraint::Ratio(1, 3),
|
||||
Constraint::Ratio(1, 3),
|
||||
]);
|
||||
],
|
||||
)
|
||||
.block(Block::default().title("Colors").borders(Borders::ALL));
|
||||
f.render_widget(table, chunks[0]);
|
||||
}
|
||||
|
||||
@@ -146,10 +146,9 @@ fn render_ingredients(selected_row: usize, area: Rect, buf: &mut Buffer) {
|
||||
let rows = INGREDIENTS.iter().map(|&i| i.into()).collect_vec();
|
||||
let theme = THEME.recipe;
|
||||
StatefulWidget::render(
|
||||
Table::new(rows)
|
||||
Table::new(rows, [Constraint::Length(7), Constraint::Length(30)])
|
||||
.block(Block::new().style(theme.ingredients))
|
||||
.header(Row::new(vec!["Qty", "Ingredient"]).style(theme.ingredients_header))
|
||||
.widths(&[Constraint::Length(7), Constraint::Length(30)])
|
||||
.highlight_style(Style::new().light_yellow()),
|
||||
area,
|
||||
buf,
|
||||
|
||||
@@ -50,9 +50,8 @@ fn render_hops(selected_row: usize, area: Rect, buf: &mut Buffer) {
|
||||
.title_alignment(Alignment::Center)
|
||||
.padding(Padding::new(1, 1, 1, 1));
|
||||
StatefulWidget::render(
|
||||
Table::new(rows)
|
||||
Table::new(rows, [Constraint::Max(100), Constraint::Length(15)])
|
||||
.header(Row::new(vec!["Host", "Address"]).set_style(THEME.traceroute.header))
|
||||
.widths(&[Constraint::Max(100), Constraint::Length(15)])
|
||||
.highlight_style(THEME.traceroute.selected)
|
||||
.block(block),
|
||||
area,
|
||||
|
||||
@@ -181,9 +181,9 @@ fn run_app<B: Backend>(
|
||||
if key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Left => app.items.unselect(),
|
||||
KeyCode::Down => app.items.next(),
|
||||
KeyCode::Up => app.items.previous(),
|
||||
KeyCode::Left | KeyCode::Char('h') => app.items.unselect(),
|
||||
KeyCode::Down | KeyCode::Char('j') => app.items.next(),
|
||||
KeyCode::Up | KeyCode::Char('k') => app.items.previous(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -273,6 +273,6 @@ fn ui(f: &mut Frame, app: &mut App) {
|
||||
.collect();
|
||||
let events_list = List::new(events)
|
||||
.block(Block::default().borders(Borders::ALL).title("List"))
|
||||
.start_corner(Corner::BottomLeft);
|
||||
.direction(ListDirection::BottomToTop);
|
||||
f.render_widget(events_list, chunks[1]);
|
||||
}
|
||||
|
||||
71
examples/ratatui-logo.rs
Normal file
71
examples/ratatui-logo.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
use std::{
|
||||
io::{self, stdout},
|
||||
thread::sleep,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
||||
use indoc::indoc;
|
||||
use itertools::izip;
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
/// A fun example of using half block characters to draw a logo
|
||||
fn main() -> io::Result<()> {
|
||||
let r = indoc! {"
|
||||
▄▄▄
|
||||
█▄▄▀
|
||||
█ █
|
||||
"}
|
||||
.lines();
|
||||
let a = indoc! {"
|
||||
▄▄
|
||||
█▄▄█
|
||||
█ █
|
||||
"}
|
||||
.lines();
|
||||
let t = indoc! {"
|
||||
▄▄▄
|
||||
█
|
||||
█
|
||||
"}
|
||||
.lines();
|
||||
let u = indoc! {"
|
||||
▄ ▄
|
||||
█ █
|
||||
▀▄▄▀
|
||||
"}
|
||||
.lines();
|
||||
let i = indoc! {"
|
||||
▄
|
||||
█
|
||||
█
|
||||
"}
|
||||
.lines();
|
||||
let mut terminal = init()?;
|
||||
terminal.draw(|frame| {
|
||||
let logo = izip!(r, a.clone(), t.clone(), a, t, u, i)
|
||||
.map(|(r, a, t, a2, t2, u, i)| {
|
||||
format!("{:5}{:5}{:4}{:5}{:4}{:5}{:5}", r, a, t, a2, t2, u, i)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
frame.render_widget(Paragraph::new(logo), frame.size());
|
||||
})?;
|
||||
sleep(Duration::from_secs(5));
|
||||
restore()?;
|
||||
println!();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn init() -> io::Result<Terminal<impl Backend>> {
|
||||
enable_raw_mode()?;
|
||||
let options = TerminalOptions {
|
||||
viewport: Viewport::Inline(3),
|
||||
};
|
||||
Terminal::with_options(CrosstermBackend::new(stdout()), options)
|
||||
}
|
||||
|
||||
pub fn restore() -> io::Result<()> {
|
||||
disable_raw_mode()?;
|
||||
Ok(())
|
||||
}
|
||||
12
examples/ratatui-logo.tape
Normal file
12
examples/ratatui-logo.tape
Normal file
@@ -0,0 +1,12 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/popup.tape`
|
||||
Output "target/ratatui-logo.gif"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 550
|
||||
Set Height 220
|
||||
Hide
|
||||
Type "cargo run --example=ratatui-logo --features=crossterm"
|
||||
Enter
|
||||
Sleep 2s
|
||||
Show
|
||||
Sleep 2s
|
||||
@@ -104,8 +104,8 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
|
||||
if key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Down => app.next(),
|
||||
KeyCode::Up => app.previous(),
|
||||
KeyCode::Down | KeyCode::Char('j') => app.next(),
|
||||
KeyCode::Up | KeyCode::Char('k') => app.previous(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -127,6 +127,13 @@ fn ui(f: &mut Frame, app: &mut App) {
|
||||
.style(normal_style)
|
||||
.height(1)
|
||||
.bottom_margin(1);
|
||||
let footer_cells = ["Footer1", "Footer2", "Footer3"]
|
||||
.iter()
|
||||
.map(|f| Cell::from(*f).style(Style::default().fg(Color::Yellow)));
|
||||
let footer = Row::new(footer_cells)
|
||||
.style(normal_style)
|
||||
.height(1)
|
||||
.top_margin(1);
|
||||
let rows = app.items.iter().map(|item| {
|
||||
let height = item
|
||||
.iter()
|
||||
@@ -137,15 +144,18 @@ fn ui(f: &mut Frame, app: &mut App) {
|
||||
let cells = item.iter().map(|c| Cell::from(*c));
|
||||
Row::new(cells).height(height as u16).bottom_margin(1)
|
||||
});
|
||||
let t = Table::new(rows)
|
||||
.header(header)
|
||||
.block(Block::default().borders(Borders::ALL).title("Table"))
|
||||
.highlight_style(selected_style)
|
||||
.highlight_symbol(">> ")
|
||||
.widths(&[
|
||||
let t = Table::new(
|
||||
rows,
|
||||
[
|
||||
Constraint::Percentage(50),
|
||||
Constraint::Max(30),
|
||||
Constraint::Min(10),
|
||||
]);
|
||||
],
|
||||
)
|
||||
.header(header)
|
||||
.footer(footer)
|
||||
.block(Block::default().borders(Borders::ALL).title("Table"))
|
||||
.highlight_style(selected_style)
|
||||
.highlight_symbol(">> ");
|
||||
f.render_stateful_widget(t, rects[0], &mut app.state);
|
||||
}
|
||||
|
||||
@@ -69,8 +69,8 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
|
||||
if key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Right => app.next(),
|
||||
KeyCode::Left => app.previous(),
|
||||
KeyCode::Right | KeyCode::Char('l') => app.next(),
|
||||
KeyCode::Left | KeyCode::Char('h') => app.previous(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,3 +3,4 @@ group_imports = "StdExternalCrate"
|
||||
imports_granularity = "Crate"
|
||||
wrap_comments = true
|
||||
comment_width = 100
|
||||
format_code_in_doc_comments = true
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
//!
|
||||
//! Additionally, a [`TestBackend`] is provided for testing purposes.
|
||||
//!
|
||||
//! See the [Backend Comparison] section of the [Ratatui Book] for more details on the different
|
||||
//! See the [Backend Comparison] section of the [Ratatui Website] for more details on the different
|
||||
//! backends.
|
||||
//!
|
||||
//! Each backend supports a number of features, such as [raw mode](#raw-mode), [alternate
|
||||
@@ -26,6 +26,7 @@
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use std::io::stdout;
|
||||
//!
|
||||
//! use ratatui::prelude::*;
|
||||
//!
|
||||
//! let backend = CrosstermBackend::new(stdout());
|
||||
@@ -97,8 +98,8 @@
|
||||
//! [Termwiz]: https://crates.io/crates/termwiz
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/tree/main/examples#readme
|
||||
//! [Backend Comparison]:
|
||||
//! https://ratatui-org.github.io/ratatui-book/concepts/backends/comparison.html
|
||||
//! [Ratatui Book]: https://ratatui-org.github.io/ratatui-book
|
||||
//! https://ratatui.rs/concepts/backends/comparison/
|
||||
//! [Ratatui Website]: https://ratatui-org.github.io/ratatui-book
|
||||
use std::io;
|
||||
|
||||
use strum::{Display, EnumString};
|
||||
@@ -228,7 +229,7 @@ pub trait Backend {
|
||||
/// [`get_cursor`]: Backend::get_cursor
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()>;
|
||||
|
||||
/// Clears the whole terminal scree
|
||||
/// Clears the whole terminal screen
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
|
||||
@@ -10,8 +10,8 @@ use crossterm::{
|
||||
cursor::{Hide, MoveTo, Show},
|
||||
execute, queue,
|
||||
style::{
|
||||
Attribute as CAttribute, Color as CColor, Print, SetAttribute, SetBackgroundColor,
|
||||
SetForegroundColor,
|
||||
Attribute as CAttribute, Attributes as CAttributes, Color as CColor, ContentStyle, Print,
|
||||
SetAttribute, SetBackgroundColor, SetForegroundColor,
|
||||
},
|
||||
terminal::{self, Clear},
|
||||
};
|
||||
@@ -21,7 +21,7 @@ use crate::{
|
||||
buffer::Cell,
|
||||
layout::Size,
|
||||
prelude::Rect,
|
||||
style::{Color, Modifier},
|
||||
style::{Color, Modifier, Style},
|
||||
};
|
||||
|
||||
/// A [`Backend`] implementation that uses [Crossterm] to render to the terminal.
|
||||
@@ -43,7 +43,8 @@ use crate::{
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use std::io::{stdout, stderr};
|
||||
/// use std::io::{stderr, stdout};
|
||||
///
|
||||
/// use crossterm::{
|
||||
/// terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
/// ExecutableCommand,
|
||||
@@ -161,7 +162,7 @@ where
|
||||
underline_color = cell.underline_color;
|
||||
}
|
||||
|
||||
queue!(self.writer, Print(&cell.symbol))?;
|
||||
queue!(self.writer, Print(cell.symbol()))?;
|
||||
}
|
||||
|
||||
#[cfg(feature = "underline-color")]
|
||||
@@ -274,6 +275,32 @@ impl From<Color> for CColor {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CColor> for Color {
|
||||
fn from(value: CColor) -> Self {
|
||||
match value {
|
||||
CColor::Reset => Self::Reset,
|
||||
CColor::Black => Self::Black,
|
||||
CColor::DarkRed => Self::Red,
|
||||
CColor::DarkGreen => Self::Green,
|
||||
CColor::DarkYellow => Self::Yellow,
|
||||
CColor::DarkBlue => Self::Blue,
|
||||
CColor::DarkMagenta => Self::Magenta,
|
||||
CColor::DarkCyan => Self::Cyan,
|
||||
CColor::Grey => Self::Gray,
|
||||
CColor::DarkGrey => Self::DarkGray,
|
||||
CColor::Red => Self::LightRed,
|
||||
CColor::Green => Self::LightGreen,
|
||||
CColor::Blue => Self::LightBlue,
|
||||
CColor::Yellow => Self::LightYellow,
|
||||
CColor::Magenta => Self::LightMagenta,
|
||||
CColor::Cyan => Self::LightCyan,
|
||||
CColor::White => Self::White,
|
||||
CColor::Rgb { r, g, b } => Self::Rgb(r, g, b),
|
||||
CColor::AnsiValue(v) => Self::Indexed(v),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The `ModifierDiff` struct is used to calculate the difference between two `Modifier`
|
||||
/// values. This is useful when updating the terminal display, as it allows for more
|
||||
/// efficient updates by only sending the necessary changes.
|
||||
@@ -344,3 +371,303 @@ impl ModifierDiff {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CAttribute> for Modifier {
|
||||
fn from(value: CAttribute) -> Self {
|
||||
// `Attribute*s*` (note the *s*) contains multiple `Attribute`
|
||||
// We convert `Attribute` to `Attribute*s*` (containing only 1 value) to avoid implementing
|
||||
// the conversion again
|
||||
Modifier::from(CAttributes::from(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CAttributes> for Modifier {
|
||||
fn from(value: CAttributes) -> Self {
|
||||
let mut res = Modifier::empty();
|
||||
|
||||
if value.has(CAttribute::Bold) {
|
||||
res |= Modifier::BOLD;
|
||||
}
|
||||
if value.has(CAttribute::Dim) {
|
||||
res |= Modifier::DIM;
|
||||
}
|
||||
if value.has(CAttribute::Italic) {
|
||||
res |= Modifier::ITALIC;
|
||||
}
|
||||
if value.has(CAttribute::Underlined)
|
||||
|| value.has(CAttribute::DoubleUnderlined)
|
||||
|| value.has(CAttribute::Undercurled)
|
||||
|| value.has(CAttribute::Underdotted)
|
||||
|| value.has(CAttribute::Underdashed)
|
||||
{
|
||||
res |= Modifier::UNDERLINED;
|
||||
}
|
||||
if value.has(CAttribute::SlowBlink) {
|
||||
res |= Modifier::SLOW_BLINK;
|
||||
}
|
||||
if value.has(CAttribute::RapidBlink) {
|
||||
res |= Modifier::RAPID_BLINK;
|
||||
}
|
||||
if value.has(CAttribute::Reverse) {
|
||||
res |= Modifier::REVERSED;
|
||||
}
|
||||
if value.has(CAttribute::Hidden) {
|
||||
res |= Modifier::HIDDEN;
|
||||
}
|
||||
if value.has(CAttribute::CrossedOut) {
|
||||
res |= Modifier::CROSSED_OUT;
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ContentStyle> for Style {
|
||||
fn from(value: ContentStyle) -> Self {
|
||||
let mut sub_modifier = Modifier::empty();
|
||||
|
||||
if value.attributes.has(CAttribute::NoBold) {
|
||||
sub_modifier |= Modifier::BOLD;
|
||||
}
|
||||
if value.attributes.has(CAttribute::NoItalic) {
|
||||
sub_modifier |= Modifier::ITALIC;
|
||||
}
|
||||
if value.attributes.has(CAttribute::NotCrossedOut) {
|
||||
sub_modifier |= Modifier::CROSSED_OUT;
|
||||
}
|
||||
if value.attributes.has(CAttribute::NoUnderline) {
|
||||
sub_modifier |= Modifier::UNDERLINED;
|
||||
}
|
||||
if value.attributes.has(CAttribute::NoHidden) {
|
||||
sub_modifier |= Modifier::HIDDEN;
|
||||
}
|
||||
if value.attributes.has(CAttribute::NoBlink) {
|
||||
sub_modifier |= Modifier::RAPID_BLINK | Modifier::SLOW_BLINK;
|
||||
}
|
||||
if value.attributes.has(CAttribute::NoReverse) {
|
||||
sub_modifier |= Modifier::REVERSED;
|
||||
}
|
||||
|
||||
Self {
|
||||
fg: value.foreground_color.map(|c| c.into()),
|
||||
bg: value.background_color.map(|c| c.into()),
|
||||
#[cfg(feature = "underline-color")]
|
||||
underline_color: value.underline_color.map(|c| c.into()),
|
||||
add_modifier: value.attributes.into(),
|
||||
sub_modifier,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn from_crossterm_color() {
|
||||
assert_eq!(Color::from(CColor::Reset), Color::Reset);
|
||||
assert_eq!(Color::from(CColor::Black), Color::Black);
|
||||
assert_eq!(Color::from(CColor::DarkGrey), Color::DarkGray);
|
||||
assert_eq!(Color::from(CColor::Red), Color::LightRed);
|
||||
assert_eq!(Color::from(CColor::DarkRed), Color::Red);
|
||||
assert_eq!(Color::from(CColor::Green), Color::LightGreen);
|
||||
assert_eq!(Color::from(CColor::DarkGreen), Color::Green);
|
||||
assert_eq!(Color::from(CColor::Yellow), Color::LightYellow);
|
||||
assert_eq!(Color::from(CColor::DarkYellow), Color::Yellow);
|
||||
assert_eq!(Color::from(CColor::Blue), Color::LightBlue);
|
||||
assert_eq!(Color::from(CColor::DarkBlue), Color::Blue);
|
||||
assert_eq!(Color::from(CColor::Magenta), Color::LightMagenta);
|
||||
assert_eq!(Color::from(CColor::DarkMagenta), Color::Magenta);
|
||||
assert_eq!(Color::from(CColor::Cyan), Color::LightCyan);
|
||||
assert_eq!(Color::from(CColor::DarkCyan), Color::Cyan);
|
||||
assert_eq!(Color::from(CColor::White), Color::White);
|
||||
assert_eq!(Color::from(CColor::Grey), Color::Gray);
|
||||
assert_eq!(
|
||||
Color::from(CColor::Rgb { r: 0, g: 0, b: 0 }),
|
||||
Color::Rgb(0, 0, 0)
|
||||
);
|
||||
assert_eq!(
|
||||
Color::from(CColor::Rgb {
|
||||
r: 10,
|
||||
g: 20,
|
||||
b: 30
|
||||
}),
|
||||
Color::Rgb(10, 20, 30)
|
||||
);
|
||||
assert_eq!(Color::from(CColor::AnsiValue(32)), Color::Indexed(32));
|
||||
assert_eq!(Color::from(CColor::AnsiValue(37)), Color::Indexed(37));
|
||||
}
|
||||
|
||||
mod modifier {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn from_crossterm_attribute() {
|
||||
assert_eq!(Modifier::from(CAttribute::Reset), Modifier::empty());
|
||||
assert_eq!(Modifier::from(CAttribute::Bold), Modifier::BOLD);
|
||||
assert_eq!(Modifier::from(CAttribute::Italic), Modifier::ITALIC);
|
||||
assert_eq!(Modifier::from(CAttribute::Underlined), Modifier::UNDERLINED);
|
||||
assert_eq!(
|
||||
Modifier::from(CAttribute::DoubleUnderlined),
|
||||
Modifier::UNDERLINED
|
||||
);
|
||||
assert_eq!(
|
||||
Modifier::from(CAttribute::Underdotted),
|
||||
Modifier::UNDERLINED
|
||||
);
|
||||
assert_eq!(Modifier::from(CAttribute::Dim), Modifier::DIM);
|
||||
assert_eq!(
|
||||
Modifier::from(CAttribute::NormalIntensity),
|
||||
Modifier::empty()
|
||||
);
|
||||
assert_eq!(
|
||||
Modifier::from(CAttribute::CrossedOut),
|
||||
Modifier::CROSSED_OUT
|
||||
);
|
||||
assert_eq!(Modifier::from(CAttribute::NoUnderline), Modifier::empty());
|
||||
assert_eq!(Modifier::from(CAttribute::OverLined), Modifier::empty());
|
||||
assert_eq!(Modifier::from(CAttribute::SlowBlink), Modifier::SLOW_BLINK);
|
||||
assert_eq!(
|
||||
Modifier::from(CAttribute::RapidBlink),
|
||||
Modifier::RAPID_BLINK
|
||||
);
|
||||
assert_eq!(Modifier::from(CAttribute::Hidden), Modifier::HIDDEN);
|
||||
assert_eq!(Modifier::from(CAttribute::NoHidden), Modifier::empty());
|
||||
assert_eq!(Modifier::from(CAttribute::Reverse), Modifier::REVERSED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_crossterm_attributes() {
|
||||
assert_eq!(
|
||||
Modifier::from(CAttributes::from(CAttribute::Bold)),
|
||||
Modifier::BOLD
|
||||
);
|
||||
assert_eq!(
|
||||
Modifier::from(CAttributes::from(
|
||||
[CAttribute::Bold, CAttribute::Italic].as_ref()
|
||||
)),
|
||||
Modifier::BOLD | Modifier::ITALIC
|
||||
);
|
||||
assert_eq!(
|
||||
Modifier::from(CAttributes::from(
|
||||
[CAttribute::Bold, CAttribute::NotCrossedOut].as_ref()
|
||||
)),
|
||||
Modifier::BOLD
|
||||
);
|
||||
assert_eq!(
|
||||
Modifier::from(CAttributes::from(
|
||||
[CAttribute::Dim, CAttribute::Underdotted].as_ref()
|
||||
)),
|
||||
Modifier::DIM | Modifier::UNDERLINED
|
||||
);
|
||||
assert_eq!(
|
||||
Modifier::from(CAttributes::from(
|
||||
[CAttribute::Dim, CAttribute::SlowBlink, CAttribute::Italic].as_ref()
|
||||
)),
|
||||
Modifier::DIM | Modifier::SLOW_BLINK | Modifier::ITALIC
|
||||
);
|
||||
assert_eq!(
|
||||
Modifier::from(CAttributes::from(
|
||||
[
|
||||
CAttribute::Hidden,
|
||||
CAttribute::NoUnderline,
|
||||
CAttribute::NotCrossedOut
|
||||
]
|
||||
.as_ref()
|
||||
)),
|
||||
Modifier::HIDDEN
|
||||
);
|
||||
assert_eq!(
|
||||
Modifier::from(CAttributes::from(CAttribute::Reverse)),
|
||||
Modifier::REVERSED
|
||||
);
|
||||
assert_eq!(
|
||||
Modifier::from(CAttributes::from(CAttribute::Reset)),
|
||||
Modifier::empty()
|
||||
);
|
||||
assert_eq!(
|
||||
Modifier::from(CAttributes::from(
|
||||
[CAttribute::RapidBlink, CAttribute::CrossedOut].as_ref()
|
||||
)),
|
||||
Modifier::RAPID_BLINK | Modifier::CROSSED_OUT
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_crossterm_content_style() {
|
||||
assert_eq!(Style::from(ContentStyle::default()), Style::default());
|
||||
assert_eq!(
|
||||
Style::from(ContentStyle {
|
||||
foreground_color: Some(CColor::DarkYellow),
|
||||
..Default::default()
|
||||
}),
|
||||
Style::default().fg(Color::Yellow)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from(ContentStyle {
|
||||
background_color: Some(CColor::DarkYellow),
|
||||
..Default::default()
|
||||
}),
|
||||
Style::default().bg(Color::Yellow)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from(ContentStyle {
|
||||
attributes: CAttributes::from(CAttribute::Bold),
|
||||
..Default::default()
|
||||
}),
|
||||
Style::default().add_modifier(Modifier::BOLD)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from(ContentStyle {
|
||||
attributes: CAttributes::from(CAttribute::NoBold),
|
||||
..Default::default()
|
||||
}),
|
||||
Style::default().remove_modifier(Modifier::BOLD)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from(ContentStyle {
|
||||
attributes: CAttributes::from(CAttribute::Italic),
|
||||
..Default::default()
|
||||
}),
|
||||
Style::default().add_modifier(Modifier::ITALIC)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from(ContentStyle {
|
||||
attributes: CAttributes::from(CAttribute::NoItalic),
|
||||
..Default::default()
|
||||
}),
|
||||
Style::default().remove_modifier(Modifier::ITALIC)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from(ContentStyle {
|
||||
attributes: CAttributes::from([CAttribute::Bold, CAttribute::Italic].as_ref()),
|
||||
..Default::default()
|
||||
}),
|
||||
Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.add_modifier(Modifier::ITALIC)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from(ContentStyle {
|
||||
attributes: CAttributes::from([CAttribute::NoBold, CAttribute::NoItalic].as_ref()),
|
||||
..Default::default()
|
||||
}),
|
||||
Style::default()
|
||||
.remove_modifier(Modifier::BOLD)
|
||||
.remove_modifier(Modifier::ITALIC)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "underline-color")]
|
||||
fn from_crossterm_content_style_underline() {
|
||||
assert_eq!(
|
||||
Style::from(ContentStyle {
|
||||
underline_color: Some(CColor::DarkRed),
|
||||
..Default::default()
|
||||
}),
|
||||
Style::default().underline_color(Color::Red)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,11 +9,13 @@ use std::{
|
||||
io::{self, Write},
|
||||
};
|
||||
|
||||
use termion::{color as tcolor, style as tstyle};
|
||||
|
||||
use crate::{
|
||||
backend::{Backend, ClearType, WindowSize},
|
||||
buffer::Cell,
|
||||
prelude::Rect,
|
||||
style::{Color, Modifier},
|
||||
style::{Color, Modifier, Style},
|
||||
};
|
||||
|
||||
/// A [`Backend`] implementation that uses [Termion] to render to the terminal.
|
||||
@@ -36,7 +38,8 @@ use crate::{
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use std::io::{stdout, stderr};
|
||||
/// use std::io::{stderr, stdout};
|
||||
///
|
||||
/// use ratatui::prelude::*;
|
||||
/// use termion::{raw::IntoRawMode, screen::IntoAlternateScreen};
|
||||
///
|
||||
@@ -179,7 +182,7 @@ where
|
||||
write!(string, "{}", Bg(cell.bg)).unwrap();
|
||||
bg = cell.bg;
|
||||
}
|
||||
string.push_str(&cell.symbol);
|
||||
string.push_str(cell.symbol());
|
||||
}
|
||||
write!(
|
||||
self.writer,
|
||||
@@ -274,6 +277,82 @@ impl fmt::Display for Bg {
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! from_termion_for_color {
|
||||
($termion_color:ident, $color: ident) => {
|
||||
impl From<tcolor::$termion_color> for Color {
|
||||
fn from(_: tcolor::$termion_color) -> Self {
|
||||
Color::$color
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tcolor::Bg<tcolor::$termion_color>> for Style {
|
||||
fn from(_: tcolor::Bg<tcolor::$termion_color>) -> Self {
|
||||
Style::default().bg(Color::$color)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tcolor::Fg<tcolor::$termion_color>> for Style {
|
||||
fn from(_: tcolor::Fg<tcolor::$termion_color>) -> Self {
|
||||
Style::default().fg(Color::$color)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
from_termion_for_color!(Reset, Reset);
|
||||
from_termion_for_color!(Black, Black);
|
||||
from_termion_for_color!(Red, Red);
|
||||
from_termion_for_color!(Green, Green);
|
||||
from_termion_for_color!(Yellow, Yellow);
|
||||
from_termion_for_color!(Blue, Blue);
|
||||
from_termion_for_color!(Magenta, Magenta);
|
||||
from_termion_for_color!(Cyan, Cyan);
|
||||
from_termion_for_color!(White, Gray);
|
||||
from_termion_for_color!(LightBlack, DarkGray);
|
||||
from_termion_for_color!(LightRed, LightRed);
|
||||
from_termion_for_color!(LightGreen, LightGreen);
|
||||
from_termion_for_color!(LightBlue, LightBlue);
|
||||
from_termion_for_color!(LightYellow, LightYellow);
|
||||
from_termion_for_color!(LightMagenta, LightMagenta);
|
||||
from_termion_for_color!(LightCyan, LightCyan);
|
||||
from_termion_for_color!(LightWhite, White);
|
||||
|
||||
impl From<tcolor::AnsiValue> for Color {
|
||||
fn from(value: tcolor::AnsiValue) -> Self {
|
||||
Color::Indexed(value.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tcolor::Bg<tcolor::AnsiValue>> for Style {
|
||||
fn from(value: tcolor::Bg<tcolor::AnsiValue>) -> Self {
|
||||
Style::default().bg(Color::Indexed(value.0 .0))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tcolor::Fg<tcolor::AnsiValue>> for Style {
|
||||
fn from(value: tcolor::Fg<tcolor::AnsiValue>) -> Self {
|
||||
Style::default().fg(Color::Indexed(value.0 .0))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tcolor::Rgb> for Color {
|
||||
fn from(value: tcolor::Rgb) -> Self {
|
||||
Color::Rgb(value.0, value.1, value.2)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tcolor::Bg<tcolor::Rgb>> for Style {
|
||||
fn from(value: tcolor::Bg<tcolor::Rgb>) -> Self {
|
||||
Style::default().bg(Color::Rgb(value.0 .0, value.0 .1, value.0 .2))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tcolor::Fg<tcolor::Rgb>> for Style {
|
||||
fn from(value: tcolor::Fg<tcolor::Rgb>) -> Self {
|
||||
Style::default().fg(Color::Rgb(value.0 .0, value.0 .1, value.0 .2))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ModifierDiff {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let remove = self.from - self.to;
|
||||
@@ -338,3 +417,147 @@ impl fmt::Display for ModifierDiff {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! from_termion_for_modifier {
|
||||
($termion_modifier:ident, $modifier: ident) => {
|
||||
impl From<tstyle::$termion_modifier> for Modifier {
|
||||
fn from(_: tstyle::$termion_modifier) -> Self {
|
||||
Modifier::$modifier
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
from_termion_for_modifier!(Invert, REVERSED);
|
||||
from_termion_for_modifier!(Bold, BOLD);
|
||||
from_termion_for_modifier!(Italic, ITALIC);
|
||||
from_termion_for_modifier!(Underline, UNDERLINED);
|
||||
from_termion_for_modifier!(Faint, DIM);
|
||||
from_termion_for_modifier!(CrossedOut, CROSSED_OUT);
|
||||
from_termion_for_modifier!(Blink, SLOW_BLINK);
|
||||
|
||||
impl From<termion::style::Reset> for Modifier {
|
||||
fn from(_: termion::style::Reset) -> Self {
|
||||
Modifier::empty()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::style::Stylize;
|
||||
|
||||
#[test]
|
||||
fn from_termion_color() {
|
||||
assert_eq!(Color::from(tcolor::Reset), Color::Reset);
|
||||
assert_eq!(Color::from(tcolor::Black), Color::Black);
|
||||
assert_eq!(Color::from(tcolor::Red), Color::Red);
|
||||
assert_eq!(Color::from(tcolor::Green), Color::Green);
|
||||
assert_eq!(Color::from(tcolor::Yellow), Color::Yellow);
|
||||
assert_eq!(Color::from(tcolor::Blue), Color::Blue);
|
||||
assert_eq!(Color::from(tcolor::Magenta), Color::Magenta);
|
||||
assert_eq!(Color::from(tcolor::Cyan), Color::Cyan);
|
||||
assert_eq!(Color::from(tcolor::White), Color::Gray);
|
||||
assert_eq!(Color::from(tcolor::LightBlack), Color::DarkGray);
|
||||
assert_eq!(Color::from(tcolor::LightRed), Color::LightRed);
|
||||
assert_eq!(Color::from(tcolor::LightGreen), Color::LightGreen);
|
||||
assert_eq!(Color::from(tcolor::LightBlue), Color::LightBlue);
|
||||
assert_eq!(Color::from(tcolor::LightYellow), Color::LightYellow);
|
||||
assert_eq!(Color::from(tcolor::LightMagenta), Color::LightMagenta);
|
||||
assert_eq!(Color::from(tcolor::LightCyan), Color::LightCyan);
|
||||
assert_eq!(Color::from(tcolor::LightWhite), Color::White);
|
||||
assert_eq!(Color::from(tcolor::AnsiValue(31)), Color::Indexed(31));
|
||||
assert_eq!(Color::from(tcolor::Rgb(1, 2, 3)), Color::Rgb(1, 2, 3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_termion_bg() {
|
||||
use tc::Bg;
|
||||
use tcolor as tc;
|
||||
|
||||
assert_eq!(Style::from(Bg(tc::Reset)), Style::new().bg(Color::Reset));
|
||||
assert_eq!(Style::from(Bg(tc::Black)), Style::new().on_black());
|
||||
assert_eq!(Style::from(Bg(tc::Red)), Style::new().on_red());
|
||||
assert_eq!(Style::from(Bg(tc::Green)), Style::new().on_green());
|
||||
assert_eq!(Style::from(Bg(tc::Yellow)), Style::new().on_yellow());
|
||||
assert_eq!(Style::from(Bg(tc::Blue)), Style::new().on_blue());
|
||||
assert_eq!(Style::from(Bg(tc::Magenta)), Style::new().on_magenta());
|
||||
assert_eq!(Style::from(Bg(tc::Cyan)), Style::new().on_cyan());
|
||||
assert_eq!(Style::from(Bg(tc::White)), Style::new().on_gray());
|
||||
assert_eq!(Style::from(Bg(tc::LightBlack)), Style::new().on_dark_gray());
|
||||
assert_eq!(Style::from(Bg(tc::LightRed)), Style::new().on_light_red());
|
||||
assert_eq!(
|
||||
Style::from(Bg(tc::LightGreen)),
|
||||
Style::new().on_light_green()
|
||||
);
|
||||
assert_eq!(Style::from(Bg(tc::LightBlue)), Style::new().on_light_blue());
|
||||
assert_eq!(
|
||||
Style::from(Bg(tc::LightYellow)),
|
||||
Style::new().on_light_yellow()
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from(Bg(tc::LightMagenta)),
|
||||
Style::new().on_light_magenta()
|
||||
);
|
||||
assert_eq!(Style::from(Bg(tc::LightCyan)), Style::new().on_light_cyan());
|
||||
assert_eq!(Style::from(Bg(tc::LightWhite)), Style::new().on_white());
|
||||
assert_eq!(
|
||||
Style::from(Bg(tc::AnsiValue(31))),
|
||||
Style::new().bg(Color::Indexed(31))
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from(Bg(tc::Rgb(1, 2, 3))),
|
||||
Style::new().bg(Color::Rgb(1, 2, 3))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_termion_fg() {
|
||||
use tc::Fg;
|
||||
use tcolor as tc;
|
||||
|
||||
assert_eq!(Style::from(Fg(tc::Reset)), Style::new().fg(Color::Reset));
|
||||
assert_eq!(Style::from(Fg(tc::Black)), Style::new().black());
|
||||
assert_eq!(Style::from(Fg(tc::Red)), Style::new().red());
|
||||
assert_eq!(Style::from(Fg(tc::Green)), Style::new().green());
|
||||
assert_eq!(Style::from(Fg(tc::Yellow)), Style::new().yellow());
|
||||
assert_eq!(Style::from(Fg(tc::Blue)), Style::default().blue());
|
||||
assert_eq!(Style::from(Fg(tc::Magenta)), Style::default().magenta());
|
||||
assert_eq!(Style::from(Fg(tc::Cyan)), Style::default().cyan());
|
||||
assert_eq!(Style::from(Fg(tc::White)), Style::default().gray());
|
||||
assert_eq!(Style::from(Fg(tc::LightBlack)), Style::new().dark_gray());
|
||||
assert_eq!(Style::from(Fg(tc::LightRed)), Style::new().light_red());
|
||||
assert_eq!(Style::from(Fg(tc::LightGreen)), Style::new().light_green());
|
||||
assert_eq!(Style::from(Fg(tc::LightBlue)), Style::new().light_blue());
|
||||
assert_eq!(
|
||||
Style::from(Fg(tc::LightYellow)),
|
||||
Style::new().light_yellow()
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from(Fg(tc::LightMagenta)),
|
||||
Style::new().light_magenta()
|
||||
);
|
||||
assert_eq!(Style::from(Fg(tc::LightCyan)), Style::new().light_cyan());
|
||||
assert_eq!(Style::from(Fg(tc::LightWhite)), Style::new().white());
|
||||
assert_eq!(
|
||||
Style::from(Fg(tc::AnsiValue(31))),
|
||||
Style::default().fg(Color::Indexed(31))
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from(Fg(tc::Rgb(1, 2, 3))),
|
||||
Style::default().fg(Color::Rgb(1, 2, 3))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_termion_style() {
|
||||
assert_eq!(Modifier::from(tstyle::Invert), Modifier::REVERSED);
|
||||
assert_eq!(Modifier::from(tstyle::Bold), Modifier::BOLD);
|
||||
assert_eq!(Modifier::from(tstyle::Italic), Modifier::ITALIC);
|
||||
assert_eq!(Modifier::from(tstyle::Underline), Modifier::UNDERLINED);
|
||||
assert_eq!(Modifier::from(tstyle::Faint), Modifier::DIM);
|
||||
assert_eq!(Modifier::from(tstyle::CrossedOut), Modifier::CROSSED_OUT);
|
||||
assert_eq!(Modifier::from(tstyle::Blink), Modifier::SLOW_BLINK);
|
||||
assert_eq!(Modifier::from(tstyle::Reset), Modifier::empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ use std::{error::Error, io};
|
||||
|
||||
use termwiz::{
|
||||
caps::Capabilities,
|
||||
cell::{AttributeChange, Blink, Intensity, Underline},
|
||||
color::{AnsiColor, ColorAttribute, SrgbaTuple},
|
||||
cell::{AttributeChange, Blink, CellAttributes, Intensity, Underline},
|
||||
color::{AnsiColor, ColorAttribute, ColorSpec, LinearRgba, RgbColor, SrgbaTuple},
|
||||
surface::{Change, CursorVisibility, Position},
|
||||
terminal::{buffered::BufferedTerminal, ScreenSize, SystemTerminal, Terminal},
|
||||
};
|
||||
@@ -20,7 +20,7 @@ use crate::{
|
||||
buffer::Cell,
|
||||
layout::Size,
|
||||
prelude::Rect,
|
||||
style::{Color, Modifier},
|
||||
style::{Color, Modifier, Style},
|
||||
};
|
||||
|
||||
/// A [`Backend`] implementation that uses [Termwiz] to render to the terminal.
|
||||
@@ -176,7 +176,7 @@ impl Backend for TermwizBackend {
|
||||
},
|
||||
)));
|
||||
|
||||
self.buffered_terminal.add_change(&cell.symbol);
|
||||
self.buffered_terminal.add_change(cell.symbol());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -249,12 +249,73 @@ impl Backend for TermwizBackend {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CellAttributes> for Style {
|
||||
fn from(value: CellAttributes) -> Self {
|
||||
let mut style = Style::new()
|
||||
.add_modifier(value.intensity().into())
|
||||
.add_modifier(value.underline().into())
|
||||
.add_modifier(value.blink().into());
|
||||
|
||||
if value.italic() {
|
||||
style.add_modifier |= Modifier::ITALIC;
|
||||
}
|
||||
if value.reverse() {
|
||||
style.add_modifier |= Modifier::REVERSED;
|
||||
}
|
||||
if value.strikethrough() {
|
||||
style.add_modifier |= Modifier::CROSSED_OUT;
|
||||
}
|
||||
if value.invisible() {
|
||||
style.add_modifier |= Modifier::HIDDEN;
|
||||
}
|
||||
|
||||
style.fg = Some(value.foreground().into());
|
||||
style.bg = Some(value.background().into());
|
||||
#[cfg(feature = "underline_color")]
|
||||
{
|
||||
style.underline_color = Some(value.underline_color().into());
|
||||
}
|
||||
|
||||
style
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Intensity> for Modifier {
|
||||
fn from(value: Intensity) -> Self {
|
||||
match value {
|
||||
Intensity::Normal => Modifier::empty(),
|
||||
Intensity::Bold => Modifier::BOLD,
|
||||
Intensity::Half => Modifier::DIM,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Underline> for Modifier {
|
||||
fn from(value: Underline) -> Self {
|
||||
match value {
|
||||
Underline::None => Modifier::empty(),
|
||||
_ => Modifier::UNDERLINED,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Blink> for Modifier {
|
||||
fn from(value: Blink) -> Self {
|
||||
match value {
|
||||
Blink::None => Modifier::empty(),
|
||||
Blink::Slow => Modifier::SLOW_BLINK,
|
||||
Blink::Rapid => Modifier::RAPID_BLINK,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color> for ColorAttribute {
|
||||
fn from(color: Color) -> ColorAttribute {
|
||||
match color {
|
||||
Color::Reset => ColorAttribute::Default,
|
||||
Color::Black => AnsiColor::Black.into(),
|
||||
Color::Gray | Color::DarkGray => AnsiColor::Grey.into(),
|
||||
Color::DarkGray => AnsiColor::Grey.into(),
|
||||
Color::Gray => AnsiColor::Silver.into(),
|
||||
Color::Red => AnsiColor::Maroon.into(),
|
||||
Color::LightRed => AnsiColor::Red.into(),
|
||||
Color::Green => AnsiColor::Green.into(),
|
||||
@@ -276,7 +337,326 @@ impl From<Color> for ColorAttribute {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AnsiColor> for Color {
|
||||
fn from(value: AnsiColor) -> Self {
|
||||
match value {
|
||||
AnsiColor::Black => Color::Black,
|
||||
AnsiColor::Grey => Color::DarkGray,
|
||||
AnsiColor::Silver => Color::Gray,
|
||||
AnsiColor::Maroon => Color::Red,
|
||||
AnsiColor::Red => Color::LightRed,
|
||||
AnsiColor::Green => Color::Green,
|
||||
AnsiColor::Lime => Color::LightGreen,
|
||||
AnsiColor::Olive => Color::Yellow,
|
||||
AnsiColor::Yellow => Color::LightYellow,
|
||||
AnsiColor::Purple => Color::Magenta,
|
||||
AnsiColor::Fuchsia => Color::LightMagenta,
|
||||
AnsiColor::Teal => Color::Cyan,
|
||||
AnsiColor::Aqua => Color::LightCyan,
|
||||
AnsiColor::White => Color::White,
|
||||
AnsiColor::Navy => Color::Blue,
|
||||
AnsiColor::Blue => Color::LightBlue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ColorAttribute> for Color {
|
||||
fn from(value: ColorAttribute) -> Self {
|
||||
match value {
|
||||
ColorAttribute::TrueColorWithDefaultFallback(srgba)
|
||||
| ColorAttribute::TrueColorWithPaletteFallback(srgba, _) => srgba.into(),
|
||||
ColorAttribute::PaletteIndex(i) => Color::Indexed(i),
|
||||
ColorAttribute::Default => Color::Reset,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ColorSpec> for Color {
|
||||
fn from(value: ColorSpec) -> Self {
|
||||
match value {
|
||||
ColorSpec::Default => Color::Reset,
|
||||
ColorSpec::PaletteIndex(i) => Color::Indexed(i),
|
||||
ColorSpec::TrueColor(srgba) => srgba.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SrgbaTuple> for Color {
|
||||
fn from(value: SrgbaTuple) -> Self {
|
||||
let (r, g, b, _) = value.to_srgb_u8();
|
||||
Color::Rgb(r, g, b)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RgbColor> for Color {
|
||||
fn from(value: RgbColor) -> Self {
|
||||
let (r, g, b) = value.to_tuple_rgb8();
|
||||
Color::Rgb(r, g, b)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LinearRgba> for Color {
|
||||
fn from(value: LinearRgba) -> Self {
|
||||
value.to_srgb().into()
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn u16_max(i: usize) -> u16 {
|
||||
u16::try_from(i).unwrap_or(u16::MAX)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::style::Stylize;
|
||||
|
||||
mod into_color {
|
||||
use Color as C;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn from_linear_rgba() {
|
||||
// full black + opaque
|
||||
assert_eq!(C::from(LinearRgba(0., 0., 0., 1.)), Color::Rgb(0, 0, 0));
|
||||
// full black + transparent
|
||||
assert_eq!(C::from(LinearRgba(0., 0., 0., 0.)), Color::Rgb(0, 0, 0));
|
||||
|
||||
// full white + opaque
|
||||
assert_eq!(C::from(LinearRgba(1., 1., 1., 1.)), C::Rgb(254, 254, 254));
|
||||
// full white + transparent
|
||||
assert_eq!(C::from(LinearRgba(1., 1., 1., 0.)), C::Rgb(254, 254, 254));
|
||||
|
||||
// full red
|
||||
assert_eq!(C::from(LinearRgba(1., 0., 0., 1.)), C::Rgb(254, 0, 0));
|
||||
// full green
|
||||
assert_eq!(C::from(LinearRgba(0., 1., 0., 1.)), C::Rgb(0, 254, 0));
|
||||
// full blue
|
||||
assert_eq!(C::from(LinearRgba(0., 0., 1., 1.)), C::Rgb(0, 0, 254));
|
||||
|
||||
// See https://stackoverflow.com/questions/12524623/what-are-the-practical-differences-when-working-with-colors-in-a-linear-vs-a-no
|
||||
// for an explanation
|
||||
|
||||
// half red
|
||||
assert_eq!(C::from(LinearRgba(0.214, 0., 0., 1.)), C::Rgb(127, 0, 0));
|
||||
// half green
|
||||
assert_eq!(C::from(LinearRgba(0., 0.214, 0., 1.)), C::Rgb(0, 127, 0));
|
||||
// half blue
|
||||
assert_eq!(C::from(LinearRgba(0., 0., 0.214, 1.)), C::Rgb(0, 0, 127));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_srgba() {
|
||||
// full black + opaque
|
||||
assert_eq!(C::from(SrgbaTuple(0., 0., 0., 1.)), Color::Rgb(0, 0, 0));
|
||||
// full black + transparent
|
||||
assert_eq!(C::from(SrgbaTuple(0., 0., 0., 0.)), Color::Rgb(0, 0, 0));
|
||||
|
||||
// full white + opaque
|
||||
assert_eq!(C::from(SrgbaTuple(1., 1., 1., 1.)), C::Rgb(255, 255, 255));
|
||||
// full white + transparent
|
||||
assert_eq!(C::from(SrgbaTuple(1., 1., 1., 0.)), C::Rgb(255, 255, 255));
|
||||
|
||||
// full red
|
||||
assert_eq!(C::from(SrgbaTuple(1., 0., 0., 1.)), C::Rgb(255, 0, 0));
|
||||
// full green
|
||||
assert_eq!(C::from(SrgbaTuple(0., 1., 0., 1.)), C::Rgb(0, 255, 0));
|
||||
// full blue
|
||||
assert_eq!(C::from(SrgbaTuple(0., 0., 1., 1.)), C::Rgb(0, 0, 255));
|
||||
|
||||
// half red
|
||||
assert_eq!(C::from(SrgbaTuple(0.5, 0., 0., 1.)), C::Rgb(127, 0, 0));
|
||||
// half green
|
||||
assert_eq!(C::from(SrgbaTuple(0., 0.5, 0., 1.)), C::Rgb(0, 127, 0));
|
||||
// half blue
|
||||
assert_eq!(C::from(SrgbaTuple(0., 0., 0.5, 1.)), C::Rgb(0, 0, 127));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_rgbcolor() {
|
||||
// full black
|
||||
assert_eq!(C::from(RgbColor::new_8bpc(0, 0, 0)), Color::Rgb(0, 0, 0));
|
||||
// full white
|
||||
assert_eq!(
|
||||
C::from(RgbColor::new_8bpc(255, 255, 255)),
|
||||
C::Rgb(255, 255, 255)
|
||||
);
|
||||
|
||||
// full red
|
||||
assert_eq!(C::from(RgbColor::new_8bpc(255, 0, 0)), C::Rgb(255, 0, 0));
|
||||
// full green
|
||||
assert_eq!(C::from(RgbColor::new_8bpc(0, 255, 0)), C::Rgb(0, 255, 0));
|
||||
// full blue
|
||||
assert_eq!(C::from(RgbColor::new_8bpc(0, 0, 255)), C::Rgb(0, 0, 255));
|
||||
|
||||
// half red
|
||||
assert_eq!(C::from(RgbColor::new_8bpc(127, 0, 0)), C::Rgb(127, 0, 0));
|
||||
// half green
|
||||
assert_eq!(C::from(RgbColor::new_8bpc(0, 127, 0)), C::Rgb(0, 127, 0));
|
||||
// half blue
|
||||
assert_eq!(C::from(RgbColor::new_8bpc(0, 0, 127)), C::Rgb(0, 0, 127));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_colorspec() {
|
||||
assert_eq!(C::from(ColorSpec::Default), C::Reset);
|
||||
assert_eq!(C::from(ColorSpec::PaletteIndex(33)), C::Indexed(33));
|
||||
assert_eq!(
|
||||
C::from(ColorSpec::TrueColor(SrgbaTuple(0., 0., 0., 1.))),
|
||||
C::Rgb(0, 0, 0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_colorattribute() {
|
||||
assert_eq!(C::from(ColorAttribute::Default), C::Reset);
|
||||
assert_eq!(C::from(ColorAttribute::PaletteIndex(32)), C::Indexed(32));
|
||||
assert_eq!(
|
||||
C::from(ColorAttribute::TrueColorWithDefaultFallback(SrgbaTuple(
|
||||
0., 0., 0., 1.
|
||||
))),
|
||||
C::Rgb(0, 0, 0)
|
||||
);
|
||||
assert_eq!(
|
||||
C::from(ColorAttribute::TrueColorWithPaletteFallback(
|
||||
SrgbaTuple(0., 0., 0., 1.),
|
||||
31
|
||||
)),
|
||||
C::Rgb(0, 0, 0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_ansicolor() {
|
||||
assert_eq!(C::from(AnsiColor::Black), Color::Black);
|
||||
assert_eq!(C::from(AnsiColor::Grey), Color::DarkGray);
|
||||
assert_eq!(C::from(AnsiColor::Silver), Color::Gray);
|
||||
assert_eq!(C::from(AnsiColor::Maroon), Color::Red);
|
||||
assert_eq!(C::from(AnsiColor::Red), Color::LightRed);
|
||||
assert_eq!(C::from(AnsiColor::Green), Color::Green);
|
||||
assert_eq!(C::from(AnsiColor::Lime), Color::LightGreen);
|
||||
assert_eq!(C::from(AnsiColor::Olive), Color::Yellow);
|
||||
assert_eq!(C::from(AnsiColor::Yellow), Color::LightYellow);
|
||||
assert_eq!(C::from(AnsiColor::Purple), Color::Magenta);
|
||||
assert_eq!(C::from(AnsiColor::Fuchsia), Color::LightMagenta);
|
||||
assert_eq!(C::from(AnsiColor::Teal), Color::Cyan);
|
||||
assert_eq!(C::from(AnsiColor::Aqua), Color::LightCyan);
|
||||
assert_eq!(C::from(AnsiColor::White), Color::White);
|
||||
assert_eq!(C::from(AnsiColor::Navy), Color::Blue);
|
||||
assert_eq!(C::from(AnsiColor::Blue), Color::LightBlue);
|
||||
}
|
||||
}
|
||||
|
||||
mod into_modifier {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn from_intensity() {
|
||||
assert_eq!(Modifier::from(Intensity::Normal), Modifier::empty());
|
||||
assert_eq!(Modifier::from(Intensity::Bold), Modifier::BOLD);
|
||||
assert_eq!(Modifier::from(Intensity::Half), Modifier::DIM);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_underline() {
|
||||
assert_eq!(Modifier::from(Underline::None), Modifier::empty());
|
||||
assert_eq!(Modifier::from(Underline::Single), Modifier::UNDERLINED);
|
||||
assert_eq!(Modifier::from(Underline::Double), Modifier::UNDERLINED);
|
||||
assert_eq!(Modifier::from(Underline::Curly), Modifier::UNDERLINED);
|
||||
assert_eq!(Modifier::from(Underline::Dashed), Modifier::UNDERLINED);
|
||||
assert_eq!(Modifier::from(Underline::Dotted), Modifier::UNDERLINED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_blink() {
|
||||
assert_eq!(Modifier::from(Blink::None), Modifier::empty());
|
||||
assert_eq!(Modifier::from(Blink::Slow), Modifier::SLOW_BLINK);
|
||||
assert_eq!(Modifier::from(Blink::Rapid), Modifier::RAPID_BLINK);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_cell_attribute_for_style() {
|
||||
// default
|
||||
assert_eq!(
|
||||
Style::from(CellAttributes::default()),
|
||||
Style::new().fg(Color::Reset).bg(Color::Reset)
|
||||
);
|
||||
// foreground color
|
||||
assert_eq!(
|
||||
Style::from(
|
||||
CellAttributes::default()
|
||||
.set_foreground(ColorAttribute::PaletteIndex(31))
|
||||
.to_owned()
|
||||
),
|
||||
Style::new().fg(Color::Indexed(31)).bg(Color::Reset)
|
||||
);
|
||||
// background color
|
||||
assert_eq!(
|
||||
Style::from(
|
||||
CellAttributes::default()
|
||||
.set_background(ColorAttribute::PaletteIndex(31))
|
||||
.to_owned()
|
||||
),
|
||||
Style::new().fg(Color::Reset).bg(Color::Indexed(31))
|
||||
);
|
||||
// underline color
|
||||
#[cfg(feature = "underline_color")]
|
||||
assert_eq!(
|
||||
Style::from(
|
||||
CellAttributes::default()
|
||||
.set_underline_color(AnsiColor::Red)
|
||||
.set
|
||||
.to_owned()
|
||||
),
|
||||
Style::new()
|
||||
.fg(Color::Reset)
|
||||
.bg(Color::Reset)
|
||||
.underline_color(Color::Red)
|
||||
);
|
||||
// underlined
|
||||
assert_eq!(
|
||||
Style::from(
|
||||
CellAttributes::default()
|
||||
.set_underline(Underline::Single)
|
||||
.to_owned()
|
||||
),
|
||||
Style::new().fg(Color::Reset).bg(Color::Reset).underlined()
|
||||
);
|
||||
// blink
|
||||
assert_eq!(
|
||||
Style::from(CellAttributes::default().set_blink(Blink::Slow).to_owned()),
|
||||
Style::new().fg(Color::Reset).bg(Color::Reset).slow_blink()
|
||||
);
|
||||
// intensity
|
||||
assert_eq!(
|
||||
Style::from(
|
||||
CellAttributes::default()
|
||||
.set_intensity(Intensity::Bold)
|
||||
.to_owned()
|
||||
),
|
||||
Style::new().fg(Color::Reset).bg(Color::Reset).bold()
|
||||
);
|
||||
// italic
|
||||
assert_eq!(
|
||||
Style::from(CellAttributes::default().set_italic(true).to_owned()),
|
||||
Style::new().fg(Color::Reset).bg(Color::Reset).italic()
|
||||
);
|
||||
// reversed
|
||||
assert_eq!(
|
||||
Style::from(CellAttributes::default().set_reverse(true).to_owned()),
|
||||
Style::new().fg(Color::Reset).bg(Color::Reset).reversed()
|
||||
);
|
||||
// strikethrough
|
||||
assert_eq!(
|
||||
Style::from(CellAttributes::default().set_strikethrough(true).to_owned()),
|
||||
Style::new().fg(Color::Reset).bg(Color::Reset).crossed_out()
|
||||
);
|
||||
// hidden
|
||||
assert_eq!(
|
||||
Style::from(CellAttributes::default().set_invisible(true).to_owned()),
|
||||
Style::new().fg(Color::Reset).bg(Color::Reset).hidden()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use std::{
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{
|
||||
backend::{Backend, WindowSize},
|
||||
backend::{Backend, ClearType, WindowSize},
|
||||
buffer::{Buffer, Cell},
|
||||
layout::{Rect, Size},
|
||||
};
|
||||
@@ -56,11 +56,11 @@ fn buffer_view(buffer: &Buffer) -> String {
|
||||
view.push('"');
|
||||
for (x, c) in cells.iter().enumerate() {
|
||||
if skip == 0 {
|
||||
view.push_str(&c.symbol);
|
||||
view.push_str(c.symbol());
|
||||
} else {
|
||||
overwritten.push((x, &c.symbol));
|
||||
overwritten.push((x, c.symbol()));
|
||||
}
|
||||
skip = std::cmp::max(skip, c.symbol.width()).saturating_sub(1);
|
||||
skip = std::cmp::max(skip, c.symbol().width()).saturating_sub(1);
|
||||
}
|
||||
view.push('"');
|
||||
if !overwritten.is_empty() {
|
||||
@@ -179,6 +179,71 @@ impl Backend for TestBackend {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear_region(&mut self, clear_type: super::ClearType) -> io::Result<()> {
|
||||
match clear_type {
|
||||
ClearType::All => self.clear()?,
|
||||
ClearType::AfterCursor => {
|
||||
let index = self.buffer.index_of(self.pos.0, self.pos.1) + 1;
|
||||
self.buffer.content[index..].fill(Cell::default());
|
||||
}
|
||||
ClearType::BeforeCursor => {
|
||||
let index = self.buffer.index_of(self.pos.0, self.pos.1);
|
||||
self.buffer.content[..index].fill(Cell::default());
|
||||
}
|
||||
ClearType::CurrentLine => {
|
||||
let line_start_index = self.buffer.index_of(0, self.pos.1);
|
||||
let line_end_index = self.buffer.index_of(self.width - 1, self.pos.1);
|
||||
self.buffer.content[line_start_index..=line_end_index].fill(Cell::default());
|
||||
}
|
||||
ClearType::UntilNewLine => {
|
||||
let index = self.buffer.index_of(self.pos.0, self.pos.1);
|
||||
let line_end_index = self.buffer.index_of(self.width - 1, self.pos.1);
|
||||
self.buffer.content[index..=line_end_index].fill(Cell::default());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Inserts n line breaks at the current cursor position.
|
||||
///
|
||||
/// After the insertion, the cursor x position will be incremented by 1 (unless it's already
|
||||
/// at the end of line). This is a common behaviour of terminals in raw mode.
|
||||
///
|
||||
/// If the number of lines to append is fewer than the number of lines in the buffer after the
|
||||
/// cursor y position then the cursor is moved down by n rows.
|
||||
///
|
||||
/// If the number of lines to append is greater than the number of lines in the buffer after
|
||||
/// the cursor y position then that number of empty lines (at most the buffer's height in this
|
||||
/// case but this limit is instead replaced with scrolling in most backend implementations) will
|
||||
/// be added after the current position and the cursor will be moved to the last row.
|
||||
fn append_lines(&mut self, n: u16) -> io::Result<()> {
|
||||
let (cur_x, cur_y) = self.get_cursor()?;
|
||||
|
||||
// the next column ensuring that we don't go past the last column
|
||||
let new_cursor_x = cur_x.saturating_add(1).min(self.width.saturating_sub(1));
|
||||
|
||||
let max_y = self.height.saturating_sub(1);
|
||||
let lines_after_cursor = max_y.saturating_sub(cur_y);
|
||||
if n > lines_after_cursor {
|
||||
let rotate_by = n.saturating_sub(lines_after_cursor).min(max_y);
|
||||
|
||||
if rotate_by == self.height - 1 {
|
||||
self.clear()?;
|
||||
}
|
||||
|
||||
self.set_cursor(0, rotate_by)?;
|
||||
self.clear_region(ClearType::BeforeCursor)?;
|
||||
self.buffer
|
||||
.content
|
||||
.rotate_left((self.width * rotate_by).into());
|
||||
}
|
||||
|
||||
let new_cursor_y = cur_y.saturating_add(n).min(max_y);
|
||||
self.set_cursor(new_cursor_x, new_cursor_y)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn size(&self) -> Result<Rect, io::Error> {
|
||||
Ok(Rect::new(0, 0, self.width, self.height))
|
||||
}
|
||||
@@ -310,13 +375,299 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn clear() {
|
||||
let mut backend = TestBackend::new(10, 2);
|
||||
let mut backend = TestBackend::new(10, 4);
|
||||
let mut cell = Cell::default();
|
||||
cell.set_symbol("a");
|
||||
backend.draw([(0, 0, &cell)].into_iter()).unwrap();
|
||||
backend.draw([(0, 1, &cell)].into_iter()).unwrap();
|
||||
backend.clear().unwrap();
|
||||
backend.assert_buffer(&Buffer::with_lines(vec![" "; 2]));
|
||||
backend.assert_buffer(&Buffer::with_lines(vec![
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_region_all() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines(vec![
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
]);
|
||||
|
||||
backend.clear_region(ClearType::All).unwrap();
|
||||
backend.assert_buffer(&Buffer::with_lines(vec![
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_region_after_cursor() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines(vec![
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
]);
|
||||
|
||||
backend.set_cursor(3, 2).unwrap();
|
||||
backend.clear_region(ClearType::AfterCursor).unwrap();
|
||||
backend.assert_buffer(&Buffer::with_lines(vec![
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaa ",
|
||||
" ",
|
||||
" ",
|
||||
]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_region_before_cursor() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines(vec![
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
]);
|
||||
|
||||
backend.set_cursor(5, 3).unwrap();
|
||||
backend.clear_region(ClearType::BeforeCursor).unwrap();
|
||||
backend.assert_buffer(&Buffer::with_lines(vec![
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" aaaaa",
|
||||
"aaaaaaaaaa",
|
||||
]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_region_current_line() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines(vec![
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
]);
|
||||
|
||||
backend.set_cursor(3, 1).unwrap();
|
||||
backend.clear_region(ClearType::CurrentLine).unwrap();
|
||||
backend.assert_buffer(&Buffer::with_lines(vec![
|
||||
"aaaaaaaaaa",
|
||||
" ",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_region_until_new_line() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines(vec![
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
]);
|
||||
|
||||
backend.set_cursor(3, 0).unwrap();
|
||||
backend.clear_region(ClearType::UntilNewLine).unwrap();
|
||||
backend.assert_buffer(&Buffer::with_lines(vec![
|
||||
"aaa ",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_lines_not_at_last_line() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines(vec![
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
"dddddddddd",
|
||||
"eeeeeeeeee",
|
||||
]);
|
||||
|
||||
backend.set_cursor(0, 0).unwrap();
|
||||
|
||||
// If the cursor is not at the last line in the terminal the addition of a
|
||||
// newline simply moves the cursor down and to the right
|
||||
|
||||
backend.append_lines(1).unwrap();
|
||||
assert_eq!(backend.get_cursor().unwrap(), (1, 1));
|
||||
|
||||
backend.append_lines(1).unwrap();
|
||||
assert_eq!(backend.get_cursor().unwrap(), (2, 2));
|
||||
|
||||
backend.append_lines(1).unwrap();
|
||||
assert_eq!(backend.get_cursor().unwrap(), (3, 3));
|
||||
|
||||
backend.append_lines(1).unwrap();
|
||||
assert_eq!(backend.get_cursor().unwrap(), (4, 4));
|
||||
|
||||
// As such the buffer should remain unchanged
|
||||
backend.assert_buffer(&Buffer::with_lines(vec![
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
"dddddddddd",
|
||||
"eeeeeeeeee",
|
||||
]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_lines_at_last_line() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines(vec![
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
"dddddddddd",
|
||||
"eeeeeeeeee",
|
||||
]);
|
||||
|
||||
// If the cursor is at the last line in the terminal the addition of a
|
||||
// newline will scroll the contents of the buffer
|
||||
backend.set_cursor(0, 4).unwrap();
|
||||
|
||||
backend.append_lines(1).unwrap();
|
||||
|
||||
backend.buffer = Buffer::with_lines(vec![
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
"dddddddddd",
|
||||
"eeeeeeeeee",
|
||||
" ",
|
||||
]);
|
||||
|
||||
// It also moves the cursor to the right, as is common of the behaviour of
|
||||
// terminals in raw-mode
|
||||
assert_eq!(backend.get_cursor().unwrap(), (1, 4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_multiple_lines_not_at_last_line() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines(vec![
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
"dddddddddd",
|
||||
"eeeeeeeeee",
|
||||
]);
|
||||
|
||||
backend.set_cursor(0, 0).unwrap();
|
||||
|
||||
// If the cursor is not at the last line in the terminal the addition of multiple
|
||||
// newlines simply moves the cursor n lines down and to the right by 1
|
||||
|
||||
backend.append_lines(4).unwrap();
|
||||
assert_eq!(backend.get_cursor().unwrap(), (1, 4));
|
||||
|
||||
// As such the buffer should remain unchanged
|
||||
backend.assert_buffer(&Buffer::with_lines(vec![
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
"dddddddddd",
|
||||
"eeeeeeeeee",
|
||||
]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_multiple_lines_past_last_line() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines(vec![
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
"dddddddddd",
|
||||
"eeeeeeeeee",
|
||||
]);
|
||||
|
||||
backend.set_cursor(0, 3).unwrap();
|
||||
|
||||
backend.append_lines(3).unwrap();
|
||||
assert_eq!(backend.get_cursor().unwrap(), (1, 4));
|
||||
|
||||
backend.assert_buffer(&Buffer::with_lines(vec![
|
||||
"cccccccccc",
|
||||
"dddddddddd",
|
||||
"eeeeeeeeee",
|
||||
" ",
|
||||
" ",
|
||||
]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_multiple_lines_where_cursor_at_end_appends_height_lines() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines(vec![
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
"dddddddddd",
|
||||
"eeeeeeeeee",
|
||||
]);
|
||||
|
||||
backend.set_cursor(0, 4).unwrap();
|
||||
|
||||
backend.append_lines(5).unwrap();
|
||||
assert_eq!(backend.get_cursor().unwrap(), (1, 4));
|
||||
|
||||
backend.assert_buffer(&Buffer::with_lines(vec![
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_multiple_lines_where_cursor_appends_height_lines() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines(vec![
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
"dddddddddd",
|
||||
"eeeeeeeeee",
|
||||
]);
|
||||
|
||||
backend.set_cursor(0, 0).unwrap();
|
||||
|
||||
backend.append_lines(5).unwrap();
|
||||
assert_eq!(backend.get_cursor().unwrap(), (1, 4));
|
||||
|
||||
backend.assert_buffer(&Buffer::with_lines(vec![
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
"dddddddddd",
|
||||
"eeeeeeeeee",
|
||||
" ",
|
||||
]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
1052
src/buffer.rs
1052
src/buffer.rs
File diff suppressed because it is too large
Load Diff
82
src/buffer/assert.rs
Normal file
82
src/buffer/assert.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
/// Assert that two buffers are equal by comparing their areas and content.
|
||||
///
|
||||
/// On panic, displays the areas or the content and a diff of the contents.
|
||||
#[macro_export]
|
||||
macro_rules! assert_buffer_eq {
|
||||
($actual_expr:expr, $expected_expr:expr) => {
|
||||
match (&$actual_expr, &$expected_expr) {
|
||||
(actual, expected) => {
|
||||
if actual.area != expected.area {
|
||||
panic!(
|
||||
indoc::indoc!(
|
||||
"
|
||||
buffer areas not equal
|
||||
expected: {:?}
|
||||
actual: {:?}"
|
||||
),
|
||||
expected, actual
|
||||
);
|
||||
}
|
||||
let diff = expected.diff(&actual);
|
||||
if !diff.is_empty() {
|
||||
let nice_diff = diff
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, (x, y, cell))| {
|
||||
let expected_cell = expected.get(*x, *y);
|
||||
indoc::formatdoc! {"
|
||||
{i}: at ({x}, {y})
|
||||
expected: {expected_cell:?}
|
||||
actual: {cell:?}
|
||||
"}
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n");
|
||||
panic!(
|
||||
indoc::indoc!(
|
||||
"
|
||||
buffer contents not equal
|
||||
expected: {:?}
|
||||
actual: {:?}
|
||||
diff:
|
||||
{}"
|
||||
),
|
||||
expected, actual, nice_diff
|
||||
);
|
||||
}
|
||||
// shouldn't get here, but this guards against future behavior
|
||||
// that changes equality but not area or content
|
||||
assert_eq!(actual, expected, "buffers not equal");
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::prelude::*;
|
||||
|
||||
#[test]
|
||||
fn assert_buffer_eq_does_not_panic_on_equal_buffers() {
|
||||
let buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
|
||||
let other_buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
|
||||
assert_buffer_eq!(buffer, other_buffer);
|
||||
}
|
||||
|
||||
#[should_panic]
|
||||
#[test]
|
||||
fn assert_buffer_eq_panics_on_unequal_area() {
|
||||
let buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
|
||||
let other_buffer = Buffer::empty(Rect::new(0, 0, 6, 1));
|
||||
assert_buffer_eq!(buffer, other_buffer);
|
||||
}
|
||||
|
||||
#[should_panic]
|
||||
#[test]
|
||||
fn assert_buffer_eq_panics_on_unequal_style() {
|
||||
let buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
|
||||
let mut other_buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
|
||||
other_buffer.set_string(0, 0, " ", Style::default().fg(Color::Red));
|
||||
assert_buffer_eq!(buffer, other_buffer);
|
||||
}
|
||||
}
|
||||
891
src/buffer/buffer.rs
Normal file
891
src/buffer/buffer.rs
Normal file
@@ -0,0 +1,891 @@
|
||||
use std::{
|
||||
cmp::min,
|
||||
fmt::{Debug, Formatter, Result},
|
||||
};
|
||||
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{buffer::Cell, prelude::*};
|
||||
|
||||
/// A buffer that maps to the desired content of the terminal after the draw call
|
||||
///
|
||||
/// No widget in the library interacts directly with the terminal. Instead each of them is required
|
||||
/// to draw their state to an intermediate buffer. It is basically a grid where each cell contains
|
||||
/// a grapheme, a foreground color and a background color. This grid will then be used to output
|
||||
/// the appropriate escape sequences and characters to draw the UI as the user has defined it.
|
||||
///
|
||||
/// # Examples:
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::{buffer::Cell, prelude::*};
|
||||
///
|
||||
/// let mut buf = Buffer::empty(Rect {
|
||||
/// x: 0,
|
||||
/// y: 0,
|
||||
/// width: 10,
|
||||
/// height: 5,
|
||||
/// });
|
||||
/// buf.get_mut(0, 2).set_symbol("x");
|
||||
/// assert_eq!(buf.get(0, 2).symbol(), "x");
|
||||
///
|
||||
/// buf.set_string(
|
||||
/// 3,
|
||||
/// 0,
|
||||
/// "string",
|
||||
/// Style::default().fg(Color::Red).bg(Color::White),
|
||||
/// );
|
||||
/// let cell = buf.get_mut(5, 0);
|
||||
/// assert_eq!(cell.symbol(), "r");
|
||||
/// assert_eq!(cell.fg, Color::Red);
|
||||
/// assert_eq!(cell.bg, Color::White);
|
||||
///
|
||||
/// buf.get_mut(5, 0).set_char('x');
|
||||
/// assert_eq!(buf.get(5, 0).symbol(), "x");
|
||||
/// ```
|
||||
#[derive(Default, Clone, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Buffer {
|
||||
/// The area represented by this buffer
|
||||
pub area: Rect,
|
||||
/// The content of the buffer. The length of this Vec should always be equal to area.width *
|
||||
/// area.height
|
||||
pub content: Vec<Cell>,
|
||||
}
|
||||
|
||||
impl Buffer {
|
||||
/// Returns a Buffer with all cells set to the default one
|
||||
pub fn empty(area: Rect) -> Buffer {
|
||||
let cell = Cell::default();
|
||||
Buffer::filled(area, &cell)
|
||||
}
|
||||
|
||||
/// Returns a Buffer with all cells initialized with the attributes of the given Cell
|
||||
pub fn filled(area: Rect, cell: &Cell) -> Buffer {
|
||||
let size = area.area() as usize;
|
||||
let mut content = Vec::with_capacity(size);
|
||||
for _ in 0..size {
|
||||
content.push(cell.clone());
|
||||
}
|
||||
Buffer { area, content }
|
||||
}
|
||||
|
||||
/// Returns a Buffer containing the given lines
|
||||
pub fn with_lines<'a, S>(lines: Vec<S>) -> Buffer
|
||||
where
|
||||
S: Into<Line<'a>>,
|
||||
{
|
||||
let lines = lines.into_iter().map(Into::into).collect::<Vec<_>>();
|
||||
let height = lines.len() as u16;
|
||||
let width = lines.iter().map(Line::width).max().unwrap_or_default() as u16;
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, width, height));
|
||||
for (y, line) in lines.iter().enumerate() {
|
||||
buffer.set_line(0, y as u16, line, width);
|
||||
}
|
||||
buffer
|
||||
}
|
||||
|
||||
/// Returns the content of the buffer as a slice
|
||||
pub fn content(&self) -> &[Cell] {
|
||||
&self.content
|
||||
}
|
||||
|
||||
/// Returns the area covered by this buffer
|
||||
pub fn area(&self) -> &Rect {
|
||||
&self.area
|
||||
}
|
||||
|
||||
/// Returns a reference to Cell at the given coordinates
|
||||
pub fn get(&self, x: u16, y: u16) -> &Cell {
|
||||
let i = self.index_of(x, y);
|
||||
&self.content[i]
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to Cell at the given coordinates
|
||||
pub fn get_mut(&mut self, x: u16, y: u16) -> &mut Cell {
|
||||
let i = self.index_of(x, y);
|
||||
&mut self.content[i]
|
||||
}
|
||||
|
||||
/// Returns the index in the `Vec<Cell>` for the given global (x, y) coordinates.
|
||||
///
|
||||
/// Global coordinates are offset by the Buffer's area offset (`x`/`y`).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let rect = Rect::new(200, 100, 10, 10);
|
||||
/// let buffer = Buffer::empty(rect);
|
||||
/// // Global coordinates to the top corner of this buffer's area
|
||||
/// assert_eq!(buffer.index_of(200, 100), 0);
|
||||
/// ```
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics when given an coordinate that is outside of this Buffer's area.
|
||||
///
|
||||
/// ```should_panic
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let rect = Rect::new(200, 100, 10, 10);
|
||||
/// let buffer = Buffer::empty(rect);
|
||||
/// // Top coordinate is outside of the buffer in global coordinate space, as the Buffer's area
|
||||
/// // starts at (200, 100).
|
||||
/// buffer.index_of(0, 0); // Panics
|
||||
/// ```
|
||||
pub fn index_of(&self, x: u16, y: u16) -> usize {
|
||||
debug_assert!(
|
||||
x >= self.area.left()
|
||||
&& x < self.area.right()
|
||||
&& y >= self.area.top()
|
||||
&& y < self.area.bottom(),
|
||||
"Trying to access position outside the buffer: x={x}, y={y}, area={:?}",
|
||||
self.area
|
||||
);
|
||||
((y - self.area.y) * self.area.width + (x - self.area.x)) as usize
|
||||
}
|
||||
|
||||
/// Returns the (global) coordinates of a cell given its index
|
||||
///
|
||||
/// Global coordinates are offset by the Buffer's area offset (`x`/`y`).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let rect = Rect::new(200, 100, 10, 10);
|
||||
/// let buffer = Buffer::empty(rect);
|
||||
/// assert_eq!(buffer.pos_of(0), (200, 100));
|
||||
/// assert_eq!(buffer.pos_of(14), (204, 101));
|
||||
/// ```
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics when given an index that is outside the Buffer's content.
|
||||
///
|
||||
/// ```should_panic
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let rect = Rect::new(0, 0, 10, 10); // 100 cells in total
|
||||
/// let buffer = Buffer::empty(rect);
|
||||
/// // Index 100 is the 101th cell, which lies outside of the area of this Buffer.
|
||||
/// buffer.pos_of(100); // Panics
|
||||
/// ```
|
||||
pub fn pos_of(&self, i: usize) -> (u16, u16) {
|
||||
debug_assert!(
|
||||
i < self.content.len(),
|
||||
"Trying to get the coords of a cell outside the buffer: i={i} len={}",
|
||||
self.content.len()
|
||||
);
|
||||
(
|
||||
self.area.x + (i as u16) % self.area.width,
|
||||
self.area.y + (i as u16) / self.area.width,
|
||||
)
|
||||
}
|
||||
|
||||
/// Print a string, starting at the position (x, y)
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
pub fn set_string<T, S>(&mut self, x: u16, y: u16, string: T, style: S)
|
||||
where
|
||||
T: AsRef<str>,
|
||||
S: Into<Style>,
|
||||
{
|
||||
self.set_stringn(x, y, string, usize::MAX, style.into());
|
||||
}
|
||||
|
||||
/// Print at most the first n characters of a string if enough space is available
|
||||
/// until the end of the line
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
pub fn set_stringn<T, S>(
|
||||
&mut self,
|
||||
x: u16,
|
||||
y: u16,
|
||||
string: T,
|
||||
width: usize,
|
||||
style: S,
|
||||
) -> (u16, u16)
|
||||
where
|
||||
T: AsRef<str>,
|
||||
S: Into<Style>,
|
||||
{
|
||||
let style = style.into();
|
||||
let mut index = self.index_of(x, y);
|
||||
let mut x_offset = x as usize;
|
||||
let graphemes = UnicodeSegmentation::graphemes(string.as_ref(), true);
|
||||
let max_offset = min(self.area.right() as usize, width.saturating_add(x as usize));
|
||||
for s in graphemes {
|
||||
let width = s.width();
|
||||
if width == 0 {
|
||||
continue;
|
||||
}
|
||||
// `x_offset + width > max_offset` could be integer overflow on 32-bit machines if we
|
||||
// change dimensions to usize or u32 and someone resizes the terminal to 1x2^32.
|
||||
if width > max_offset.saturating_sub(x_offset) {
|
||||
break;
|
||||
}
|
||||
|
||||
self.content[index].set_symbol(s);
|
||||
self.content[index].set_style(style);
|
||||
// Reset following cells if multi-width (they would be hidden by the grapheme),
|
||||
for i in index + 1..index + width {
|
||||
self.content[i].reset();
|
||||
}
|
||||
index += width;
|
||||
x_offset += width;
|
||||
}
|
||||
(x_offset as u16, y)
|
||||
}
|
||||
|
||||
/// Print a line, starting at the position (x, y)
|
||||
pub fn set_line(&mut self, x: u16, y: u16, line: &Line<'_>, width: u16) -> (u16, u16) {
|
||||
let mut remaining_width = width;
|
||||
let mut x = x;
|
||||
for span in &line.spans {
|
||||
if remaining_width == 0 {
|
||||
break;
|
||||
}
|
||||
let pos = self.set_stringn(
|
||||
x,
|
||||
y,
|
||||
span.content.as_ref(),
|
||||
remaining_width as usize,
|
||||
span.style,
|
||||
);
|
||||
let w = pos.0.saturating_sub(x);
|
||||
x = pos.0;
|
||||
remaining_width = remaining_width.saturating_sub(w);
|
||||
}
|
||||
(x, y)
|
||||
}
|
||||
|
||||
/// Print a span, starting at the position (x, y)
|
||||
pub fn set_span(&mut self, x: u16, y: u16, span: &Span<'_>, width: u16) -> (u16, u16) {
|
||||
self.set_stringn(x, y, span.content.as_ref(), width as usize, span.style)
|
||||
}
|
||||
|
||||
/// Set the style of all cells in the given area.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
pub fn set_style<S: Into<Style>>(&mut self, area: Rect, style: S) {
|
||||
let style = style.into();
|
||||
let area = self.area.intersection(area);
|
||||
for y in area.top()..area.bottom() {
|
||||
for x in area.left()..area.right() {
|
||||
self.get_mut(x, y).set_style(style);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Resize the buffer so that the mapped area matches the given area and that the buffer
|
||||
/// length is equal to area.width * area.height
|
||||
pub fn resize(&mut self, area: Rect) {
|
||||
let length = area.area() as usize;
|
||||
if self.content.len() > length {
|
||||
self.content.truncate(length);
|
||||
} else {
|
||||
self.content.resize(length, Cell::default());
|
||||
}
|
||||
self.area = area;
|
||||
}
|
||||
|
||||
/// Reset all cells in the buffer
|
||||
pub fn reset(&mut self) {
|
||||
for c in &mut self.content {
|
||||
c.reset();
|
||||
}
|
||||
}
|
||||
|
||||
/// Merge an other buffer into this one
|
||||
pub fn merge(&mut self, other: &Buffer) {
|
||||
let area = self.area.union(other.area);
|
||||
let cell = Cell::default();
|
||||
self.content.resize(area.area() as usize, cell.clone());
|
||||
|
||||
// Move original content to the appropriate space
|
||||
let size = self.area.area() as usize;
|
||||
for i in (0..size).rev() {
|
||||
let (x, y) = self.pos_of(i);
|
||||
// New index in content
|
||||
let k = ((y - area.y) * area.width + x - area.x) as usize;
|
||||
if i != k {
|
||||
self.content[k] = self.content[i].clone();
|
||||
self.content[i] = cell.clone();
|
||||
}
|
||||
}
|
||||
|
||||
// Push content of the other buffer into this one (may erase previous
|
||||
// data)
|
||||
let size = other.area.area() as usize;
|
||||
for i in 0..size {
|
||||
let (x, y) = other.pos_of(i);
|
||||
// New index in content
|
||||
let k = ((y - area.y) * area.width + x - area.x) as usize;
|
||||
self.content[k] = other.content[i].clone();
|
||||
}
|
||||
self.area = area;
|
||||
}
|
||||
|
||||
/// Builds a minimal sequence of coordinates and Cells necessary to update the UI from
|
||||
/// self to other.
|
||||
///
|
||||
/// We're assuming that buffers are well-formed, that is no double-width cell is followed by
|
||||
/// a non-blank cell.
|
||||
///
|
||||
/// # Multi-width characters handling:
|
||||
///
|
||||
/// ```text
|
||||
/// (Index:) `01`
|
||||
/// Prev: `コ`
|
||||
/// Next: `aa`
|
||||
/// Updates: `0: a, 1: a'
|
||||
/// ```
|
||||
///
|
||||
/// ```text
|
||||
/// (Index:) `01`
|
||||
/// Prev: `a `
|
||||
/// Next: `コ`
|
||||
/// Updates: `0: コ` (double width symbol at index 0 - skip index 1)
|
||||
/// ```
|
||||
///
|
||||
/// ```text
|
||||
/// (Index:) `012`
|
||||
/// Prev: `aaa`
|
||||
/// Next: `aコ`
|
||||
/// Updates: `0: a, 1: コ` (double width symbol at index 1 - skip index 2)
|
||||
/// ```
|
||||
pub fn diff<'a>(&self, other: &'a Buffer) -> Vec<(u16, u16, &'a Cell)> {
|
||||
let previous_buffer = &self.content;
|
||||
let next_buffer = &other.content;
|
||||
|
||||
let mut updates: Vec<(u16, u16, &Cell)> = vec![];
|
||||
// Cells invalidated by drawing/replacing preceding multi-width characters:
|
||||
let mut invalidated: usize = 0;
|
||||
// Cells from the current buffer to skip due to preceding multi-width characters taking
|
||||
// their place (the skipped cells should be blank anyway), or due to per-cell-skipping:
|
||||
let mut to_skip: usize = 0;
|
||||
for (i, (current, previous)) in next_buffer.iter().zip(previous_buffer.iter()).enumerate() {
|
||||
if !current.skip && (current != previous || invalidated > 0) && to_skip == 0 {
|
||||
let (x, y) = self.pos_of(i);
|
||||
updates.push((x, y, &next_buffer[i]));
|
||||
}
|
||||
|
||||
to_skip = current.symbol().width().saturating_sub(1);
|
||||
|
||||
let affected_width = std::cmp::max(current.symbol().width(), previous.symbol().width());
|
||||
invalidated = std::cmp::max(affected_width, invalidated).saturating_sub(1);
|
||||
}
|
||||
updates
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Buffer {
|
||||
/// Writes a debug representation of the buffer to the given formatter.
|
||||
///
|
||||
/// The format is like a pretty printed struct, with the following fields:
|
||||
/// * `area`: displayed as `Rect { x: 1, y: 2, width: 3, height: 4 }`
|
||||
/// * `content`: displayed as a list of strings representing the content of the buffer
|
||||
/// * `styles`: displayed as a list of: `{ x: 1, y: 2, fg: Color::Red, bg: Color::Blue,
|
||||
/// modifier: Modifier::BOLD }` only showing a value when there is a change in style.
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
|
||||
f.write_fmt(format_args!(
|
||||
"Buffer {{\n area: {:?},\n content: [\n",
|
||||
&self.area
|
||||
))?;
|
||||
let mut last_style = None;
|
||||
let mut styles = vec![];
|
||||
for (y, line) in self.content.chunks(self.area.width as usize).enumerate() {
|
||||
let mut overwritten = vec![];
|
||||
let mut skip: usize = 0;
|
||||
f.write_str(" \"")?;
|
||||
for (x, c) in line.iter().enumerate() {
|
||||
if skip == 0 {
|
||||
f.write_str(c.symbol())?;
|
||||
} else {
|
||||
overwritten.push((x, c.symbol()));
|
||||
}
|
||||
skip = std::cmp::max(skip, c.symbol().width()).saturating_sub(1);
|
||||
#[cfg(feature = "underline-color")]
|
||||
{
|
||||
let style = (c.fg, c.bg, c.underline_color, c.modifier);
|
||||
if last_style != Some(style) {
|
||||
last_style = Some(style);
|
||||
styles.push((x, y, c.fg, c.bg, c.underline_color, c.modifier));
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "underline-color"))]
|
||||
{
|
||||
let style = (c.fg, c.bg, c.modifier);
|
||||
if last_style != Some(style) {
|
||||
last_style = Some(style);
|
||||
styles.push((x, y, c.fg, c.bg, c.modifier));
|
||||
}
|
||||
}
|
||||
}
|
||||
if !overwritten.is_empty() {
|
||||
f.write_fmt(format_args!(
|
||||
"// hidden by multi-width symbols: {overwritten:?}"
|
||||
))?;
|
||||
}
|
||||
f.write_str("\",\n")?;
|
||||
}
|
||||
f.write_str(" ],\n styles: [\n")?;
|
||||
for s in styles {
|
||||
#[cfg(feature = "underline-color")]
|
||||
f.write_fmt(format_args!(
|
||||
" x: {}, y: {}, fg: {:?}, bg: {:?}, underline: {:?}, modifier: {:?},\n",
|
||||
s.0, s.1, s.2, s.3, s.4, s.5
|
||||
))?;
|
||||
#[cfg(not(feature = "underline-color"))]
|
||||
f.write_fmt(format_args!(
|
||||
" x: {}, y: {}, fg: {:?}, bg: {:?}, modifier: {:?},\n",
|
||||
s.0, s.1, s.2, s.3, s.4
|
||||
))?;
|
||||
}
|
||||
f.write_str(" ]\n}")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::assert_buffer_eq;
|
||||
|
||||
fn cell(s: &str) -> Cell {
|
||||
let mut cell = Cell::default();
|
||||
cell.set_symbol(s);
|
||||
cell
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn debug() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 12, 2));
|
||||
buf.set_string(0, 0, "Hello World!", Style::default());
|
||||
buf.set_string(
|
||||
0,
|
||||
1,
|
||||
"G'day World!",
|
||||
Style::default()
|
||||
.fg(Color::Green)
|
||||
.bg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
);
|
||||
#[cfg(feature = "underline-color")]
|
||||
assert_eq!(
|
||||
format!("{buf:?}"),
|
||||
indoc::indoc!(
|
||||
"
|
||||
Buffer {
|
||||
area: Rect { x: 0, y: 0, width: 12, height: 2 },
|
||||
content: [
|
||||
\"Hello World!\",
|
||||
\"G'day World!\",
|
||||
],
|
||||
styles: [
|
||||
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 0, y: 1, fg: Green, bg: Yellow, underline: Reset, modifier: BOLD,
|
||||
]
|
||||
}"
|
||||
)
|
||||
);
|
||||
#[cfg(not(feature = "underline-color"))]
|
||||
assert_eq!(
|
||||
format!("{buf:?}"),
|
||||
indoc::indoc!(
|
||||
"
|
||||
Buffer {
|
||||
area: Rect { x: 0, y: 0, width: 12, height: 2 },
|
||||
content: [
|
||||
\"Hello World!\",
|
||||
\"G'day World!\",
|
||||
],
|
||||
styles: [
|
||||
x: 0, y: 0, fg: Reset, bg: Reset, modifier: NONE,
|
||||
x: 0, y: 1, fg: Green, bg: Yellow, modifier: BOLD,
|
||||
]
|
||||
}"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_translates_to_and_from_coordinates() {
|
||||
let rect = Rect::new(200, 100, 50, 80);
|
||||
let buf = Buffer::empty(rect);
|
||||
|
||||
// First cell is at the upper left corner.
|
||||
assert_eq!(buf.pos_of(0), (200, 100));
|
||||
assert_eq!(buf.index_of(200, 100), 0);
|
||||
|
||||
// Last cell is in the lower right.
|
||||
assert_eq!(buf.pos_of(buf.content.len() - 1), (249, 179));
|
||||
assert_eq!(buf.index_of(249, 179), buf.content.len() - 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "outside the buffer")]
|
||||
fn pos_of_panics_on_out_of_bounds() {
|
||||
let rect = Rect::new(0, 0, 10, 10);
|
||||
let buf = Buffer::empty(rect);
|
||||
|
||||
// There are a total of 100 cells; zero-indexed means that 100 would be the 101st cell.
|
||||
buf.pos_of(100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "outside the buffer")]
|
||||
fn index_of_panics_on_out_of_bounds() {
|
||||
let rect = Rect::new(0, 0, 10, 10);
|
||||
let buf = Buffer::empty(rect);
|
||||
|
||||
// width is 10; zero-indexed means that 10 would be the 11th cell.
|
||||
buf.index_of(10, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_string() {
|
||||
let area = Rect::new(0, 0, 5, 1);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
|
||||
// Zero-width
|
||||
buffer.set_stringn(0, 0, "aaa", 0, Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" "]));
|
||||
|
||||
buffer.set_string(0, 0, "aaa", Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["aaa "]));
|
||||
|
||||
// Width limit:
|
||||
buffer.set_stringn(0, 0, "bbbbbbbbbbbbbb", 4, Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["bbbb "]));
|
||||
|
||||
buffer.set_string(0, 0, "12345", Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["12345"]));
|
||||
|
||||
// Width truncation:
|
||||
buffer.set_string(0, 0, "123456", Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["12345"]));
|
||||
|
||||
// multi-line
|
||||
buffer = Buffer::empty(Rect::new(0, 0, 5, 2));
|
||||
buffer.set_string(0, 0, "12345", Style::default());
|
||||
buffer.set_string(0, 1, "67890", Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["12345", "67890"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_string_multi_width_overwrite() {
|
||||
let area = Rect::new(0, 0, 5, 1);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
|
||||
// multi-width overwrite
|
||||
buffer.set_string(0, 0, "aaaaa", Style::default());
|
||||
buffer.set_string(0, 0, "称号", Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["称号a"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_string_zero_width() {
|
||||
let area = Rect::new(0, 0, 1, 1);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
|
||||
// Leading grapheme with zero width
|
||||
let s = "\u{1}a";
|
||||
buffer.set_stringn(0, 0, s, 1, Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["a"]));
|
||||
|
||||
// Trailing grapheme with zero with
|
||||
let s = "a\u{1}";
|
||||
buffer.set_stringn(0, 0, s, 1, Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["a"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_string_double_width() {
|
||||
let area = Rect::new(0, 0, 5, 1);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
buffer.set_string(0, 0, "コン", Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["コン "]));
|
||||
|
||||
// Only 1 space left.
|
||||
buffer.set_string(0, 0, "コンピ", Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["コン "]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_style() {
|
||||
let mut buffer = Buffer::with_lines(vec!["aaaaa", "bbbbb", "ccccc"]);
|
||||
buffer.set_style(Rect::new(0, 1, 5, 1), Style::new().red());
|
||||
assert_buffer_eq!(
|
||||
buffer,
|
||||
Buffer::with_lines(vec!["aaaaa".into(), "bbbbb".red(), "ccccc".into(),])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_style_does_not_panic_when_out_of_area() {
|
||||
let mut buffer = Buffer::with_lines(vec!["aaaaa", "bbbbb", "ccccc"]);
|
||||
buffer.set_style(Rect::new(0, 1, 10, 3), Style::new().red());
|
||||
assert_buffer_eq!(
|
||||
buffer,
|
||||
Buffer::with_lines(vec!["aaaaa".into(), "bbbbb".red(), "ccccc".red(),])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_lines() {
|
||||
let buffer =
|
||||
Buffer::with_lines(vec!["┌────────┐", "│コンピュ│", "│ーa 上で│", "└────────┘"]);
|
||||
assert_eq!(buffer.area.x, 0);
|
||||
assert_eq!(buffer.area.y, 0);
|
||||
assert_eq!(buffer.area.width, 10);
|
||||
assert_eq!(buffer.area.height, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_empty_empty() {
|
||||
let area = Rect::new(0, 0, 40, 40);
|
||||
let prev = Buffer::empty(area);
|
||||
let next = Buffer::empty(area);
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(diff, vec![]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_empty_filled() {
|
||||
let area = Rect::new(0, 0, 40, 40);
|
||||
let prev = Buffer::empty(area);
|
||||
let next = Buffer::filled(area, Cell::default().set_symbol("a"));
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(diff.len(), 40 * 40);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_filled_filled() {
|
||||
let area = Rect::new(0, 0, 40, 40);
|
||||
let prev = Buffer::filled(area, Cell::default().set_symbol("a"));
|
||||
let next = Buffer::filled(area, Cell::default().set_symbol("a"));
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(diff, vec![]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_single_width() {
|
||||
let prev = Buffer::with_lines(vec![
|
||||
" ",
|
||||
"┌Title─┐ ",
|
||||
"│ │ ",
|
||||
"│ │ ",
|
||||
"└──────┘ ",
|
||||
]);
|
||||
let next = Buffer::with_lines(vec![
|
||||
" ",
|
||||
"┌TITLE─┐ ",
|
||||
"│ │ ",
|
||||
"│ │ ",
|
||||
"└──────┘ ",
|
||||
]);
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(
|
||||
diff,
|
||||
vec![
|
||||
(2, 1, &cell("I")),
|
||||
(3, 1, &cell("T")),
|
||||
(4, 1, &cell("L")),
|
||||
(5, 1, &cell("E")),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[rustfmt::skip]
|
||||
fn diff_multi_width() {
|
||||
let prev = Buffer::with_lines(vec![
|
||||
"┌Title─┐ ",
|
||||
"└──────┘ ",
|
||||
]);
|
||||
let next = Buffer::with_lines(vec![
|
||||
"┌称号──┐ ",
|
||||
"└──────┘ ",
|
||||
]);
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(
|
||||
diff,
|
||||
vec![
|
||||
(1, 0, &cell("称")),
|
||||
// Skipped "i"
|
||||
(3, 0, &cell("号")),
|
||||
// Skipped "l"
|
||||
(5, 0, &cell("─")),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_multi_width_offset() {
|
||||
let prev = Buffer::with_lines(vec!["┌称号──┐"]);
|
||||
let next = Buffer::with_lines(vec!["┌─称号─┐"]);
|
||||
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(
|
||||
diff,
|
||||
vec![(1, 0, &cell("─")), (2, 0, &cell("称")), (4, 0, &cell("号")),]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_skip() {
|
||||
let prev = Buffer::with_lines(vec!["123"]);
|
||||
let mut next = Buffer::with_lines(vec!["456"]);
|
||||
for i in 1..3 {
|
||||
next.content[i].set_skip(true);
|
||||
}
|
||||
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(diff, vec![(0, 0, &cell("4"))],);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge() {
|
||||
let mut one = Buffer::filled(
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("1"),
|
||||
);
|
||||
let two = Buffer::filled(
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 2,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("2"),
|
||||
);
|
||||
one.merge(&two);
|
||||
assert_buffer_eq!(one, Buffer::with_lines(vec!["11", "11", "22", "22"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge2() {
|
||||
let mut one = Buffer::filled(
|
||||
Rect {
|
||||
x: 2,
|
||||
y: 2,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("1"),
|
||||
);
|
||||
let two = Buffer::filled(
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("2"),
|
||||
);
|
||||
one.merge(&two);
|
||||
assert_buffer_eq!(
|
||||
one,
|
||||
Buffer::with_lines(vec!["22 ", "22 ", " 11", " 11"])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge3() {
|
||||
let mut one = Buffer::filled(
|
||||
Rect {
|
||||
x: 3,
|
||||
y: 3,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("1"),
|
||||
);
|
||||
let two = Buffer::filled(
|
||||
Rect {
|
||||
x: 1,
|
||||
y: 1,
|
||||
width: 3,
|
||||
height: 4,
|
||||
},
|
||||
Cell::default().set_symbol("2"),
|
||||
);
|
||||
one.merge(&two);
|
||||
let mut merged = Buffer::with_lines(vec!["222 ", "222 ", "2221", "2221"]);
|
||||
merged.area = Rect {
|
||||
x: 1,
|
||||
y: 1,
|
||||
width: 4,
|
||||
height: 4,
|
||||
};
|
||||
assert_buffer_eq!(one, merged);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_skip() {
|
||||
let mut one = Buffer::filled(
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("1"),
|
||||
);
|
||||
let two = Buffer::filled(
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 1,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("2").set_skip(true),
|
||||
);
|
||||
one.merge(&two);
|
||||
let skipped: Vec<bool> = one.content().iter().map(|c| c.skip).collect();
|
||||
assert_eq!(skipped, vec![false, false, true, true, true, true]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_skip2() {
|
||||
let mut one = Buffer::filled(
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("1").set_skip(true),
|
||||
);
|
||||
let two = Buffer::filled(
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 1,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("2"),
|
||||
);
|
||||
one.merge(&two);
|
||||
let skipped: Vec<bool> = one.content().iter().map(|c| c.skip).collect();
|
||||
assert_eq!(skipped, vec![true, true, false, false, false, false]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_lines_accepts_into_lines() {
|
||||
use crate::style::Stylize;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 3, 2));
|
||||
buf.set_string(0, 0, "foo", Style::new().red());
|
||||
buf.set_string(0, 1, "bar", Style::new().blue());
|
||||
assert_eq!(buf, Buffer::with_lines(vec!["foo".red(), "bar".blue()]));
|
||||
}
|
||||
}
|
||||
160
src/buffer/cell.rs
Normal file
160
src/buffer/cell.rs
Normal file
@@ -0,0 +1,160 @@
|
||||
use std::fmt::Debug;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
/// A buffer cell
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Cell {
|
||||
#[deprecated(
|
||||
since = "0.24.1",
|
||||
note = "This field will be hidden at next major version. Use `Cell::symbol` method to get \
|
||||
the value. Use `Cell::set_symbol` to update the field. Use `Cell::default` to \
|
||||
create `Cell` instance"
|
||||
)]
|
||||
/// The string to be drawn in the cell.
|
||||
///
|
||||
/// This accepts unicode grapheme clusters which might take up more than one cell.
|
||||
pub symbol: String,
|
||||
|
||||
/// The foreground color of the cell.
|
||||
pub fg: Color,
|
||||
|
||||
/// The background color of the cell.
|
||||
pub bg: Color,
|
||||
|
||||
/// The underline color of the cell.
|
||||
///
|
||||
/// This is only used when the `underline-color` feature is enabled.
|
||||
#[cfg(feature = "underline-color")]
|
||||
pub underline_color: Color,
|
||||
|
||||
/// The modifier of the cell.
|
||||
pub modifier: Modifier,
|
||||
|
||||
/// Whether the cell should be skipped when copying (diffing) the buffer to the screen.
|
||||
pub skip: bool,
|
||||
}
|
||||
|
||||
#[allow(deprecated)] // For Cell::symbol
|
||||
impl Cell {
|
||||
/// Gets the symbol of the cell.
|
||||
pub fn symbol(&self) -> &str {
|
||||
self.symbol.as_str()
|
||||
}
|
||||
|
||||
/// Sets the symbol of the cell.
|
||||
pub fn set_symbol(&mut self, symbol: &str) -> &mut Cell {
|
||||
self.symbol.clear();
|
||||
self.symbol.push_str(symbol);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the symbol of the cell to a single character.
|
||||
pub fn set_char(&mut self, ch: char) -> &mut Cell {
|
||||
self.symbol.clear();
|
||||
self.symbol.push(ch);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the foreground color of the cell.
|
||||
pub fn set_fg(&mut self, color: Color) -> &mut Cell {
|
||||
self.fg = color;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the background color of the cell.
|
||||
pub fn set_bg(&mut self, color: Color) -> &mut Cell {
|
||||
self.bg = color;
|
||||
self
|
||||
}
|
||||
/// Sets the style of the cell.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
pub fn set_style<S: Into<Style>>(&mut self, style: S) -> &mut Cell {
|
||||
let style = style.into();
|
||||
if let Some(c) = style.fg {
|
||||
self.fg = c;
|
||||
}
|
||||
if let Some(c) = style.bg {
|
||||
self.bg = c;
|
||||
}
|
||||
#[cfg(feature = "underline-color")]
|
||||
if let Some(c) = style.underline_color {
|
||||
self.underline_color = c;
|
||||
}
|
||||
self.modifier.insert(style.add_modifier);
|
||||
self.modifier.remove(style.sub_modifier);
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the style of the cell.
|
||||
pub fn style(&self) -> Style {
|
||||
#[cfg(feature = "underline-color")]
|
||||
return Style::default()
|
||||
.fg(self.fg)
|
||||
.bg(self.bg)
|
||||
.underline_color(self.underline_color)
|
||||
.add_modifier(self.modifier);
|
||||
|
||||
#[cfg(not(feature = "underline-color"))]
|
||||
return Style::default()
|
||||
.fg(self.fg)
|
||||
.bg(self.bg)
|
||||
.add_modifier(self.modifier);
|
||||
}
|
||||
|
||||
/// Sets the cell to be skipped when copying (diffing) the buffer to the screen.
|
||||
///
|
||||
/// This is helpful when it is necessary to prevent the buffer from overwriting a cell that is
|
||||
/// covered by an image from some terminal graphics protocol (Sixel / iTerm / Kitty ...).
|
||||
pub fn set_skip(&mut self, skip: bool) -> &mut Cell {
|
||||
self.skip = skip;
|
||||
self
|
||||
}
|
||||
|
||||
/// Resets the cell to the default state.
|
||||
pub fn reset(&mut self) {
|
||||
self.symbol.clear();
|
||||
self.symbol.push(' ');
|
||||
self.fg = Color::Reset;
|
||||
self.bg = Color::Reset;
|
||||
#[cfg(feature = "underline-color")]
|
||||
{
|
||||
self.underline_color = Color::Reset;
|
||||
}
|
||||
self.modifier = Modifier::empty();
|
||||
self.skip = false;
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Cell {
|
||||
fn default() -> Cell {
|
||||
#[allow(deprecated)] // For Cell::symbol
|
||||
Cell {
|
||||
symbol: " ".into(),
|
||||
fg: Color::Reset,
|
||||
bg: Color::Reset,
|
||||
#[cfg(feature = "underline-color")]
|
||||
underline_color: Color::Reset,
|
||||
modifier: Modifier::empty(),
|
||||
skip: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn symbol_field() {
|
||||
let mut cell = Cell::default();
|
||||
assert_eq!(cell.symbol(), " ");
|
||||
cell.set_symbol("あ"); // Multi-byte character
|
||||
assert_eq!(cell.symbol(), "あ");
|
||||
cell.set_symbol("👨👩👧👦"); // Multiple code units combined with ZWJ
|
||||
assert_eq!(cell.symbol(), "👨👩👧👦");
|
||||
}
|
||||
}
|
||||
1504
src/layout.rs
1504
src/layout.rs
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,10 @@ use std::{
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
mod offset;
|
||||
|
||||
pub use offset::*;
|
||||
|
||||
/// A simple rectangle used in the computation of the layout and to give widgets a hint about the
|
||||
/// area they are supposed to render to.
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
@@ -106,6 +110,26 @@ impl Rect {
|
||||
}
|
||||
}
|
||||
|
||||
/// Moves the `Rect` without modifying its size.
|
||||
///
|
||||
/// Moves the `Rect` according to the given offset without modifying its [`width`](Rect::width)
|
||||
/// or [`height`](Rect::height).
|
||||
/// - Positive `x` moves the whole `Rect` to the right, negative to the left.
|
||||
/// - Positive `y` moves the whole `Rect` to the bottom, negative to the top.
|
||||
///
|
||||
/// See [`Offset`] for details.
|
||||
pub fn offset(self, offset: Offset) -> Rect {
|
||||
Rect {
|
||||
x: i32::from(self.x)
|
||||
.saturating_add(offset.x)
|
||||
.clamp(0, (u16::MAX - self.width) as i32) as u16,
|
||||
y: i32::from(self.y)
|
||||
.saturating_add(offset.y)
|
||||
.clamp(0, (u16::MAX - self.height) as i32) as u16,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a new rect that contains both the current one and the given one.
|
||||
pub fn union(self, other: Rect) -> Rect {
|
||||
let x1 = min(self.x, other.x);
|
||||
@@ -131,8 +155,8 @@ impl Rect {
|
||||
Rect {
|
||||
x: x1,
|
||||
y: y1,
|
||||
width: x2 - x1,
|
||||
height: y2 - y1,
|
||||
width: x2.saturating_sub(x1),
|
||||
height: y2.saturating_sub(y1),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,6 +231,39 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offset() {
|
||||
assert_eq!(
|
||||
Rect::new(1, 2, 3, 4).offset(Offset { x: 5, y: 6 }),
|
||||
Rect::new(6, 8, 3, 4),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn negative_offset() {
|
||||
assert_eq!(
|
||||
Rect::new(4, 3, 3, 4).offset(Offset { x: -2, y: -1 }),
|
||||
Rect::new(2, 2, 3, 4),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn negative_offset_saturate() {
|
||||
assert_eq!(
|
||||
Rect::new(1, 2, 3, 4).offset(Offset { x: -5, y: -6 }),
|
||||
Rect::new(0, 0, 3, 4),
|
||||
);
|
||||
}
|
||||
|
||||
/// Offsets a [`Rect`] making it go outside [`u16::MAX`], it should keep its size.
|
||||
#[test]
|
||||
fn offset_saturate_max() {
|
||||
assert_eq!(
|
||||
Rect::new(u16::MAX - 500, u16::MAX - 500, 100, 100).offset(Offset { x: 1000, y: 1000 }),
|
||||
Rect::new(u16::MAX - 100, u16::MAX - 100, 100, 100),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn union() {
|
||||
assert_eq!(
|
||||
@@ -223,6 +280,14 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersection_underflow() {
|
||||
assert_eq!(
|
||||
Rect::new(1, 1, 2, 2).intersection(Rect::new(4, 4, 2, 2)),
|
||||
Rect::new(4, 4, 0, 0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersects() {
|
||||
assert!(Rect::new(1, 2, 3, 4).intersects(Rect::new(2, 3, 4, 5)));
|
||||
|
||||
12
src/layout/rect/offset.rs
Normal file
12
src/layout/rect/offset.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
/// Amounts by which to move a [`Rect`](super::Rect).
|
||||
///
|
||||
/// Positive numbers move to the right/bottom and negative to the left/top.
|
||||
///
|
||||
/// See [`Rect::offset`](super::Rect::offset)
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub struct Offset {
|
||||
/// How much to move on the X axis
|
||||
pub x: i32,
|
||||
/// How much to move on the Y axis
|
||||
pub y: i32,
|
||||
}
|
||||
126
src/lib.rs
126
src/lib.rs
@@ -4,27 +4,29 @@
|
||||
//!
|
||||
//! <div align="center">
|
||||
//!
|
||||
//! [![Crate Badge]](https://crates.io/crates/ratatui) [![License Badge]](./LICENSE) [![CI
|
||||
//! Badge]](https://github.com/ratatui-org/ratatui/actions?query=workflow%3ACI+) [![Docs
|
||||
//! Badge]](https://docs.rs/crate/ratatui/)<br>
|
||||
//! [![Dependencies Badge]](https://deps.rs/repo/github/ratatui-org/ratatui) [![Codecov
|
||||
//! Badge]](https://app.codecov.io/gh/ratatui-org/ratatui) [![Discord
|
||||
//! Badge]](https://discord.gg/pMCEU9hNEj) [![Matrix
|
||||
//! Badge]](https://matrix.to/#/#ratatui:matrix.org)<br>
|
||||
//! [Documentation](https://docs.rs/ratatui) · [Ratatui Book](https://ratatui.rs) ·
|
||||
//! [Examples](https://github.com/ratatui-org/ratatui/tree/main/examples) · [Report a
|
||||
//! bug](https://github.com/ratatui-org/ratatui/issues/new?labels=bug&projects=&template=bug_report.md)
|
||||
//! · [Request a
|
||||
//! Feature](https://github.com/ratatui-org/ratatui/issues/new?labels=enhancement&projects=&template=feature_request.md)
|
||||
//! [![Crate Badge]](https://crates.io/crates/ratatui)
|
||||
//! [![License Badge]](./LICENSE)
|
||||
//! [![CI Badge]](https://github.com/ratatui-org/ratatui/actions?query=workflow%3ACI+)
|
||||
//! [![Docs Badge]](https://docs.rs/crate/ratatui/)<br>
|
||||
//! [![Dependencies Badge]](https://deps.rs/repo/github/ratatui-org/ratatui)
|
||||
//! [![Codecov Badge]](https://app.codecov.io/gh/ratatui-org/ratatui)
|
||||
//! [![Discord Badge]](https://discord.gg/pMCEU9hNEj)
|
||||
//! [![Matrix Badge]](https://matrix.to/#/#ratatui:matrix.org)<br>
|
||||
//!
|
||||
//! [Documentation](https://docs.rs/ratatui)
|
||||
//! · [Ratatui Website](https://ratatui.rs)
|
||||
//! · [Examples](https://github.com/ratatui-org/ratatui/tree/main/examples)
|
||||
//! · [Report a bug](https://github.com/ratatui-org/ratatui/issues/new?labels=bug&projects=&template=bug_report.md)
|
||||
//! · [Request a Feature](https://github.com/ratatui-org/ratatui/issues/new?labels=enhancement&projects=&template=feature_request.md)
|
||||
//! · [Send a Pull Request](https://github.com/ratatui-org/ratatui/compare)
|
||||
//!
|
||||
//! </div>
|
||||
//!
|
||||
//! # Ratatui
|
||||
//!
|
||||
//! [Ratatui] is a crate for cooking up terminal user interfaces in rust. It is a lightweight
|
||||
//! library that provides a set of widgets and utilities to build complex rust TUIs. Ratatui was
|
||||
//! forked from the [Tui-rs crate] in 2023 in order to continue its development.
|
||||
//! [Ratatui] is a crate for cooking up terminal user interfaces in Rust. It is a lightweight
|
||||
//! library that provides a set of widgets and utilities to build complex Rust TUIs. Ratatui was
|
||||
//! forked from the [tui-rs] crate in 2023 in order to continue its development.
|
||||
//!
|
||||
//! ## Installation
|
||||
//!
|
||||
@@ -35,7 +37,7 @@
|
||||
//! ```
|
||||
//!
|
||||
//! Ratatui uses [Crossterm] by default as it works on most platforms. See the [Installation]
|
||||
//! section of the [Ratatui Book] for more details on how to use other backends ([Termion] /
|
||||
//! section of the [Ratatui Website] for more details on how to use other backends ([Termion] /
|
||||
//! [Termwiz]).
|
||||
//!
|
||||
//! ## Introduction
|
||||
@@ -43,12 +45,12 @@
|
||||
//! Ratatui is based on the principle of immediate rendering with intermediate buffers. This means
|
||||
//! that for each frame, your app must render all widgets that are supposed to be part of the UI.
|
||||
//! This is in contrast to the retained mode style of rendering where widgets are updated and then
|
||||
//! automatically redrawn on the next frame. See the [Rendering] section of the [Ratatui Book] for
|
||||
//! more info.
|
||||
//! automatically redrawn on the next frame. See the [Rendering] section of the [Ratatui Website]
|
||||
//! for more info.
|
||||
//!
|
||||
//! ## Other documentation
|
||||
//!
|
||||
//! - [Ratatui Book] - explains the library's concepts and provides step-by-step tutorials
|
||||
//! - [Ratatui Website] - explains the library's concepts and provides step-by-step tutorials
|
||||
//! - [Examples] - a collection of examples that demonstrate how to use the library.
|
||||
//! - [API Documentation] - the full API documentation for the library on docs.rs.
|
||||
//! - [Changelog] - generated by [git-cliff] utilizing [Conventional Commits].
|
||||
@@ -60,12 +62,11 @@
|
||||
//! The following example demonstrates the minimal amount of code necessary to setup a terminal and
|
||||
//! render "Hello World!". The full code for this example which contains a little more detail is in
|
||||
//! [hello_world.rs]. For more guidance on different ways to structure your application see the
|
||||
//! [Application Patterns] and [Hello World tutorial] sections in the [Ratatui Book] and the various
|
||||
//! [Examples]. There are also several starter templates available:
|
||||
//! [Application Patterns] and [Hello World tutorial] sections in the [Ratatui Website] and the
|
||||
//! various [Examples]. There are also several starter templates available:
|
||||
//!
|
||||
//! - [rust-tui-template]
|
||||
//! - [ratatui-async-template] (book and template)
|
||||
//! - [simple-tui-rs]
|
||||
//! - [template]
|
||||
//! - [async-template] (book and template)
|
||||
//!
|
||||
//! Every application built with `ratatui` needs to implement the following steps:
|
||||
//!
|
||||
@@ -89,30 +90,31 @@
|
||||
//!
|
||||
//! Most applications should enter the Alternate Screen when starting and leave it when exiting and
|
||||
//! also enable raw mode to disable line buffering and enable reading key events. See the [`backend`
|
||||
//! module] and the [Backends] section of the [Ratatui Book] for more info.
|
||||
//! module] and the [Backends] section of the [Ratatui Website] for more info.
|
||||
//!
|
||||
//! ### Drawing the UI
|
||||
//!
|
||||
//! The drawing logic is delegated to a closure that takes a [`Frame`] instance as argument. The
|
||||
//! [`Frame`] provides the size of the area to draw to and allows the app to render any [`Widget`]
|
||||
//! using the provided [`render_widget`] method. See the [Widgets] section of the [Ratatui Book] for
|
||||
//! more info.
|
||||
//! using the provided [`render_widget`] method. See the [Widgets] section of the [Ratatui Website]
|
||||
//! for more info.
|
||||
//!
|
||||
//! ### Handling events
|
||||
//!
|
||||
//! Ratatui does not include any input handling. Instead event handling can be implemented by
|
||||
//! calling backend library methods directly. See the [Handling Events] section of the [Ratatui
|
||||
//! Book] for more info. For example, if you are using [Crossterm], you can use the
|
||||
//! Website] for more info. For example, if you are using [Crossterm], you can use the
|
||||
//! [`crossterm::event`] module to handle events.
|
||||
//!
|
||||
//! ### Example
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use std::io::{self, stdout};
|
||||
//!
|
||||
//! use crossterm::{
|
||||
//! event::{self, Event, KeyCode},
|
||||
//! terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
//! ExecutableCommand,
|
||||
//! terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}
|
||||
//! };
|
||||
//! use ratatui::{prelude::*, widgets::*};
|
||||
//!
|
||||
@@ -138,7 +140,7 @@
|
||||
//! if key.kind == event::KeyEventKind::Press && key.code == KeyCode::Char('q') {
|
||||
//! return Ok(true);
|
||||
//! }
|
||||
//! }
|
||||
//! }
|
||||
//! }
|
||||
//! Ok(false)
|
||||
//! }
|
||||
@@ -161,20 +163,21 @@
|
||||
//! The library comes with a basic yet useful layout management object called [`Layout`] which
|
||||
//! allows you to split the available space into multiple areas and then render widgets in each
|
||||
//! area. This lets you describe a responsive terminal UI by nesting layouts. See the [Layout]
|
||||
//! section of the [Ratatui Book] for more info.
|
||||
//! section of the [Ratatui Website] for more info.
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use ratatui::{prelude::*, widgets::*};
|
||||
//!
|
||||
//! fn ui(frame: &mut Frame) {
|
||||
//! let main_layout = Layout::default()
|
||||
//! .direction(Direction::Vertical)
|
||||
//! .constraints([
|
||||
//! let main_layout = Layout::new(
|
||||
//! Direction::Vertical,
|
||||
//! [
|
||||
//! Constraint::Length(1),
|
||||
//! Constraint::Min(0),
|
||||
//! Constraint::Length(1),
|
||||
//! ])
|
||||
//! .split(frame.size());
|
||||
//! ],
|
||||
//! )
|
||||
//! .split(frame.size());
|
||||
//! frame.render_widget(
|
||||
//! Block::new().borders(Borders::TOP).title("Title Bar"),
|
||||
//! main_layout[0],
|
||||
@@ -184,10 +187,11 @@
|
||||
//! main_layout[2],
|
||||
//! );
|
||||
//!
|
||||
//! let inner_layout = Layout::default()
|
||||
//! .direction(Direction::Horizontal)
|
||||
//! .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
//! .split(main_layout[1]);
|
||||
//! let inner_layout = Layout::new(
|
||||
//! Direction::Horizontal,
|
||||
//! [Constraint::Percentage(50), Constraint::Percentage(50)],
|
||||
//! )
|
||||
//! .split(main_layout[1]);
|
||||
//! frame.render_widget(
|
||||
//! Block::default().borders(Borders::ALL).title("Left"),
|
||||
//! inner_layout[0],
|
||||
@@ -213,22 +217,23 @@
|
||||
//! important one is [`Style`] which represents the foreground and background colors and the text
|
||||
//! attributes of a [`Span`]. The [`style` module] also provides a [`Stylize`] trait that allows
|
||||
//! short-hand syntax to apply a style to widgets and text. See the [Styling Text] section of the
|
||||
//! [Ratatui Book] for more info.
|
||||
//! [Ratatui Website] for more info.
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use ratatui::{prelude::*, widgets::*};
|
||||
//!
|
||||
//! fn ui(frame: &mut Frame) {
|
||||
//! let areas = Layout::default()
|
||||
//! .direction(Direction::Vertical)
|
||||
//! .constraints([
|
||||
//! let areas = Layout::new(
|
||||
//! Direction::Vertical,
|
||||
//! [
|
||||
//! Constraint::Length(1),
|
||||
//! Constraint::Length(1),
|
||||
//! Constraint::Length(1),
|
||||
//! Constraint::Length(1),
|
||||
//! Constraint::Min(0),
|
||||
//! ])
|
||||
//! .split(frame.size());
|
||||
//! ],
|
||||
//! )
|
||||
//! .split(frame.size());
|
||||
//!
|
||||
//! let span1 = Span::raw("Hello ");
|
||||
//! let span2 = Span::styled(
|
||||
@@ -282,21 +287,20 @@
|
||||
doc = "[`calendar`]: widgets::calendar::Monthly"
|
||||
)]
|
||||
//!
|
||||
//! [Ratatui Book]: https://ratatui.rs
|
||||
//! [Installation]: https://ratatui.rs/installation.html
|
||||
//! [Rendering]: https://ratatui.rs/concepts/rendering/index.html
|
||||
//! [Application Patterns]: https://ratatui.rs/concepts/application_patterns/index.html
|
||||
//! [Hello World tutorial]: https://ratatui.rs/tutorial/hello_world.html
|
||||
//! [Backends]: https://ratatui.rs/concepts/backends/index.html
|
||||
//! [Widgets]: https://ratatui.rs/how-to/widgets/index.html
|
||||
//! [Handling Events]: https://ratatui.rs/concepts/event_handling.html
|
||||
//! [Layout]: https://ratatui.rs/how-to/layout/index.html
|
||||
//! [Styling Text]: https://ratatui.rs/how-to/render/style-text.html
|
||||
//! [rust-tui-template]: https://github.com/ratatui-org/rust-tui-template
|
||||
//! [ratatui-async-template]: https://ratatui-org.github.io/ratatui-async-template/
|
||||
//! [simple-tui-rs]: https://github.com/pmsanford/simple-tui-rs
|
||||
//! [Ratatui Website]: https://ratatui.rs/
|
||||
//! [Installation]: https://ratatui.rs/installation/
|
||||
//! [Rendering]: https://ratatui.rs/concepts/rendering/
|
||||
//! [Application Patterns]: https://ratatui.rs/concepts/application-patterns/
|
||||
//! [Hello World tutorial]: https://ratatui.rs/tutorials/hello-world/
|
||||
//! [Backends]: https://ratatui.rs/concepts/backends/
|
||||
//! [Widgets]: https://ratatui.rs/how-to/widgets/
|
||||
//! [Handling Events]: https://ratatui.rs/concepts/event-handling/
|
||||
//! [Layout]: https://ratatui.rs/how-to/layout/
|
||||
//! [Styling Text]: https://ratatui.rs/how-to/render/style-text/
|
||||
//! [template]: https://github.com/ratatui-org/template
|
||||
//! [async-template]: https://ratatui-org.github.io/async-template
|
||||
//! [Examples]: https://github.com/ratatui-org/ratatui/tree/main/examples
|
||||
//! [git-cliff]: https://github.com/orhun/git-cliff
|
||||
//! [git-cliff]: https://git-cliff.org
|
||||
//! [Conventional Commits]: https://www.conventionalcommits.org
|
||||
//! [API Documentation]: https://docs.rs/ratatui
|
||||
//! [Changelog]: https://github.com/ratatui-org/ratatui/blob/main/CHANGELOG.md
|
||||
@@ -322,7 +326,7 @@
|
||||
//! [Crossterm]: https://crates.io/crates/crossterm
|
||||
//! [Termion]: https://crates.io/crates/termion
|
||||
//! [Termwiz]: https://crates.io/crates/termwiz
|
||||
//! [Tui-rs crate]: https://crates.io/crates/tui
|
||||
//! [tui-rs]: https://crates.io/crates/tui
|
||||
//! [hello_world.rs]: https://github.com/ratatui-org/ratatui/blob/main/examples/hello_world.rs
|
||||
//! [Crate Badge]: https://img.shields.io/crates/v/ratatui?logo=rust&style=flat-square
|
||||
//! [CI Badge]:
|
||||
|
||||
301
src/style.rs
301
src/style.rs
@@ -16,9 +16,9 @@
|
||||
//! use ratatui::prelude::*;
|
||||
//!
|
||||
//! let heading_style = Style::new()
|
||||
//! .fg(Color::Black)
|
||||
//! .bg(Color::Green)
|
||||
//! .add_modifier(Modifier::ITALIC | Modifier::BOLD);
|
||||
//! .fg(Color::Black)
|
||||
//! .bg(Color::Green)
|
||||
//! .add_modifier(Modifier::ITALIC | Modifier::BOLD);
|
||||
//! let span = Span::styled("hello", heading_style);
|
||||
//! ```
|
||||
//!
|
||||
@@ -44,16 +44,24 @@
|
||||
//! use ratatui::{prelude::*, widgets::*};
|
||||
//!
|
||||
//! assert_eq!(
|
||||
//! "hello".red().on_blue().bold(),
|
||||
//! "hello".red().on_blue().bold(),
|
||||
//! Span::styled(
|
||||
//! "hello",
|
||||
//! Style::default().fg(Color::Red).bg(Color::Blue).add_modifier(Modifier::BOLD))
|
||||
//! Style::default()
|
||||
//! .fg(Color::Red)
|
||||
//! .bg(Color::Blue)
|
||||
//! .add_modifier(Modifier::BOLD)
|
||||
//! )
|
||||
//! );
|
||||
//!
|
||||
//! assert_eq!(
|
||||
//! Paragraph::new("hello").red().on_blue().bold(),
|
||||
//! Paragraph::new("hello")
|
||||
//! .style(Style::default().fg(Color::Red).bg(Color::Blue).add_modifier(Modifier::BOLD))
|
||||
//! Paragraph::new("hello").style(
|
||||
//! Style::default()
|
||||
//! .fg(Color::Red)
|
||||
//! .bg(Color::Blue)
|
||||
//! .add_modifier(Modifier::BOLD)
|
||||
//! )
|
||||
//! );
|
||||
//! ```
|
||||
//!
|
||||
@@ -74,6 +82,9 @@ bitflags! {
|
||||
///
|
||||
/// They are bitflags so they can easily be composed.
|
||||
///
|
||||
/// `From<Modifier> for Style` is implemented so you can use `Modifier` anywhere that accepts
|
||||
/// `Into<Style>`.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
@@ -113,7 +124,7 @@ impl fmt::Debug for Modifier {
|
||||
/// Style lets you control the main characteristics of the displayed elements.
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::{prelude::*};
|
||||
/// use ratatui::prelude::*;
|
||||
///
|
||||
/// Style::default()
|
||||
/// .fg(Color::Black)
|
||||
@@ -130,23 +141,43 @@ impl fmt::Debug for Modifier {
|
||||
///
|
||||
/// For more information about the style shorthands, see the [`Stylize`] trait.
|
||||
///
|
||||
/// We implement conversions from [`Color`] and [`Modifier`] to [`Style`] so you can use them
|
||||
/// anywhere that accepts `Into<Style>`.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// Line::styled("hello", Style::new().fg(Color::Red));
|
||||
/// // simplifies to
|
||||
/// Line::styled("hello", Color::Red);
|
||||
///
|
||||
/// Line::styled("hello", Style::new().add_modifier(Modifier::BOLD));
|
||||
/// // simplifies to
|
||||
/// Line::styled("hello", Modifier::BOLD);
|
||||
/// ```
|
||||
///
|
||||
/// Styles represents an incremental change. If you apply the styles S1, S2, S3 to a cell of the
|
||||
/// terminal buffer, the style of this cell will be the result of the merge of S1, S2 and S3, not
|
||||
/// just S3.
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::{prelude::*};
|
||||
/// use ratatui::prelude::*;
|
||||
///
|
||||
/// let styles = [
|
||||
/// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC),
|
||||
/// Style::default().bg(Color::Red).add_modifier(Modifier::UNDERLINED),
|
||||
/// Style::default()
|
||||
/// .fg(Color::Blue)
|
||||
/// .add_modifier(Modifier::BOLD | Modifier::ITALIC),
|
||||
/// Style::default()
|
||||
/// .bg(Color::Red)
|
||||
/// .add_modifier(Modifier::UNDERLINED),
|
||||
/// #[cfg(feature = "underline-color")]
|
||||
/// Style::default().underline_color(Color::Green),
|
||||
/// Style::default().fg(Color::Yellow).remove_modifier(Modifier::ITALIC),
|
||||
/// Style::default()
|
||||
/// .fg(Color::Yellow)
|
||||
/// .remove_modifier(Modifier::ITALIC),
|
||||
/// ];
|
||||
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
|
||||
/// for style in &styles {
|
||||
/// buffer.get_mut(0, 0).set_style(*style);
|
||||
/// buffer.get_mut(0, 0).set_style(*style);
|
||||
/// }
|
||||
/// assert_eq!(
|
||||
/// Style {
|
||||
@@ -165,15 +196,17 @@ impl fmt::Debug for Modifier {
|
||||
/// reset all properties until that point use [`Style::reset`].
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::{prelude::*};
|
||||
/// use ratatui::prelude::*;
|
||||
///
|
||||
/// let styles = [
|
||||
/// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC),
|
||||
/// Style::default()
|
||||
/// .fg(Color::Blue)
|
||||
/// .add_modifier(Modifier::BOLD | Modifier::ITALIC),
|
||||
/// Style::reset().fg(Color::Yellow),
|
||||
/// ];
|
||||
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
|
||||
/// for style in &styles {
|
||||
/// buffer.get_mut(0, 0).set_style(*style);
|
||||
/// buffer.get_mut(0, 0).set_style(*style);
|
||||
/// }
|
||||
/// assert_eq!(
|
||||
/// Style {
|
||||
@@ -211,10 +244,11 @@ impl Styled for Style {
|
||||
*self
|
||||
}
|
||||
|
||||
fn set_style(self, style: Style) -> Self::Item {
|
||||
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
|
||||
self.patch(style)
|
||||
}
|
||||
}
|
||||
|
||||
impl Style {
|
||||
pub const fn new() -> Style {
|
||||
Style {
|
||||
@@ -249,6 +283,7 @@ impl Style {
|
||||
/// let diff = Style::default().fg(Color::Red);
|
||||
/// assert_eq!(style.patch(diff), Style::default().fg(Color::Red));
|
||||
/// ```
|
||||
#[must_use = "`fg` returns the modified style without modifying the original"]
|
||||
pub const fn fg(mut self, color: Color) -> Style {
|
||||
self.fg = Some(color);
|
||||
self
|
||||
@@ -264,6 +299,7 @@ impl Style {
|
||||
/// let diff = Style::default().bg(Color::Red);
|
||||
/// assert_eq!(style.patch(diff), Style::default().bg(Color::Red));
|
||||
/// ```
|
||||
#[must_use = "`bg` returns the modified style without modifying the original"]
|
||||
pub const fn bg(mut self, color: Color) -> Style {
|
||||
self.bg = Some(color);
|
||||
self
|
||||
@@ -283,11 +319,21 @@ impl Style {
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let style = Style::default().underline_color(Color::Blue).add_modifier(Modifier::UNDERLINED);
|
||||
/// let diff = Style::default().underline_color(Color::Red).add_modifier(Modifier::UNDERLINED);
|
||||
/// assert_eq!(style.patch(diff), Style::default().underline_color(Color::Red).add_modifier(Modifier::UNDERLINED));
|
||||
/// let style = Style::default()
|
||||
/// .underline_color(Color::Blue)
|
||||
/// .add_modifier(Modifier::UNDERLINED);
|
||||
/// let diff = Style::default()
|
||||
/// .underline_color(Color::Red)
|
||||
/// .add_modifier(Modifier::UNDERLINED);
|
||||
/// assert_eq!(
|
||||
/// style.patch(diff),
|
||||
/// Style::default()
|
||||
/// .underline_color(Color::Red)
|
||||
/// .add_modifier(Modifier::UNDERLINED)
|
||||
/// );
|
||||
/// ```
|
||||
#[cfg(feature = "underline-color")]
|
||||
#[must_use = "`underline_color` returns the modified style without modifying the original"]
|
||||
pub const fn underline_color(mut self, color: Color) -> Style {
|
||||
self.underline_color = Some(color);
|
||||
self
|
||||
@@ -307,6 +353,7 @@ impl Style {
|
||||
/// assert_eq!(patched.add_modifier, Modifier::BOLD | Modifier::ITALIC);
|
||||
/// assert_eq!(patched.sub_modifier, Modifier::empty());
|
||||
/// ```
|
||||
#[must_use = "`add_modifier` returns the modified style without modifying the original"]
|
||||
pub const fn add_modifier(mut self, modifier: Modifier) -> Style {
|
||||
self.sub_modifier = self.sub_modifier.difference(modifier);
|
||||
self.add_modifier = self.add_modifier.union(modifier);
|
||||
@@ -327,6 +374,7 @@ impl Style {
|
||||
/// assert_eq!(patched.add_modifier, Modifier::BOLD);
|
||||
/// assert_eq!(patched.sub_modifier, Modifier::ITALIC);
|
||||
/// ```
|
||||
#[must_use = "`remove_modifier` returns the modified style without modifying the original"]
|
||||
pub const fn remove_modifier(mut self, modifier: Modifier) -> Style {
|
||||
self.add_modifier = self.add_modifier.difference(modifier);
|
||||
self.sub_modifier = self.sub_modifier.union(modifier);
|
||||
@@ -336,6 +384,9 @@ impl Style {
|
||||
/// Results in a combined style that is equivalent to applying the two individual styles to
|
||||
/// a style one after the other.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```
|
||||
/// # use ratatui::prelude::*;
|
||||
@@ -344,9 +395,12 @@ impl Style {
|
||||
/// let combined = style_1.patch(style_2);
|
||||
/// assert_eq!(
|
||||
/// Style::default().patch(style_1).patch(style_2),
|
||||
/// Style::default().patch(combined));
|
||||
/// Style::default().patch(combined)
|
||||
/// );
|
||||
/// ```
|
||||
pub fn patch(mut self, other: Style) -> Style {
|
||||
#[must_use = "`patch` returns the modified style without modifying the original"]
|
||||
pub fn patch<S: Into<Style>>(mut self, other: S) -> Style {
|
||||
let other = other.into();
|
||||
self.fg = other.fg.or(self.fg);
|
||||
self.bg = other.bg.or(self.bg);
|
||||
|
||||
@@ -364,6 +418,134 @@ impl Style {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color> for Style {
|
||||
/// Creates a new `Style` with the given foreground color.
|
||||
///
|
||||
/// To specify a foreground and background color, use the `from((fg, bg))` constructor.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let style = Style::from(Color::Red);
|
||||
/// ```
|
||||
fn from(color: Color) -> Self {
|
||||
Self::new().fg(color)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(Color, Color)> for Style {
|
||||
/// Creates a new `Style` with the given foreground and background colors.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// // red foreground, blue background
|
||||
/// let style = Style::from((Color::Red, Color::Blue));
|
||||
/// // default foreground, blue background
|
||||
/// let style = Style::from((Color::Reset, Color::Blue));
|
||||
/// ```
|
||||
fn from((fg, bg): (Color, Color)) -> Self {
|
||||
Self::new().fg(fg).bg(bg)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Modifier> for Style {
|
||||
/// Creates a new `Style` with the given modifier added.
|
||||
///
|
||||
/// To specify multiple modifiers, use the `|` operator.
|
||||
///
|
||||
/// To specify modifiers to add and remove, use the `from((add_modifier, sub_modifier))`
|
||||
/// constructor.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// // add bold and italic
|
||||
/// let style = Style::from(Modifier::BOLD|Modifier::ITALIC);
|
||||
fn from(modifier: Modifier) -> Self {
|
||||
Self::new().add_modifier(modifier)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(Modifier, Modifier)> for Style {
|
||||
/// Creates a new `Style` with the given modifiers added and removed.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// // add bold and italic, remove dim
|
||||
/// let style = Style::from((Modifier::BOLD | Modifier::ITALIC, Modifier::DIM));
|
||||
/// ```
|
||||
fn from((add_modifier, sub_modifier): (Modifier, Modifier)) -> Self {
|
||||
Self::new()
|
||||
.add_modifier(add_modifier)
|
||||
.remove_modifier(sub_modifier)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(Color, Modifier)> for Style {
|
||||
/// Creates a new `Style` with the given foreground color and modifier added.
|
||||
///
|
||||
/// To specify multiple modifiers, use the `|` operator.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// // red foreground, add bold and italic
|
||||
/// let style = Style::from((Color::Red, Modifier::BOLD | Modifier::ITALIC));
|
||||
/// ```
|
||||
fn from((fg, modifier): (Color, Modifier)) -> Self {
|
||||
Self::new().fg(fg).add_modifier(modifier)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(Color, Color, Modifier)> for Style {
|
||||
/// Creates a new `Style` with the given foreground and background colors and modifier added.
|
||||
///
|
||||
/// To specify multiple modifiers, use the `|` operator.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// // red foreground, blue background, add bold and italic
|
||||
/// let style = Style::from((Color::Red, Color::Blue, Modifier::BOLD | Modifier::ITALIC));
|
||||
/// ```
|
||||
fn from((fg, bg, modifier): (Color, Color, Modifier)) -> Self {
|
||||
Self::new().fg(fg).bg(bg).add_modifier(modifier)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(Color, Color, Modifier, Modifier)> for Style {
|
||||
/// Creates a new `Style` with the given foreground and background colors and modifiers added
|
||||
/// and removed.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// // red foreground, blue background, add bold and italic, remove dim
|
||||
/// let style = Style::from((
|
||||
/// Color::Red,
|
||||
/// Color::Blue,
|
||||
/// Modifier::BOLD | Modifier::ITALIC,
|
||||
/// Modifier::DIM,
|
||||
/// ));
|
||||
/// ```
|
||||
fn from((fg, bg, add_modifier, sub_modifier): (Color, Color, Modifier, Modifier)) -> Self {
|
||||
Self::new()
|
||||
.fg(fg)
|
||||
.bg(bg)
|
||||
.add_modifier(add_modifier)
|
||||
.remove_modifier(sub_modifier)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -625,4 +807,79 @@ mod tests {
|
||||
// reset
|
||||
assert_eq!(Style::new().reset(), Style::reset());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_color() {
|
||||
assert_eq!(Style::from(Color::Red), Style::new().fg(Color::Red));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_color_color() {
|
||||
assert_eq!(
|
||||
Style::from((Color::Red, Color::Blue)),
|
||||
Style::new().fg(Color::Red).bg(Color::Blue)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_modifier() {
|
||||
assert_eq!(
|
||||
Style::from(Modifier::BOLD | Modifier::ITALIC),
|
||||
Style::new()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.add_modifier(Modifier::ITALIC)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_modifier_modifier() {
|
||||
assert_eq!(
|
||||
Style::from((Modifier::BOLD | Modifier::ITALIC, Modifier::DIM)),
|
||||
Style::new()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.add_modifier(Modifier::ITALIC)
|
||||
.remove_modifier(Modifier::DIM)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_color_modifier() {
|
||||
assert_eq!(
|
||||
Style::from((Color::Red, Modifier::BOLD | Modifier::ITALIC)),
|
||||
Style::new()
|
||||
.fg(Color::Red)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.add_modifier(Modifier::ITALIC)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_color_color_modifier() {
|
||||
assert_eq!(
|
||||
Style::from((Color::Red, Color::Blue, Modifier::BOLD | Modifier::ITALIC)),
|
||||
Style::new()
|
||||
.fg(Color::Red)
|
||||
.bg(Color::Blue)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.add_modifier(Modifier::ITALIC)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_color_color_modifier_modifier() {
|
||||
assert_eq!(
|
||||
Style::from((
|
||||
Color::Red,
|
||||
Color::Blue,
|
||||
Modifier::BOLD | Modifier::ITALIC,
|
||||
Modifier::DIM
|
||||
)),
|
||||
Style::new()
|
||||
.fg(Color::Red)
|
||||
.bg(Color::Blue)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.add_modifier(Modifier::ITALIC)
|
||||
.remove_modifier(Modifier::DIM)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,10 +35,14 @@ use std::{
|
||||
/// - we support `-` and `_` and ` ` as separators for all colors
|
||||
/// - we support both `gray` and `grey` spellings
|
||||
///
|
||||
/// `From<Color> for Style` is implemented by creating a style with the foreground color set to the
|
||||
/// given color. This allows you to use colors anywhere that accepts `Into<Style>`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use std::str::FromStr;
|
||||
///
|
||||
/// use ratatui::prelude::*;
|
||||
///
|
||||
/// assert_eq!(Color::from_str("red"), Ok(Color::Red));
|
||||
@@ -60,7 +64,7 @@ use std::{
|
||||
///
|
||||
/// [ANSI color table]: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
|
||||
pub enum Color {
|
||||
/// Resets the foreground or background color
|
||||
#[default]
|
||||
@@ -123,6 +127,17 @@ pub enum Color {
|
||||
Indexed(u8),
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl<'de> serde::Deserialize<'de> for Color {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
FromStr::from_str(&s).map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
/// Error type indicating a failure to parse a color string.
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub struct ParseColorError;
|
||||
@@ -147,6 +162,7 @@ impl std::error::Error for ParseColorError {}
|
||||
///
|
||||
/// ```
|
||||
/// use std::str::FromStr;
|
||||
///
|
||||
/// use ratatui::prelude::*;
|
||||
///
|
||||
/// let color: Color = Color::from_str("blue").unwrap();
|
||||
@@ -249,6 +265,9 @@ impl Display for Color {
|
||||
mod tests {
|
||||
use std::error::Error;
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::de::{Deserialize, IntoDeserializer};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
@@ -359,4 +378,46 @@ mod tests {
|
||||
assert_eq!(format!("{}", Color::Rgb(255, 0, 0)), "#FF0000");
|
||||
assert_eq!(format!("{}", Color::Reset), "Reset");
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
#[test]
|
||||
fn deserialize() -> Result<(), serde::de::value::Error> {
|
||||
assert_eq!(
|
||||
Color::Black,
|
||||
Color::deserialize("Black".into_deserializer())?
|
||||
);
|
||||
assert_eq!(
|
||||
Color::Magenta,
|
||||
Color::deserialize("magenta".into_deserializer())?
|
||||
);
|
||||
assert_eq!(
|
||||
Color::LightGreen,
|
||||
Color::deserialize("LightGreen".into_deserializer())?
|
||||
);
|
||||
assert_eq!(
|
||||
Color::White,
|
||||
Color::deserialize("bright-white".into_deserializer())?
|
||||
);
|
||||
assert_eq!(
|
||||
Color::Indexed(42),
|
||||
Color::deserialize("42".into_deserializer())?
|
||||
);
|
||||
assert_eq!(
|
||||
Color::Rgb(0, 255, 0),
|
||||
Color::deserialize("#00ff00".into_deserializer())?
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
#[test]
|
||||
fn deserialize_error() {
|
||||
let color: Result<_, serde::de::value::Error> =
|
||||
Color::deserialize("invalid".into_deserializer());
|
||||
assert!(color.is_err());
|
||||
|
||||
let color: Result<_, serde::de::value::Error> =
|
||||
Color::deserialize("#00000000".into_deserializer());
|
||||
assert!(color.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,14 @@ use crate::{
|
||||
pub trait Styled {
|
||||
type Item;
|
||||
|
||||
/// Returns the style of the object.
|
||||
fn style(&self) -> Style;
|
||||
fn set_style(self, style: Style) -> Self::Item;
|
||||
|
||||
/// Sets the style of the object.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item;
|
||||
}
|
||||
|
||||
/// Generates two methods for each color, one for setting the foreground color (`red()`, `blue()`,
|
||||
@@ -40,11 +46,13 @@ macro_rules! color {
|
||||
( $color:ident ) => {
|
||||
paste! {
|
||||
#[doc = "Sets the foreground color to [`" $color "`](Color::" $color:camel ")."]
|
||||
#[must_use = concat!("`", stringify!($color), "` returns the modified style without modifying the original")]
|
||||
fn $color(self) -> T {
|
||||
self.fg(Color::[<$color:camel>])
|
||||
}
|
||||
|
||||
#[doc = "Sets the background color to [`" $color "`](Color::" $color:camel ")."]
|
||||
#[must_use = concat!("`on_", stringify!($color), "` returns the modified style without modifying the original")]
|
||||
fn [<on_ $color>](self) -> T {
|
||||
self.bg(Color::[<$color:camel>])
|
||||
}
|
||||
@@ -76,6 +84,7 @@ macro_rules! modifier {
|
||||
( $modifier:ident ) => {
|
||||
paste! {
|
||||
#[doc = "Adds the [`" $modifier:upper "`](Modifier::" $modifier:upper ") modifier."]
|
||||
#[must_use = concat!("`", stringify!($modifier), "` returns the modified style without modifying the original")]
|
||||
fn [<$modifier>](self) -> T {
|
||||
self.add_modifier(Modifier::[<$modifier:upper>])
|
||||
}
|
||||
@@ -83,6 +92,7 @@ macro_rules! modifier {
|
||||
|
||||
paste! {
|
||||
#[doc = "Removes the [`" $modifier:upper "`](Modifier::" $modifier:upper ") modifier."]
|
||||
#[must_use = concat!("`not_", stringify!($modifier), "` returns the modified style without modifying the original")]
|
||||
fn [<not_ $modifier>](self) -> T {
|
||||
self.remove_modifier(Modifier::[<$modifier:upper>])
|
||||
}
|
||||
@@ -123,13 +133,22 @@ macro_rules! modifier {
|
||||
/// "world".green().on_yellow().not_bold(),
|
||||
/// ]);
|
||||
/// let paragraph = Paragraph::new(line).italic().underlined();
|
||||
/// let block = Block::default().title("Title").borders(Borders::ALL).on_white().bold();
|
||||
/// let block = Block::default()
|
||||
/// .title("Title")
|
||||
/// .borders(Borders::ALL)
|
||||
/// .on_white()
|
||||
/// .bold();
|
||||
/// ```
|
||||
pub trait Stylize<'a, T>: Sized {
|
||||
#[must_use = "`bg` returns the modified style without modifying the original"]
|
||||
fn bg(self, color: Color) -> T;
|
||||
#[must_use = "`fg` returns the modified style without modifying the original"]
|
||||
fn fg<S: Into<Color>>(self, color: S) -> T;
|
||||
#[must_use = "`reset` returns the modified style without modifying the original"]
|
||||
fn reset(self) -> T;
|
||||
#[must_use = "`add_modifier` returns the modified style without modifying the original"]
|
||||
fn add_modifier(self, modifier: Modifier) -> T;
|
||||
#[must_use = "`remove_modifier` returns the modified style without modifying the original"]
|
||||
fn remove_modifier(self, modifier: Modifier) -> T;
|
||||
|
||||
color!(black);
|
||||
@@ -196,7 +215,7 @@ impl<'a> Styled for &'a str {
|
||||
Style::default()
|
||||
}
|
||||
|
||||
fn set_style(self, style: Style) -> Self::Item {
|
||||
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
|
||||
Span::styled(self, style)
|
||||
}
|
||||
}
|
||||
@@ -208,7 +227,7 @@ impl Styled for String {
|
||||
Style::default()
|
||||
}
|
||||
|
||||
fn set_style(self, style: Style) -> Self::Item {
|
||||
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
|
||||
Span::styled(self, style)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -398,12 +398,12 @@ pub mod braille {
|
||||
/// Marker to use when plotting data points
|
||||
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub enum Marker {
|
||||
/// One point per cell in shape of dot ("•")
|
||||
/// One point per cell in shape of dot (`•`)
|
||||
#[default]
|
||||
Dot,
|
||||
/// One point per cell in shape of a block ("█")
|
||||
/// One point per cell in shape of a block (`█`)
|
||||
Block,
|
||||
/// One point per cell in the shape of a bar ("▄")
|
||||
/// One point per cell in the shape of a bar (`▄`)
|
||||
Bar,
|
||||
/// Use the [Unicode Braille Patterns](https://en.wikipedia.org/wiki/Braille_Patterns) block to
|
||||
/// represent data points.
|
||||
@@ -412,9 +412,9 @@ pub enum Marker {
|
||||
///
|
||||
/// Note: Support for this marker is limited to terminals and fonts that support Unicode
|
||||
/// Braille Patterns. If your terminal does not support this, you will see unicode replacement
|
||||
/// characters (<EFBFBD>) instead of Braille dots.
|
||||
/// characters (`<60>`) instead of Braille dots (`⠓`, `⣇`, `⣿`).
|
||||
Braille,
|
||||
/// Use the unicode block and half block characters ("█", "▄", and "▀") to represent points in
|
||||
/// Use the unicode block and half block characters (`█`, `▄`, and `▀`) to represent points in
|
||||
/// a grid that is double the resolution of the terminal. Because each terminal cell is
|
||||
/// generally about twice as tall as it is wide, this allows for a square grid of pixels.
|
||||
HalfBlock,
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use std::io::stdout;
|
||||
//!
|
||||
//! use ratatui::{prelude::*, widgets::Paragraph};
|
||||
//!
|
||||
//! let backend = CrosstermBackend::new(stdout());
|
||||
@@ -108,6 +109,7 @@ pub struct TerminalOptions {
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use std::io::stdout;
|
||||
///
|
||||
/// use ratatui::{prelude::*, widgets::Paragraph};
|
||||
///
|
||||
/// let backend = CrosstermBackend::new(stdout());
|
||||
@@ -198,10 +200,7 @@ where
|
||||
/// # use ratatui::{prelude::*, backend::TestBackend};
|
||||
/// let backend = CrosstermBackend::new(stdout());
|
||||
/// let viewport = Viewport::Fixed(Rect::new(0, 0, 10, 10));
|
||||
/// let terminal = Terminal::with_options(
|
||||
/// backend,
|
||||
/// TerminalOptions { viewport },
|
||||
/// )?;
|
||||
/// 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>> {
|
||||
@@ -459,8 +458,9 @@ where
|
||||
/// Paragraph::new(Line::from(vec![
|
||||
/// Span::raw("This line will be added "),
|
||||
/// Span::styled("before", Style::default().fg(Color::Blue)),
|
||||
/// Span::raw(" the current viewport")
|
||||
/// ])).render(buf.area, buf);
|
||||
/// Span::raw(" the current viewport"),
|
||||
/// ]))
|
||||
/// .render(buf.area, buf);
|
||||
/// });
|
||||
/// ```
|
||||
pub fn insert_before<F>(&mut self, height: u16, draw_fn: F) -> io::Result<()>
|
||||
@@ -471,38 +471,50 @@ where
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Clear the viewport off the screen
|
||||
self.clear()?;
|
||||
let height = height.min(self.last_known_size.height);
|
||||
self.backend.append_lines(height)?;
|
||||
let missing_lines =
|
||||
height.saturating_sub(self.last_known_size.bottom() - self.viewport_area.top());
|
||||
|
||||
// Move the viewport by height, but don't move it past the bottom of the terminal
|
||||
let viewport_at_bottom = self.last_known_size.bottom() - self.viewport_area.height;
|
||||
self.set_viewport_area(Rect {
|
||||
y: self
|
||||
.viewport_area
|
||||
.y
|
||||
.saturating_add(height)
|
||||
.min(viewport_at_bottom),
|
||||
..self.viewport_area
|
||||
});
|
||||
|
||||
// Draw contents into buffer
|
||||
let area = Rect {
|
||||
x: self.viewport_area.left(),
|
||||
y: self.viewport_area.top().saturating_sub(missing_lines),
|
||||
y: 0,
|
||||
width: self.viewport_area.width,
|
||||
height,
|
||||
};
|
||||
let mut buffer = Buffer::empty(area);
|
||||
|
||||
draw_fn(&mut buffer);
|
||||
|
||||
let iter = buffer.content.iter().enumerate().map(|(i, c)| {
|
||||
let (x, y) = buffer.pos_of(i);
|
||||
(x, y, c)
|
||||
});
|
||||
self.backend.draw(iter)?;
|
||||
self.backend.flush()?;
|
||||
// Split buffer into screen-sized chunks and draw
|
||||
let max_chunk_size = (self.viewport_area.top() * area.width).into();
|
||||
for buffer_content_chunk in buffer.content.chunks(max_chunk_size) {
|
||||
let chunk_size = buffer_content_chunk.len() as u16 / area.width;
|
||||
|
||||
let remaining_lines = self.last_known_size.height - area.bottom();
|
||||
let missing_lines = self.viewport_area.height.saturating_sub(remaining_lines);
|
||||
self.backend.append_lines(self.viewport_area.height)?;
|
||||
self.backend
|
||||
.append_lines(self.viewport_area.height.saturating_sub(1) + chunk_size)?;
|
||||
|
||||
self.set_viewport_area(Rect {
|
||||
x: area.left(),
|
||||
y: area.bottom().saturating_sub(missing_lines),
|
||||
width: area.width,
|
||||
height: self.viewport_area.height,
|
||||
});
|
||||
let iter = buffer_content_chunk.iter().enumerate().map(|(i, c)| {
|
||||
let (x, y) = buffer.pos_of(i);
|
||||
(
|
||||
x,
|
||||
self.viewport_area.top().saturating_sub(chunk_size) + y,
|
||||
c,
|
||||
)
|
||||
});
|
||||
self.backend.draw(iter)?;
|
||||
self.backend.flush()?;
|
||||
self.set_cursor(self.viewport_area.left(), self.viewport_area.top())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -570,7 +582,11 @@ pub struct Frame<'a> {
|
||||
impl Frame<'_> {
|
||||
/// The size of the current frame
|
||||
///
|
||||
/// This is guaranteed not to change when rendering.
|
||||
/// This is guaranteed not to change during rendering, so may be called multiple times.
|
||||
///
|
||||
/// If your app listens for a resize event from the backend, it should ignore the values from
|
||||
/// the event for any calculations that are used to render the current frame and use this value
|
||||
/// instead as this is the size of the buffer that is used to render the current frame.
|
||||
pub fn size(&self) -> Rect {
|
||||
self.viewport_area
|
||||
}
|
||||
@@ -616,10 +632,7 @@ impl Frame<'_> {
|
||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||
/// # let mut frame = terminal.get_frame();
|
||||
/// let mut state = ListState::default().with_selected(Some(1));
|
||||
/// let list = List::new(vec![
|
||||
/// ListItem::new("Item 1"),
|
||||
/// ListItem::new("Item 2"),
|
||||
/// ]);
|
||||
/// let list = List::new(vec![ListItem::new("Item 1"), ListItem::new("Item 2")]);
|
||||
/// let area = Rect::new(0, 0, 5, 5);
|
||||
/// frame.render_stateful_widget(list, area, &mut state);
|
||||
/// ```
|
||||
|
||||
@@ -31,9 +31,8 @@
|
||||
//! // Converted to Line(vec![
|
||||
//! // Span { content: Cow::Borrowed("My title"), style: Style { fg: Some(Color::Yellow), .. }
|
||||
//! // ])
|
||||
//! let block = Block::default().title(
|
||||
//! Span::styled("My title", Style::default().fg(Color::Yellow))
|
||||
//! );
|
||||
//! let block =
|
||||
//! Block::default().title(Span::styled("My title", Style::default().fg(Color::Yellow)));
|
||||
//!
|
||||
//! // A string with multiple styles.
|
||||
//! // Converted to Line(vec![
|
||||
@@ -46,8 +45,6 @@
|
||||
//! ]);
|
||||
//! ```
|
||||
|
||||
use crate::style::Style;
|
||||
|
||||
mod grapheme;
|
||||
pub use grapheme::StyledGrapheme;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::style::{Style, Styled};
|
||||
use crate::prelude::*;
|
||||
|
||||
/// A grapheme associated to a style.
|
||||
/// Note that, although `StyledGrapheme` is the smallest divisible unit of text,
|
||||
@@ -12,8 +12,15 @@ pub struct StyledGrapheme<'a> {
|
||||
}
|
||||
|
||||
impl<'a> StyledGrapheme<'a> {
|
||||
pub fn new(symbol: &'a str, style: Style) -> StyledGrapheme<'a> {
|
||||
StyledGrapheme { symbol, style }
|
||||
/// Creates a new `StyledGrapheme` with the given symbol and style.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
pub fn new<S: Into<Style>>(symbol: &'a str, style: S) -> StyledGrapheme<'a> {
|
||||
StyledGrapheme {
|
||||
symbol,
|
||||
style: style.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,8 +31,8 @@ impl<'a> Styled for StyledGrapheme<'a> {
|
||||
self.style
|
||||
}
|
||||
|
||||
fn set_style(mut self, style: Style) -> Self::Item {
|
||||
self.style = style;
|
||||
fn set_style<S: Into<Style>>(mut self, style: S) -> Self::Item {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -33,7 +40,6 @@ impl<'a> Styled for StyledGrapheme<'a> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::prelude::*;
|
||||
|
||||
#[test]
|
||||
fn new() {
|
||||
|
||||
469
src/text/line.rs
469
src/text/line.rs
@@ -1,23 +1,106 @@
|
||||
#![deny(missing_docs)]
|
||||
use std::borrow::Cow;
|
||||
|
||||
use super::{Span, Style, StyledGrapheme};
|
||||
use crate::layout::Alignment;
|
||||
use super::StyledGrapheme;
|
||||
use crate::{prelude::*, widgets::Widget};
|
||||
|
||||
/// A line of text, consisting of one or more [`Span`]s.
|
||||
///
|
||||
/// [`Line`]s are used wherever text is displayed in the terminal and represent a single line of
|
||||
/// text. When a [`Line`] is rendered, it is rendered as a single line of text, with each [`Span`]
|
||||
/// being rendered in order (left to right).
|
||||
///
|
||||
/// [`Line`]s can be created from [`Span`]s, [`String`]s, and [`&str`]s. They can be styled with a
|
||||
/// [`Style`], and have an [`Alignment`].
|
||||
///
|
||||
/// The line's [`Alignment`] is used by the rendering widget to determine how to align the line
|
||||
/// within the available space. If the line is longer than the available space, the alignment is
|
||||
/// ignored and the line is truncated.
|
||||
///
|
||||
/// The line's [`Style`] is used by the rendering widget to determine how to style the line. If the
|
||||
/// line is longer than the available space, the style is applied to the entire line, and the line
|
||||
/// is truncated. Each [`Span`] in the line will be styled with the [`Style`] of the line, and then
|
||||
/// with its own [`Style`].
|
||||
///
|
||||
/// `Line` implements the [`Widget`] trait, which means it can be rendered to a [`Buffer`]. Usually
|
||||
/// apps will use the [`Paragraph`] widget instead of rendering a [`Line`] directly as it provides
|
||||
/// more functionality.
|
||||
///
|
||||
/// # Constructor Methods
|
||||
///
|
||||
/// - [`Line::default`] creates a line with empty content and the default style.
|
||||
/// - [`Line::raw`] creates a line with the given content and the default style.
|
||||
/// - [`Line::styled`] creates a line with the given content and style.
|
||||
///
|
||||
/// # Setter Methods
|
||||
///
|
||||
/// These methods are fluent setters. They return a `Line` with the property set.
|
||||
///
|
||||
/// - [`Line::spans`] sets the content of the line.
|
||||
/// - [`Line::style`] sets the style of the line.
|
||||
/// - [`Line::alignment`] sets the alignment of the line.
|
||||
///
|
||||
/// # Other Methods
|
||||
///
|
||||
/// - [`Line::patch_style`] patches the style of the line, adding modifiers from the given style.
|
||||
/// - [`Line::reset_style`] resets the style of the line.
|
||||
/// - [`Line::width`] returns the unicode width of the content held by this line.
|
||||
/// - [`Line::styled_graphemes`] returns an iterator over the graphemes held by this line.
|
||||
///
|
||||
/// # Compatibility Notes
|
||||
///
|
||||
/// Before v0.26.0, [`Line`] did not have a `style` field and instead relied on only the styles that
|
||||
/// were set on each [`Span`] contained in the `spans` field. The [`Line::patch_style`] method was
|
||||
/// the only way to set the overall style for individual lines. For this reason, this field may not
|
||||
/// be supported yet by all widgets (outside of the `ratatui` crate itself).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::prelude::*;
|
||||
///
|
||||
/// Line::raw("unstyled");
|
||||
/// Line::styled("yellow text", Style::new().yellow());
|
||||
/// Line::from("red text").style(Style::new().red());
|
||||
/// Line::from(String::from("unstyled"));
|
||||
/// Line::from(vec![
|
||||
/// Span::styled("Hello", Style::new().blue()),
|
||||
/// Span::raw(" world!"),
|
||||
/// ]);
|
||||
/// ```
|
||||
///
|
||||
/// [`Paragraph`]: crate::widgets::Paragraph
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Line<'a> {
|
||||
/// The spans that make up this line of text.
|
||||
pub spans: Vec<Span<'a>>,
|
||||
|
||||
/// The style of this line of text.
|
||||
pub style: Style,
|
||||
|
||||
/// The alignment of this line of text.
|
||||
pub alignment: Option<Alignment>,
|
||||
}
|
||||
|
||||
impl<'a> Line<'a> {
|
||||
/// Create a line with the default style.
|
||||
///
|
||||
/// `content` can be any type that is convertible to [`Cow<str>`] (e.g. [`&str`], [`String`],
|
||||
/// [`Cow<str>`], or your own type that implements [`Into<Cow<str>>`]).
|
||||
///
|
||||
/// A [`Line`] can specify a [`Style`], which will be applied before the style of each [`Span`]
|
||||
/// in the line.
|
||||
///
|
||||
/// Any newlines in the content are removed.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// # use std::borrow::Cow;
|
||||
/// Line::raw("test content");
|
||||
/// Line::raw(String::from("test content"));
|
||||
/// Line::raw(Cow::from("test content"));
|
||||
/// ```
|
||||
pub fn raw<T>(content: T) -> Line<'a>
|
||||
where
|
||||
@@ -29,38 +112,120 @@ impl<'a> Line<'a> {
|
||||
.lines()
|
||||
.map(|v| Span::raw(v.to_string()))
|
||||
.collect(),
|
||||
alignment: None,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a line with a style.
|
||||
/// Create a line with the given style.
|
||||
// `content` can be any type that is convertible to [`Cow<str>`] (e.g. [`&str`], [`String`],
|
||||
/// [`Cow<str>`], or your own type that implements [`Into<Cow<str>>`]).
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Any newlines in the content are removed.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// # use std::borrow::Cow;
|
||||
/// let style = Style::new().yellow().italic();
|
||||
/// Line::styled("My text", style);
|
||||
/// Line::styled(String::from("My text"), style);
|
||||
/// Line::styled(Cow::from("test content"), style);
|
||||
/// ```
|
||||
pub fn styled<T, S>(content: T, style: S) -> Line<'a>
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
S: Into<Style>,
|
||||
{
|
||||
Line {
|
||||
spans: content
|
||||
.into()
|
||||
.lines()
|
||||
.map(|v| Span::raw(v.to_string()))
|
||||
.collect(),
|
||||
style: style.into(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the spans of this line of text.
|
||||
///
|
||||
/// `spans` accepts any iterator that yields items that are convertible to [`Span`] (e.g.
|
||||
/// [`&str`], [`String`], [`Span`], or your own type that implements [`Into<Span>`]).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
/// Line::styled("My text", style);
|
||||
/// Line::styled(String::from("My text"), style);
|
||||
/// let line = Line::default().spans(vec!["Hello".blue(), " world!".green()]);
|
||||
/// let line = Line::default().spans([1, 2, 3].iter().map(|i| format!("Item {}", i)));
|
||||
/// ```
|
||||
pub fn styled<T>(content: T, style: Style) -> Line<'a>
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn spans<I>(mut self, spans: I) -> Self
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
I: IntoIterator,
|
||||
I::Item: Into<Span<'a>>,
|
||||
{
|
||||
Line::from(Span::styled(content, style))
|
||||
self.spans = spans.into_iter().map(Into::into).collect();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of this line of text.
|
||||
///
|
||||
/// Defaults to [`Style::default()`].
|
||||
///
|
||||
/// Note: This field was added in v0.26.0. Prior to that, the style of a line was determined
|
||||
/// only by the style of each [`Span`] contained in the line. For this reason, this field may
|
||||
/// not be supported by all widgets (outside of the `ratatui` crate itself).
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let mut line = Line::from("foo").style(Style::new().red());
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the target alignment for this line of text.
|
||||
///
|
||||
/// Defaults to: [`None`], meaning the alignment is determined by the rendering widget.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let mut line = Line::from("Hi, what's up?");
|
||||
/// assert_eq!(None, line.alignment);
|
||||
/// assert_eq!(
|
||||
/// Some(Alignment::Right),
|
||||
/// line.alignment(Alignment::Right).alignment
|
||||
/// )
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn alignment(self, alignment: Alignment) -> Self {
|
||||
Self {
|
||||
alignment: Some(alignment),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the width of the underlying string.
|
||||
///
|
||||
/// ## Examples
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let line = Line::from(vec![
|
||||
/// Span::styled("My", Style::default().fg(Color::Yellow)),
|
||||
/// Span::raw(" text"),
|
||||
/// ]);
|
||||
/// assert_eq!(7, line.width());
|
||||
/// let line = Line::from(vec!["Hello".blue(), " world!".green()]);
|
||||
/// assert_eq!(12, line.width());
|
||||
/// ```
|
||||
pub fn width(&self) -> usize {
|
||||
self.spans.iter().map(Span::width).sum()
|
||||
@@ -71,16 +236,21 @@ impl<'a> Line<'a> {
|
||||
/// `base_style` is the [`Style`] that will be patched with each grapheme [`Style`] to get
|
||||
/// the resulting [`Style`].
|
||||
///
|
||||
/// ## Examples
|
||||
/// `base_style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`],
|
||||
/// or your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use std::iter::Iterator;
|
||||
///
|
||||
/// use ratatui::{prelude::*, text::StyledGrapheme};
|
||||
///
|
||||
/// let line = Line::styled("Text", Style::default().fg(Color::Yellow));
|
||||
/// let style = Style::default().fg(Color::Green).bg(Color::Black);
|
||||
/// assert_eq!(
|
||||
/// line.styled_graphemes(style).collect::<Vec<StyledGrapheme>>(),
|
||||
/// line.styled_graphemes(style)
|
||||
/// .collect::<Vec<StyledGrapheme>>(),
|
||||
/// vec![
|
||||
/// StyledGrapheme::new("T", Style::default().fg(Color::Yellow).bg(Color::Black)),
|
||||
/// StyledGrapheme::new("e", Style::default().fg(Color::Yellow).bg(Color::Black)),
|
||||
@@ -89,26 +259,33 @@ impl<'a> Line<'a> {
|
||||
/// ]
|
||||
/// );
|
||||
/// ```
|
||||
pub fn styled_graphemes(
|
||||
pub fn styled_graphemes<S: Into<Style>>(
|
||||
&'a self,
|
||||
base_style: Style,
|
||||
base_style: S,
|
||||
) -> impl Iterator<Item = StyledGrapheme<'a>> {
|
||||
let style = base_style.into().patch(self.style);
|
||||
self.spans
|
||||
.iter()
|
||||
.flat_map(move |span| span.styled_graphemes(base_style))
|
||||
.flat_map(move |span| span.styled_graphemes(style))
|
||||
}
|
||||
|
||||
/// Patches the style of each Span in an existing Line, adding modifiers from the given style.
|
||||
///
|
||||
/// ## Examples
|
||||
/// This is useful for when you want to apply a style to a line that already has some styling.
|
||||
/// In contrast to [`Line::style`], this method will not overwrite the existing style, but
|
||||
/// instead will add the given style's modifiers to the existing style of each `Span`.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
/// let mut raw_line = Line::from(vec![
|
||||
/// Span::raw("My"),
|
||||
/// Span::raw(" text"),
|
||||
/// ]);
|
||||
/// let style = Style::default()
|
||||
/// .fg(Color::Yellow)
|
||||
/// .add_modifier(Modifier::ITALIC);
|
||||
/// let mut raw_line = Line::from(vec![Span::raw("My"), Span::raw(" text")]);
|
||||
/// let mut styled_line = Line::from(vec![
|
||||
/// Span::styled("My", style),
|
||||
/// Span::styled(" text", style),
|
||||
@@ -119,16 +296,18 @@ impl<'a> Line<'a> {
|
||||
/// raw_line.patch_style(style);
|
||||
/// assert_eq!(raw_line, styled_line);
|
||||
/// ```
|
||||
pub fn patch_style(&mut self, style: Style) {
|
||||
pub fn patch_style<S: Into<Style>>(&mut self, style: S) {
|
||||
let style = style.into();
|
||||
for span in &mut self.spans {
|
||||
span.patch_style(style);
|
||||
}
|
||||
}
|
||||
|
||||
/// Resets the style of each Span in the Line.
|
||||
///
|
||||
/// Equivalent to calling `patch_style(Style::reset())`.
|
||||
///
|
||||
/// ## Examples
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
@@ -146,24 +325,6 @@ impl<'a> Line<'a> {
|
||||
span.reset_style();
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the target alignment for this line of text.
|
||||
/// Defaults to: [`None`], meaning the alignment is determined by the rendering widget.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let mut line = Line::from("Hi, what's up?");
|
||||
/// assert_eq!(None, line.alignment);
|
||||
/// assert_eq!(Some(Alignment::Right), line.alignment(Alignment::Right).alignment)
|
||||
/// ```
|
||||
pub fn alignment(self, alignment: Alignment) -> Self {
|
||||
Self {
|
||||
alignment: Some(alignment),
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<String> for Line<'a> {
|
||||
@@ -202,16 +363,118 @@ impl<'a> From<Line<'a>> for String {
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Line<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let area = area.intersection(buf.area);
|
||||
buf.set_style(area, self.style);
|
||||
let width = self.width() as u16;
|
||||
let offset = match self.alignment {
|
||||
Some(Alignment::Left) => 0,
|
||||
Some(Alignment::Center) => (area.width.saturating_sub(width)) / 2,
|
||||
Some(Alignment::Right) => area.width.saturating_sub(width),
|
||||
None => 0,
|
||||
};
|
||||
let mut x = area.left().saturating_add(offset);
|
||||
for span in self.spans {
|
||||
let span_width = span.width() as u16;
|
||||
let span_area = Rect {
|
||||
x,
|
||||
width: span_width,
|
||||
..area
|
||||
};
|
||||
span.render(span_area, buf);
|
||||
x = x.saturating_add(span_width);
|
||||
if x >= area.right() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
layout::Alignment,
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Line, Span, StyledGrapheme},
|
||||
};
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_width() {
|
||||
fn raw_str() {
|
||||
let line = Line::raw("test content");
|
||||
assert_eq!(line.spans, vec![Span::raw("test content")]);
|
||||
assert_eq!(line.alignment, None);
|
||||
|
||||
let line = Line::raw("a\nb");
|
||||
assert_eq!(line.spans, vec![Span::raw("a"), Span::raw("b")]);
|
||||
assert_eq!(line.alignment, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn styled_str() {
|
||||
let style = Style::new().yellow();
|
||||
let content = "Hello, world!";
|
||||
let line = Line::styled(content, style);
|
||||
assert_eq!(line.spans, vec![Span::raw(content)]);
|
||||
assert_eq!(line.style, style);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn styled_string() {
|
||||
let style = Style::new().yellow();
|
||||
let content = String::from("Hello, world!");
|
||||
let line = Line::styled(content.clone(), style);
|
||||
assert_eq!(line.spans, vec![Span::raw(content)]);
|
||||
assert_eq!(line.style, style);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn styled_cow() {
|
||||
let style = Style::new().yellow();
|
||||
let content = Cow::from("Hello, world!");
|
||||
let line = Line::styled(content.clone(), style);
|
||||
assert_eq!(line.spans, vec![Span::raw(content)]);
|
||||
assert_eq!(line.style, style);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spans_vec() {
|
||||
let line = Line::default().spans(vec!["Hello".blue(), " world!".green()]);
|
||||
assert_eq!(
|
||||
line.spans,
|
||||
vec![
|
||||
Span::styled("Hello", Style::new().blue()),
|
||||
Span::styled(" world!", Style::new().green()),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spans_iter() {
|
||||
let line = Line::default().spans([1, 2, 3].iter().map(|i| format!("Item {i}")));
|
||||
assert_eq!(
|
||||
line.spans,
|
||||
vec![
|
||||
Span::raw("Item 1"),
|
||||
Span::raw("Item 2"),
|
||||
Span::raw("Item 3"),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn style() {
|
||||
let line = Line::default().style(Style::new().red());
|
||||
assert_eq!(line.style, Style::new().red());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alignment() {
|
||||
let line = Line::from("This is left").alignment(Alignment::Left);
|
||||
assert_eq!(Some(Alignment::Left), line.alignment);
|
||||
|
||||
let line = Line::from("This is default");
|
||||
assert_eq!(None, line.alignment);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn width() {
|
||||
let line = Line::from(vec![
|
||||
Span::styled("My", Style::default().fg(Color::Yellow)),
|
||||
Span::raw(" text"),
|
||||
@@ -223,7 +486,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_patch_style() {
|
||||
fn patch_style() {
|
||||
let style = Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::ITALIC);
|
||||
@@ -240,7 +503,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reset_style() {
|
||||
fn reset_style() {
|
||||
let mut line = Line::from(vec![
|
||||
Span::styled("My", Style::default().fg(Color::Yellow)),
|
||||
Span::styled(" text", Style::default().add_modifier(Modifier::BOLD)),
|
||||
@@ -252,21 +515,21 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_string() {
|
||||
fn from_string() {
|
||||
let s = String::from("Hello, world!");
|
||||
let line = Line::from(s);
|
||||
assert_eq!(vec![Span::from("Hello, world!")], line.spans);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_str() {
|
||||
fn from_str() {
|
||||
let s = "Hello, world!";
|
||||
let line = Line::from(s);
|
||||
assert_eq!(vec![Span::from("Hello, world!")], line.spans);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_vec() {
|
||||
fn from_vec() {
|
||||
let spans = vec![
|
||||
Span::styled("Hello,", Style::default().fg(Color::Red)),
|
||||
Span::styled(" world!", Style::default().fg(Color::Green)),
|
||||
@@ -276,14 +539,14 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_span() {
|
||||
fn from_span() {
|
||||
let span = Span::styled("Hello, world!", Style::default().fg(Color::Yellow));
|
||||
let line = Line::from(span.clone());
|
||||
assert_eq!(vec![span], line.spans);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_into_string() {
|
||||
fn into_string() {
|
||||
let line = Line::from(vec![
|
||||
Span::styled("Hello,", Style::default().fg(Color::Red)),
|
||||
Span::styled(" world!", Style::default().fg(Color::Green)),
|
||||
@@ -292,15 +555,6 @@ mod tests {
|
||||
assert_eq!("Hello, world!", s);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_alignment() {
|
||||
let line = Line::from("This is left").alignment(Alignment::Left);
|
||||
assert_eq!(Some(Alignment::Left), line.alignment);
|
||||
|
||||
let line = Line::from("This is default");
|
||||
assert_eq!(None, line.alignment);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn styled_graphemes() {
|
||||
const RED: Style = Style::new().fg(Color::Red);
|
||||
@@ -331,14 +585,75 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_str() {
|
||||
let line = Line::raw("test content");
|
||||
assert_eq!(line.spans, vec![Span::raw("test content")]);
|
||||
assert_eq!(line.alignment, None);
|
||||
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);
|
||||
|
||||
let line = Line::raw("a\nb");
|
||||
assert_eq!(line.spans, vec![Span::raw("a"), Span::raw("b")]);
|
||||
assert_eq!(line.alignment, None);
|
||||
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, 11, 1));
|
||||
hello_world().render(Rect::new(0, 0, 11, 1), &mut buf);
|
||||
let mut expected = Buffer::with_lines(vec!["Hello world"]);
|
||||
expected.set_style(Rect::new(0, 0, 6, 1), BLUE.italic());
|
||||
expected.set_style(Rect::new(6, 0, 5, 1), GREEN.italic());
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
328
src/text/span.rs
328
src/text/span.rs
@@ -4,13 +4,33 @@ use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use super::StyledGrapheme;
|
||||
use crate::style::{Style, Styled};
|
||||
use crate::{prelude::*, widgets::Widget};
|
||||
|
||||
/// Represents a part of a line that is contiguous and where all characters share the same style.
|
||||
///
|
||||
/// A `Span` is the smallest unit of text that can be styled. It is usually combined in the [`Line`]
|
||||
/// type to represent a line of text where each `Span` may have a different style.
|
||||
///
|
||||
/// # Constructor Methods
|
||||
///
|
||||
/// - [`Span::default`] creates an span with empty content and the default style.
|
||||
/// - [`Span::raw`] creates an span with the specified content and the default style.
|
||||
/// - [`Span::styled`] creates an span with the specified content and style.
|
||||
///
|
||||
/// # Setter Methods
|
||||
///
|
||||
/// These methods are fluent setters. They return a new `Span` with the specified property set.
|
||||
///
|
||||
/// - [`Span::content`] sets the content of the span.
|
||||
/// - [`Span::style`] sets the style of the span.
|
||||
///
|
||||
/// # Other Methods
|
||||
///
|
||||
/// - [`Span::patch_style`] patches the style of the span, adding modifiers from the given style.
|
||||
/// - [`Span::reset_style`] resets the style of the span.
|
||||
/// - [`Span::width`] returns the unicode width of the content held by this span.
|
||||
/// - [`Span::styled_graphemes`] returns an iterator over the graphemes held by this span.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// A `Span` with `style` set to [`Style::default()`] can be created from a `&str`, a `String`, or
|
||||
@@ -35,21 +55,38 @@ use crate::style::{Style, Styled};
|
||||
///
|
||||
/// let span = Span::styled("test content", Style::new().green());
|
||||
/// let span = Span::styled(String::from("test content"), Style::new().green());
|
||||
///
|
||||
/// // using Stylize trait shortcuts
|
||||
/// let span = "test content".green();
|
||||
/// let span = String::from("test content").green();
|
||||
/// ```
|
||||
///
|
||||
/// `Span` implements [`Stylize`], which allows it to be styled using the shortcut methods. Styles
|
||||
/// applied are additive.
|
||||
/// `Span` implements the [`Styled`] trait, which allows it to be styled using the shortcut methods
|
||||
/// defined in the [`Stylize`] trait.
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::prelude::*;
|
||||
///
|
||||
/// let span = Span::raw("test content").green().on_yellow().italic();
|
||||
/// let span = Span::raw(String::from("test content")).green().on_yellow().italic();
|
||||
/// let span = Span::raw(String::from("test content"))
|
||||
/// .green()
|
||||
/// .on_yellow()
|
||||
/// .italic();
|
||||
/// ```
|
||||
///
|
||||
/// `Span` implements the [`Widget`] trait, which allows it to be rendered to a [`Buffer`]. Usually
|
||||
/// apps will use the [`Paragraph`] widget instead of rendering `Span` directly, as it handles text
|
||||
/// wrapping and alignment for you.
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::prelude::*;
|
||||
///
|
||||
/// # fn render_frame(frame: &mut Frame) {
|
||||
/// frame.render_widget("test content".green().on_yellow().italic(), frame.size());
|
||||
/// # }
|
||||
/// ```
|
||||
/// [`Line`]: crate::text::Line
|
||||
/// [`Paragraph`]: crate::widgets::Paragraph
|
||||
/// [`Stylize`]: crate::style::Stylize
|
||||
/// [`Cow<str>`]: std::borrow::Cow
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
@@ -82,6 +119,12 @@ impl<'a> Span<'a> {
|
||||
|
||||
/// Create a span with the specified style.
|
||||
///
|
||||
/// `content` accepts any type that is convertible to [`Cow<str>`] (e.g. `&str`, `String`,
|
||||
/// `&String`, etc.).
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
@@ -90,60 +133,66 @@ impl<'a> Span<'a> {
|
||||
/// Span::styled("test content", style);
|
||||
/// Span::styled(String::from("test content"), style);
|
||||
/// ```
|
||||
pub fn styled<T>(content: T, style: Style) -> Span<'a>
|
||||
pub fn styled<T, S>(content: T, style: S) -> Span<'a>
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
S: Into<Style>,
|
||||
{
|
||||
Span {
|
||||
content: content.into(),
|
||||
style,
|
||||
style: style.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the unicode width of the content held by this span.
|
||||
pub fn width(&self) -> usize {
|
||||
self.content.width()
|
||||
}
|
||||
|
||||
/// Returns an iterator over the graphemes held by this span.
|
||||
/// Sets the content of the span.
|
||||
///
|
||||
/// `base_style` is the [`Style`] that will be patched with the `Span`'s `style` to get the
|
||||
/// resulting [`Style`].
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
///
|
||||
/// # Example
|
||||
/// Accepts any type that can be converted to [`Cow<str>`] (e.g. `&str`, `String`, `&String`,
|
||||
/// etc.).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use std::iter::Iterator;
|
||||
/// use ratatui::{prelude::*, text::StyledGrapheme};
|
||||
///
|
||||
/// let span = Span::styled("Test", Style::new().green().italic());
|
||||
/// let style = Style::new().red().on_yellow();
|
||||
/// assert_eq!(
|
||||
/// span.styled_graphemes(style).collect::<Vec<StyledGrapheme>>(),
|
||||
/// vec![
|
||||
/// StyledGrapheme::new("T", Style::new().green().on_yellow().italic()),
|
||||
/// StyledGrapheme::new("e", Style::new().green().on_yellow().italic()),
|
||||
/// StyledGrapheme::new("s", Style::new().green().on_yellow().italic()),
|
||||
/// StyledGrapheme::new("t", Style::new().green().on_yellow().italic()),
|
||||
/// ],
|
||||
/// );
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let mut span = Span::default().content("content");
|
||||
/// ```
|
||||
pub fn styled_graphemes(
|
||||
&'a self,
|
||||
base_style: Style,
|
||||
) -> impl Iterator<Item = StyledGrapheme<'a>> {
|
||||
self.content
|
||||
.as_ref()
|
||||
.graphemes(true)
|
||||
.filter(|g| *g != "\n")
|
||||
.map(move |g| StyledGrapheme {
|
||||
symbol: g,
|
||||
style: base_style.patch(self.style),
|
||||
})
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn content<T>(mut self, content: T) -> Self
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
{
|
||||
self.content = content.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the span.
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
///
|
||||
/// In contrast to [`Span::patch_style`], this method replaces the style of the span instead of
|
||||
/// patching it.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let mut span = Span::default().style(Style::new().green());
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Patches the style of the Span, adding modifiers from the given style.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
@@ -152,7 +201,7 @@ impl<'a> Span<'a> {
|
||||
/// span.patch_style(Style::new().red().on_yellow().bold());
|
||||
/// assert_eq!(span.style, Style::new().red().on_yellow().italic().bold());
|
||||
/// ```
|
||||
pub fn patch_style(&mut self, style: Style) {
|
||||
pub fn patch_style<S: Into<Style>>(&mut self, style: S) {
|
||||
self.style = self.style.patch(style);
|
||||
}
|
||||
|
||||
@@ -171,6 +220,51 @@ impl<'a> Span<'a> {
|
||||
pub fn reset_style(&mut self) {
|
||||
self.patch_style(Style::reset());
|
||||
}
|
||||
|
||||
/// Returns the unicode width of the content held by this span.
|
||||
pub fn width(&self) -> usize {
|
||||
self.content.width()
|
||||
}
|
||||
|
||||
/// Returns an iterator over the graphemes held by this span.
|
||||
///
|
||||
/// `base_style` is the [`Style`] that will be patched with the `Span`'s `style` to get the
|
||||
/// resulting [`Style`].
|
||||
///
|
||||
/// `base_style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`],
|
||||
/// or your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use std::iter::Iterator;
|
||||
///
|
||||
/// use ratatui::{prelude::*, text::StyledGrapheme};
|
||||
///
|
||||
/// let span = Span::styled("Test", Style::new().green().italic());
|
||||
/// let style = Style::new().red().on_yellow();
|
||||
/// assert_eq!(
|
||||
/// span.styled_graphemes(style)
|
||||
/// .collect::<Vec<StyledGrapheme>>(),
|
||||
/// vec![
|
||||
/// StyledGrapheme::new("T", Style::new().green().on_yellow().italic()),
|
||||
/// StyledGrapheme::new("e", Style::new().green().on_yellow().italic()),
|
||||
/// StyledGrapheme::new("s", Style::new().green().on_yellow().italic()),
|
||||
/// StyledGrapheme::new("t", Style::new().green().on_yellow().italic()),
|
||||
/// ],
|
||||
/// );
|
||||
/// ```
|
||||
pub fn styled_graphemes<S: Into<Style>>(
|
||||
&'a self,
|
||||
base_style: S,
|
||||
) -> impl Iterator<Item = StyledGrapheme<'a>> {
|
||||
let style = base_style.into().patch(self.style);
|
||||
self.content
|
||||
.as_ref()
|
||||
.graphemes(true)
|
||||
.filter(|g| *g != "\n")
|
||||
.map(move |g| StyledGrapheme { symbol: g, style })
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> From<T> for Span<'a>
|
||||
@@ -189,16 +283,47 @@ impl<'a> Styled for Span<'a> {
|
||||
self.style
|
||||
}
|
||||
|
||||
fn set_style(mut self, style: Style) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
|
||||
self.style(style)
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Span<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let Rect {
|
||||
x: mut current_x,
|
||||
y,
|
||||
width,
|
||||
..
|
||||
} = area;
|
||||
let max_x = Ord::min(current_x.saturating_add(width), buf.area.right());
|
||||
for g in self.styled_graphemes(Style::default()) {
|
||||
let symbol_width = g.symbol.width();
|
||||
let next_x = current_x.saturating_add(symbol_width as u16);
|
||||
if next_x > max_x {
|
||||
break;
|
||||
}
|
||||
buf.get_mut(current_x, y)
|
||||
.set_symbol(g.symbol)
|
||||
.set_style(g.style);
|
||||
|
||||
// multi-width graphemes must clear the cells of characters that are hidden by the
|
||||
// grapheme, otherwise the hidden characters will be re-rendered if the grapheme is
|
||||
// overwritten.
|
||||
for i in (current_x + 1)..next_x {
|
||||
buf.get_mut(i, y).reset();
|
||||
// it may seem odd that the style of the hidden cells are not set to the style of
|
||||
// the grapheme, but this is how the existing buffer.set_span() method works.
|
||||
// buf.get_mut(i, y).set_style(g.style);
|
||||
}
|
||||
current_x = next_x;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::style::Stylize;
|
||||
|
||||
#[test]
|
||||
fn default() {
|
||||
@@ -239,6 +364,18 @@ mod tests {
|
||||
assert_eq!(span.style, style);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_content() {
|
||||
let span = Span::default().content("test content");
|
||||
assert_eq!(span.content, Cow::Borrowed("test content"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_style() {
|
||||
let span = Span::default().style(Style::new().green());
|
||||
assert_eq!(span.style, Style::new().green());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_ref_str_borrowed_cow() {
|
||||
let content = "test content";
|
||||
@@ -303,4 +440,103 @@ mod tests {
|
||||
assert_eq!(stylized.content, Cow::Borrowed("test content"));
|
||||
assert_eq!(stylized.style, Style::new().green().on_yellow().bold());
|
||||
}
|
||||
|
||||
mod widget {
|
||||
use super::*;
|
||||
use crate::{assert_buffer_eq, style::Stylize};
|
||||
|
||||
#[test]
|
||||
fn render() {
|
||||
let style = Style::new().green().on_yellow();
|
||||
let span = Span::styled("test content", style);
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
|
||||
span.render(buf.area, &mut buf);
|
||||
|
||||
let expected = Buffer::with_lines(vec![Line::from(vec![
|
||||
"test content".green().on_yellow(),
|
||||
" ".into(),
|
||||
])]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
}
|
||||
|
||||
/// When the content of the span is longer than the area passed to render, the content
|
||||
/// should be truncated
|
||||
#[test]
|
||||
fn render_truncates_too_long_content() {
|
||||
let style = Style::new().green().on_yellow();
|
||||
let span = Span::styled("test content", style);
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 1));
|
||||
span.render(buf.area, &mut buf);
|
||||
|
||||
let expected =
|
||||
Buffer::with_lines(vec![Line::from(vec!["test conte".green().on_yellow()])]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
}
|
||||
|
||||
/// When there is already a style set on the buffer, the style of the span should be
|
||||
/// patched with the existing style
|
||||
#[test]
|
||||
fn render_patches_existing_style() {
|
||||
let style = Style::new().green().on_yellow();
|
||||
let span = Span::styled("test content", style);
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
|
||||
buf.set_style(buf.area, Style::new().italic());
|
||||
span.render(buf.area, &mut buf);
|
||||
|
||||
let expected = Buffer::with_lines(vec![Line::from(vec![
|
||||
"test content".green().on_yellow().italic(),
|
||||
" ".italic(),
|
||||
])]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
}
|
||||
|
||||
/// When the span contains a multi-width grapheme, the grapheme will ensure that the cells
|
||||
/// of the hidden characters are cleared.
|
||||
#[test]
|
||||
fn render_multi_width_symbol() {
|
||||
let style = Style::new().green().on_yellow();
|
||||
let span = Span::styled("test 😃 content", style);
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
|
||||
span.render(buf.area, &mut buf);
|
||||
|
||||
// The existing code in buffer.set_line() handles multi-width graphemes by clearing the
|
||||
// cells of the hidden characters. This test ensures that the existing behavior is
|
||||
// preserved.
|
||||
let expected = Buffer::with_lines(vec!["test 😃 content".green().on_yellow()]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
}
|
||||
|
||||
/// When the span contains a multi-width grapheme that does not fit in the area passed to
|
||||
/// render, the entire grapheme will be truncated.
|
||||
#[test]
|
||||
fn render_multi_width_symbol_truncates_entire_symbol() {
|
||||
// the 😃 emoji is 2 columns wide so it will be truncated
|
||||
let style = Style::new().green().on_yellow();
|
||||
let span = Span::styled("test 😃 content", style);
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 6, 1));
|
||||
span.render(buf.area, &mut buf);
|
||||
|
||||
let expected = Buffer::with_lines(vec![Line::from(vec![
|
||||
"test ".green().on_yellow(),
|
||||
" ".into(),
|
||||
])]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
}
|
||||
|
||||
/// When the area passed to render overflows the buffer, the content should be truncated
|
||||
/// to fit the buffer.
|
||||
#[test]
|
||||
fn render_overflowing_area_truncates() {
|
||||
let style = Style::new().green().on_yellow();
|
||||
let span = Span::styled("test content", style);
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
|
||||
span.render(Rect::new(10, 0, 20, 1), &mut buf);
|
||||
|
||||
let expected = Buffer::with_lines(vec![Line::from(vec![
|
||||
" ".into(),
|
||||
"test ".green().on_yellow(),
|
||||
])]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use super::{Line, Span};
|
||||
use crate::style::Style;
|
||||
use crate::prelude::*;
|
||||
|
||||
/// A string split over multiple lines where each line is composed of several clusters, each with
|
||||
/// their own style.
|
||||
@@ -13,7 +12,9 @@ use crate::style::Style;
|
||||
/// ```rust
|
||||
/// use ratatui::prelude::*;
|
||||
///
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
/// let style = Style::default()
|
||||
/// .fg(Color::Yellow)
|
||||
/// .add_modifier(Modifier::ITALIC);
|
||||
///
|
||||
/// // An initial two lines of `Text` built from a `&str`
|
||||
/// let mut text = Text::from("The first line\nThe second line");
|
||||
@@ -58,17 +59,23 @@ impl<'a> Text<'a> {
|
||||
|
||||
/// Create some text (potentially multiple lines) with a style.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
/// let style = Style::default()
|
||||
/// .fg(Color::Yellow)
|
||||
/// .add_modifier(Modifier::ITALIC);
|
||||
/// Text::styled("The first line\nThe second line", style);
|
||||
/// Text::styled(String::from("The first line\nThe second line"), style);
|
||||
/// ```
|
||||
pub fn styled<T>(content: T, style: Style) -> Text<'a>
|
||||
pub fn styled<T, S>(content: T, style: S) -> Text<'a>
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
S: Into<Style>,
|
||||
{
|
||||
let mut text = Text::raw(content);
|
||||
text.patch_style(style);
|
||||
@@ -103,11 +110,16 @@ impl<'a> Text<'a> {
|
||||
|
||||
/// Patches the style of each line in an existing Text, adding modifiers from the given style.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
/// let style = Style::default()
|
||||
/// .fg(Color::Yellow)
|
||||
/// .add_modifier(Modifier::ITALIC);
|
||||
/// let mut raw_text = Text::raw("The first line\nThe second line");
|
||||
/// let styled_text = Text::styled(String::from("The first line\nThe second line"), style);
|
||||
/// assert_ne!(raw_text, styled_text);
|
||||
@@ -115,7 +127,8 @@ impl<'a> Text<'a> {
|
||||
/// raw_text.patch_style(style);
|
||||
/// assert_eq!(raw_text, styled_text);
|
||||
/// ```
|
||||
pub fn patch_style(&mut self, style: Style) {
|
||||
pub fn patch_style<S: Into<Style>>(&mut self, style: S) {
|
||||
let style = style.into();
|
||||
for line in &mut self.lines {
|
||||
line.patch_style(style);
|
||||
}
|
||||
@@ -128,7 +141,9 @@ impl<'a> Text<'a> {
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
/// let style = Style::default()
|
||||
/// .fg(Color::Yellow)
|
||||
/// .add_modifier(Modifier::ITALIC);
|
||||
/// let mut text = Text::styled("The first line\nThe second line", style);
|
||||
///
|
||||
/// text.reset_style();
|
||||
|
||||
23
src/title.rs
23
src/title.rs
@@ -29,14 +29,15 @@ use crate::{layout::Alignment, text::Line};
|
||||
/// ```
|
||||
/// use ratatui::{prelude::*, widgets::block::*};
|
||||
///
|
||||
/// Title::from(
|
||||
/// Line::from(vec!["Q".white().underlined(), "uit".gray()])
|
||||
/// );
|
||||
/// Title::from(Line::from(vec!["Q".white().underlined(), "uit".gray()]));
|
||||
/// ```
|
||||
///
|
||||
/// Complete example
|
||||
/// ```
|
||||
/// use ratatui::{prelude::*, widgets::{*, block::*}};
|
||||
/// use ratatui::{
|
||||
/// prelude::*,
|
||||
/// widgets::{block::*, *},
|
||||
/// };
|
||||
///
|
||||
/// Title::from("Title")
|
||||
/// .position(Position::Top)
|
||||
@@ -69,11 +70,9 @@ pub struct Title<'a> {
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::widgets::{*, block::*};
|
||||
/// use ratatui::widgets::{block::*, *};
|
||||
///
|
||||
/// Block::new().title(
|
||||
/// Title::from("title").position(Position::Bottom)
|
||||
/// );
|
||||
/// Block::new().title(Title::from("title").position(Position::Bottom));
|
||||
/// ```
|
||||
#[derive(Debug, Default, Display, EnumString, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum Position {
|
||||
@@ -87,7 +86,7 @@ pub enum Position {
|
||||
}
|
||||
|
||||
impl<'a> Title<'a> {
|
||||
/// Builder pattern method for setting the title content.
|
||||
/// Set the title content.
|
||||
pub fn content<T>(mut self, content: T) -> Title<'a>
|
||||
where
|
||||
T: Into<Line<'a>>,
|
||||
@@ -96,13 +95,15 @@ impl<'a> Title<'a> {
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder pattern method for setting the title alignment.
|
||||
/// Set the title alignment.
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn alignment(mut self, alignment: Alignment) -> Title<'a> {
|
||||
self.alignment = Some(alignment);
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder pattern method for setting the title position.
|
||||
/// Set the title position.
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn position(mut self, position: Position) -> Title<'a> {
|
||||
self.position = Some(position);
|
||||
self
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! `widgets` is a collection of types that implement [`Widget`] or [`StatefulWidget`] or both.
|
||||
//!
|
||||
//! All widgets are implemented using the builder pattern and are consumable objects. They are not
|
||||
//! meant to be stored but used as *commands* to draw common figures in the UI.
|
||||
//! Widgets are created for each frame as they are consumed after rendered.
|
||||
//! They are not meant to be stored but used as *commands* to draw common figures in the UI.
|
||||
//!
|
||||
//! The available widgets are:
|
||||
//! - [`Block`]: a basic widget that draws a block with optional borders, titles and styles.
|
||||
@@ -43,10 +43,10 @@ use bitflags::bitflags;
|
||||
pub use self::{
|
||||
barchart::{Bar, BarChart, BarGroup},
|
||||
block::{Block, BorderType, Padding},
|
||||
chart::{Axis, Chart, Dataset, GraphType},
|
||||
chart::{Axis, Chart, Dataset, GraphType, LegendPosition},
|
||||
clear::Clear,
|
||||
gauge::{Gauge, LineGauge},
|
||||
list::{List, ListItem, ListState},
|
||||
list::{List, ListDirection, ListItem, ListState},
|
||||
paragraph::{Paragraph, Wrap},
|
||||
scrollbar::{ScrollDirection, Scrollbar, ScrollbarOrientation, ScrollbarState},
|
||||
sparkline::{RenderDirection, Sparkline},
|
||||
@@ -130,6 +130,7 @@ pub trait Widget {
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use std::io;
|
||||
///
|
||||
/// use ratatui::{backend::TestBackend, prelude::*, widgets::*};
|
||||
///
|
||||
/// // Let's say we have some events to display.
|
||||
@@ -139,7 +140,7 @@ pub trait Widget {
|
||||
/// // `state` is the state that can be modified by the UI. It stores the index of the selected
|
||||
/// // item as well as the offset computed during the previous draw call (used to implement
|
||||
/// // natural scrolling).
|
||||
/// state: ListState
|
||||
/// state: ListState,
|
||||
/// }
|
||||
///
|
||||
/// impl Events {
|
||||
@@ -199,16 +200,17 @@ pub trait Widget {
|
||||
/// # let backend = TestBackend::new(5, 5);
|
||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||
///
|
||||
/// let mut events = Events::new(vec![
|
||||
/// String::from("Item 1"),
|
||||
/// String::from("Item 2")
|
||||
/// ]);
|
||||
/// let mut events = Events::new(vec![String::from("Item 1"), String::from("Item 2")]);
|
||||
///
|
||||
/// loop {
|
||||
/// terminal.draw(|f| {
|
||||
/// // The items managed by the application are transformed to something
|
||||
/// // that is understood by ratatui.
|
||||
/// let items: Vec<ListItem>= events.items.iter().map(|i| ListItem::new(i.as_str())).collect();
|
||||
/// let items: Vec<ListItem> = events
|
||||
/// .items
|
||||
/// .iter()
|
||||
/// .map(|i| ListItem::new(i.as_str()))
|
||||
/// .collect();
|
||||
/// // The `List` widget is then built with those items.
|
||||
/// let list = List::new(items);
|
||||
/// // Finally the widget is rendered using the associated state. `events.state` is
|
||||
|
||||
@@ -127,6 +127,7 @@ impl<'a> BarChart<'a> {
|
||||
}
|
||||
|
||||
/// Surround the [`BarChart`] with a [`Block`].
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn block(mut self, block: Block<'a>) -> BarChart<'a> {
|
||||
self.block = Some(block);
|
||||
self
|
||||
@@ -161,6 +162,7 @@ impl<'a> BarChart<'a> {
|
||||
/// // █ █ █
|
||||
/// // f b b
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn max(mut self, max: u64) -> BarChart<'a> {
|
||||
self.max = Some(max);
|
||||
self
|
||||
@@ -168,10 +170,14 @@ impl<'a> BarChart<'a> {
|
||||
|
||||
/// Set the default style of the bar.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// It is also possible to set individually the style of each [`Bar`].
|
||||
/// In this case the default style will be patched by the individual style
|
||||
pub fn bar_style(mut self, style: Style) -> BarChart<'a> {
|
||||
self.bar_style = style;
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn bar_style<S: Into<Style>>(mut self, style: S) -> BarChart<'a> {
|
||||
self.bar_style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
@@ -182,6 +188,7 @@ impl<'a> BarChart<'a> {
|
||||
///
|
||||
/// If not set, this defaults to `1`.
|
||||
/// The bar label also uses this value as its width.
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn bar_width(mut self, width: u16) -> BarChart<'a> {
|
||||
self.bar_width = width;
|
||||
self
|
||||
@@ -205,6 +212,7 @@ impl<'a> BarChart<'a> {
|
||||
/// // █ █
|
||||
/// // f b
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn bar_gap(mut self, gap: u16) -> BarChart<'a> {
|
||||
self.bar_gap = gap;
|
||||
self
|
||||
@@ -213,6 +221,7 @@ impl<'a> BarChart<'a> {
|
||||
/// The [`bar::Set`](crate::symbols::bar::Set) to use for displaying the bars.
|
||||
///
|
||||
/// If not set, the default is [`bar::NINE_LEVELS`](crate::symbols::bar::NINE_LEVELS).
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn bar_set(mut self, bar_set: symbols::bar::Set) -> BarChart<'a> {
|
||||
self.bar_set = bar_set;
|
||||
self
|
||||
@@ -220,31 +229,40 @@ impl<'a> BarChart<'a> {
|
||||
|
||||
/// Set the default value style of the bar.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// It is also possible to set individually the value style of each [`Bar`].
|
||||
/// In this case the default value style will be patched by the individual value style
|
||||
///
|
||||
/// # See also
|
||||
///
|
||||
/// [Bar::value_style] to set the value style individually.
|
||||
pub fn value_style(mut self, style: Style) -> BarChart<'a> {
|
||||
self.value_style = style;
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn value_style<S: Into<Style>>(mut self, style: S) -> BarChart<'a> {
|
||||
self.value_style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the default label style of the groups and bars.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// It is also possible to set individually the label style of each [`Bar`] or [`BarGroup`].
|
||||
/// In this case the default label style will be patched by the individual label style
|
||||
///
|
||||
/// # See also
|
||||
///
|
||||
/// [Bar::label] to set the label style individually.
|
||||
pub fn label_style(mut self, style: Style) -> BarChart<'a> {
|
||||
self.label_style = style;
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn label_style<S: Into<Style>>(mut self, style: S) -> BarChart<'a> {
|
||||
self.label_style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the gap between [`BarGroup`].
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn group_gap(mut self, gap: u16) -> BarChart<'a> {
|
||||
self.group_gap = gap;
|
||||
self
|
||||
@@ -252,9 +270,13 @@ impl<'a> BarChart<'a> {
|
||||
|
||||
/// Set the style of the entire chart.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// The style will be applied to everything that isn't styled (borders, bars, labels, ...).
|
||||
pub fn style(mut self, style: Style) -> BarChart<'a> {
|
||||
self.style = style;
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> BarChart<'a> {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
@@ -277,6 +299,7 @@ impl<'a> BarChart<'a> {
|
||||
///
|
||||
/// █bar██
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn direction(mut self, direction: Direction) -> BarChart<'a> {
|
||||
self.direction = direction;
|
||||
self
|
||||
@@ -586,7 +609,7 @@ impl<'a> Styled for BarChart<'a> {
|
||||
self.style
|
||||
}
|
||||
|
||||
fn set_style(self, style: Style) -> Self {
|
||||
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
|
||||
self.style(style)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{buffer::Buffer, prelude::Rect, style::Style, text::Line};
|
||||
use crate::prelude::*;
|
||||
|
||||
/// A bar to be shown by the [`BarChart`](crate::widgets::BarChart) widget.
|
||||
///
|
||||
@@ -49,6 +49,7 @@ impl<'a> Bar<'a> {
|
||||
///
|
||||
/// [`Bar::value_style`] to style the value.
|
||||
/// [`Bar::text_value`] to set the displayed value.
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn value(mut self, value: u64) -> Bar<'a> {
|
||||
self.value = value;
|
||||
self
|
||||
@@ -61,6 +62,7 @@ impl<'a> Bar<'a> {
|
||||
/// For [`Horizontal`](crate::layout::Direction::Horizontal) bars,
|
||||
/// display the label **in** the bar.
|
||||
/// See [`BarChart::direction`](crate::widgets::BarChart::direction) to set the direction.
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn label(mut self, label: Line<'a>) -> Bar<'a> {
|
||||
self.label = Some(label);
|
||||
self
|
||||
@@ -68,20 +70,27 @@ impl<'a> Bar<'a> {
|
||||
|
||||
/// Set the style of the bar.
|
||||
///
|
||||
/// This will apply to every non-styled element.
|
||||
/// It can be seen and used as a default value.
|
||||
pub fn style(mut self, style: Style) -> Bar<'a> {
|
||||
self.style = style;
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// This will apply to every non-styled element. It can be seen and used as a default value.
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> Bar<'a> {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the style of the value.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// # See also
|
||||
///
|
||||
/// [`Bar::value`] to set the value.
|
||||
pub fn value_style(mut self, style: Style) -> Bar<'a> {
|
||||
self.value_style = style;
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn value_style<S: Into<Style>>(mut self, style: S) -> Bar<'a> {
|
||||
self.value_style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
@@ -93,6 +102,7 @@ impl<'a> Bar<'a> {
|
||||
/// # See also
|
||||
///
|
||||
/// [`Bar::value`] to set the value.
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn text_value(mut self, text_value: String) -> Bar<'a> {
|
||||
self.text_value = Some(text_value);
|
||||
self
|
||||
|
||||
@@ -26,12 +26,14 @@ pub struct BarGroup<'a> {
|
||||
|
||||
impl<'a> BarGroup<'a> {
|
||||
/// Set the group label
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn label(mut self, label: Line<'a>) -> BarGroup<'a> {
|
||||
self.label = Some(label);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the bars of the group to be shown
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn bars(mut self, bars: &[Bar<'a>]) -> BarGroup<'a> {
|
||||
self.bars = bars.to_vec();
|
||||
self
|
||||
|
||||
@@ -13,9 +13,7 @@ use strum::{Display, EnumString};
|
||||
|
||||
pub use self::title::{Position, Title};
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Rect},
|
||||
style::{Style, Styled},
|
||||
prelude::*,
|
||||
symbols::border,
|
||||
widgets::{Borders, Widget},
|
||||
};
|
||||
@@ -220,7 +218,10 @@ impl Padding {
|
||||
///
|
||||
/// You may also use multiple titles like in the following:
|
||||
/// ```
|
||||
/// use ratatui::{prelude::*, widgets::{*, block::*}};
|
||||
/// use ratatui::{
|
||||
/// prelude::*,
|
||||
/// widgets::{block::*, *},
|
||||
/// };
|
||||
///
|
||||
/// Block::default()
|
||||
/// .title("Title 1")
|
||||
@@ -301,7 +302,10 @@ impl<'a> Block<'a> {
|
||||
/// the leftover space)
|
||||
/// - Two titles with the same alignment (notice the left titles are separated)
|
||||
/// ```
|
||||
/// use ratatui::{prelude::*, widgets::{*, block::*}};
|
||||
/// use ratatui::{
|
||||
/// prelude::*,
|
||||
/// widgets::{block::*, *},
|
||||
/// };
|
||||
///
|
||||
/// Block::default()
|
||||
/// .title("Title") // By default in the top left corner
|
||||
@@ -328,9 +332,13 @@ impl<'a> Block<'a> {
|
||||
|
||||
/// Applies the style to all titles.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// If a [`Title`] already has a style, the title's style will add on top of this one.
|
||||
pub const fn title_style(mut self, style: Style) -> Block<'a> {
|
||||
self.titles_style = style;
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn title_style<S: Into<Style>>(mut self, style: S) -> Block<'a> {
|
||||
self.titles_style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
@@ -343,7 +351,10 @@ impl<'a> Block<'a> {
|
||||
/// This example aligns all titles in the center except the "right" title which explicitly sets
|
||||
/// [`Alignment::Right`].
|
||||
/// ```
|
||||
/// use ratatui::{prelude::*, widgets::{*, block::*}};
|
||||
/// use ratatui::{
|
||||
/// prelude::*,
|
||||
/// widgets::{block::*, *},
|
||||
/// };
|
||||
///
|
||||
/// Block::default()
|
||||
/// // This title won't be aligned in the center
|
||||
@@ -352,6 +363,7 @@ impl<'a> Block<'a> {
|
||||
/// .title("bar")
|
||||
/// .title_alignment(Alignment::Center);
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub const fn title_alignment(mut self, alignment: Alignment) -> Block<'a> {
|
||||
self.titles_alignment = alignment;
|
||||
self
|
||||
@@ -372,7 +384,10 @@ impl<'a> Block<'a> {
|
||||
/// This example positions all titles on the bottom except the "top" title which explicitly sets
|
||||
/// [`Position::Top`].
|
||||
/// ```
|
||||
/// use ratatui::{prelude::*, widgets::{*, block::*}};
|
||||
/// use ratatui::{
|
||||
/// prelude::*,
|
||||
/// widgets::{block::*, *},
|
||||
/// };
|
||||
///
|
||||
/// Block::default()
|
||||
/// // This title won't be aligned in the center
|
||||
@@ -381,6 +396,7 @@ impl<'a> Block<'a> {
|
||||
/// .title("bar")
|
||||
/// .title_position(Position::Bottom);
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub const fn title_position(mut self, position: Position) -> Block<'a> {
|
||||
self.titles_position = position;
|
||||
self
|
||||
@@ -390,6 +406,9 @@ impl<'a> Block<'a> {
|
||||
///
|
||||
/// If a [`Block::style`] is defined, `border_style` will be applied on top of it.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// This example shows a `Block` with blue borders.
|
||||
@@ -399,8 +418,9 @@ impl<'a> Block<'a> {
|
||||
/// .borders(Borders::ALL)
|
||||
/// .border_style(Style::new().blue());
|
||||
/// ```
|
||||
pub const fn border_style(mut self, style: Style) -> Block<'a> {
|
||||
self.border_style = style;
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn border_style<S: Into<Style>>(mut self, style: S) -> Block<'a> {
|
||||
self.border_style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
@@ -410,9 +430,13 @@ impl<'a> Block<'a> {
|
||||
/// more specific style. Elements can be styled further with [`Block::title_style`] and
|
||||
/// [`Block::border_style`].
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// This will also apply to the widget inside that block, unless the inner widget is styled.
|
||||
pub const fn style(mut self, style: Style) -> Block<'a> {
|
||||
self.style = style;
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> Block<'a> {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
@@ -433,6 +457,7 @@ impl<'a> Block<'a> {
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// Block::default().borders(Borders::LEFT | Borders::RIGHT);
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub const fn borders(mut self, flag: Borders) -> Block<'a> {
|
||||
self.borders = flag;
|
||||
self
|
||||
@@ -449,12 +474,16 @@ impl<'a> Block<'a> {
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// Block::default().title("Block").borders(Borders::ALL).border_type(BorderType::Rounded);
|
||||
/// Block::default()
|
||||
/// .title("Block")
|
||||
/// .borders(Borders::ALL)
|
||||
/// .border_type(BorderType::Rounded);
|
||||
/// // Renders
|
||||
/// // ╭Block╮
|
||||
/// // │ │
|
||||
/// // ╰─────╯
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub const fn border_type(mut self, border_type: BorderType) -> Block<'a> {
|
||||
self.border_set = border_type.to_border_set();
|
||||
self
|
||||
@@ -473,6 +502,7 @@ impl<'a> Block<'a> {
|
||||
/// // ╔Block╗
|
||||
/// // ║ ║
|
||||
/// // ╚═════╝
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub const fn border_set(mut self, border_set: border::Set) -> Block<'a> {
|
||||
self.border_set = border_set;
|
||||
self
|
||||
@@ -508,14 +538,15 @@ impl<'a> Block<'a> {
|
||||
inner.x = inner.x.saturating_add(1).min(inner.right());
|
||||
inner.width = inner.width.saturating_sub(1);
|
||||
}
|
||||
if self.borders.intersects(Borders::TOP) || !self.titles.is_empty() {
|
||||
if self.borders.intersects(Borders::TOP) || self.have_title_at_position(Position::Top) {
|
||||
inner.y = inner.y.saturating_add(1).min(inner.bottom());
|
||||
inner.height = inner.height.saturating_sub(1);
|
||||
}
|
||||
if self.borders.intersects(Borders::RIGHT) {
|
||||
inner.width = inner.width.saturating_sub(1);
|
||||
}
|
||||
if self.borders.intersects(Borders::BOTTOM) {
|
||||
if self.borders.intersects(Borders::BOTTOM) || self.have_title_at_position(Position::Bottom)
|
||||
{
|
||||
inner.height = inner.height.saturating_sub(1);
|
||||
}
|
||||
|
||||
@@ -532,6 +563,12 @@ impl<'a> Block<'a> {
|
||||
inner
|
||||
}
|
||||
|
||||
fn have_title_at_position(&self, position: Position) -> bool {
|
||||
self.titles
|
||||
.iter()
|
||||
.any(|title| title.position.unwrap_or(self.titles_position) == position)
|
||||
}
|
||||
|
||||
/// Defines the padding inside a `Block`.
|
||||
///
|
||||
/// See [`Padding`] for more information.
|
||||
@@ -562,6 +599,7 @@ impl<'a> Block<'a> {
|
||||
/// // │ content │
|
||||
/// // └───────────┘
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub const fn padding(mut self, padding: Padding) -> Block<'a> {
|
||||
self.padding = padding;
|
||||
self
|
||||
@@ -766,7 +804,7 @@ impl<'a> Styled for Block<'a> {
|
||||
self.style
|
||||
}
|
||||
|
||||
fn set_style(self, style: Style) -> Self::Item {
|
||||
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
|
||||
self.style(style)
|
||||
}
|
||||
}
|
||||
@@ -939,6 +977,78 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inner_takes_into_account_border_and_title() {
|
||||
let test_rect = Rect::new(0, 0, 0, 2);
|
||||
|
||||
let top_top = Block::default()
|
||||
.title(Title::from("Test").position(Position::Top))
|
||||
.borders(Borders::TOP);
|
||||
assert_eq!(top_top.inner(test_rect), Rect::new(0, 1, 0, 1));
|
||||
|
||||
let top_bot = Block::default()
|
||||
.title(Title::from("Test").position(Position::Top))
|
||||
.borders(Borders::BOTTOM);
|
||||
assert_eq!(top_bot.inner(test_rect), Rect::new(0, 1, 0, 0));
|
||||
|
||||
let bot_top = Block::default()
|
||||
.title(Title::from("Test").position(Position::Bottom))
|
||||
.borders(Borders::TOP);
|
||||
assert_eq!(bot_top.inner(test_rect), Rect::new(0, 1, 0, 0));
|
||||
|
||||
let bot_bot = Block::default()
|
||||
.title(Title::from("Test").position(Position::Bottom))
|
||||
.borders(Borders::BOTTOM);
|
||||
assert_eq!(bot_bot.inner(test_rect), Rect::new(0, 0, 0, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn have_title_at_position_takes_into_account_all_positioning_declarations() {
|
||||
let block = Block::default();
|
||||
assert!(!block.have_title_at_position(Position::Top));
|
||||
assert!(!block.have_title_at_position(Position::Bottom));
|
||||
|
||||
let block = Block::default().title(Title::from("Test").position(Position::Top));
|
||||
assert!(block.have_title_at_position(Position::Top));
|
||||
assert!(!block.have_title_at_position(Position::Bottom));
|
||||
|
||||
let block = Block::default().title(Title::from("Test").position(Position::Bottom));
|
||||
assert!(!block.have_title_at_position(Position::Top));
|
||||
assert!(block.have_title_at_position(Position::Bottom));
|
||||
|
||||
let block = Block::default()
|
||||
.title(Title::from("Test").position(Position::Top))
|
||||
.title_position(Position::Bottom);
|
||||
assert!(block.have_title_at_position(Position::Top));
|
||||
assert!(!block.have_title_at_position(Position::Bottom));
|
||||
|
||||
let block = Block::default()
|
||||
.title(Title::from("Test").position(Position::Bottom))
|
||||
.title_position(Position::Top);
|
||||
assert!(!block.have_title_at_position(Position::Top));
|
||||
assert!(block.have_title_at_position(Position::Bottom));
|
||||
|
||||
let block = Block::default()
|
||||
.title(Title::from("Test").position(Position::Top))
|
||||
.title(Title::from("Test").position(Position::Bottom));
|
||||
assert!(block.have_title_at_position(Position::Top));
|
||||
assert!(block.have_title_at_position(Position::Bottom));
|
||||
|
||||
let block = Block::default()
|
||||
.title(Title::from("Test").position(Position::Top))
|
||||
.title(Title::from("Test"))
|
||||
.title_position(Position::Bottom);
|
||||
assert!(block.have_title_at_position(Position::Top));
|
||||
assert!(block.have_title_at_position(Position::Bottom));
|
||||
|
||||
let block = Block::default()
|
||||
.title(Title::from("Test"))
|
||||
.title(Title::from("Test").position(Position::Bottom))
|
||||
.title_position(Position::Top);
|
||||
assert!(block.have_title_at_position(Position::Top));
|
||||
assert!(block.have_title_at_position(Position::Bottom));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn border_type_can_be_const() {
|
||||
const _PLAIN: border::Set = BorderType::border_symbols(BorderType::Plain);
|
||||
@@ -997,15 +1107,60 @@ mod tests {
|
||||
const _DEFAULT_STYLE: Style = Style::new();
|
||||
const _DEFAULT_PADDING: Padding = Padding::uniform(1);
|
||||
const _DEFAULT_BLOCK: Block = Block::new()
|
||||
.title_style(_DEFAULT_STYLE)
|
||||
// the following methods are no longer const because they use Into<Style>
|
||||
// .style(_DEFAULT_STYLE) // no longer const
|
||||
// .border_style(_DEFAULT_STYLE) // no longer const
|
||||
// .title_style(_DEFAULT_STYLE) // no longer const
|
||||
.title_alignment(Alignment::Left)
|
||||
.title_position(Position::Top)
|
||||
.borders(Borders::ALL)
|
||||
.border_style(_DEFAULT_STYLE)
|
||||
.style(_DEFAULT_STYLE)
|
||||
.padding(_DEFAULT_PADDING);
|
||||
}
|
||||
|
||||
/// This test ensures that we have some coverage on the Style::from() implementations
|
||||
#[test]
|
||||
fn block_style() {
|
||||
// nominal style
|
||||
let block = Block::default().style(Style::new().red());
|
||||
assert_eq!(block.style, Style::new().red());
|
||||
|
||||
// auto-convert from Color
|
||||
let block = Block::default().style(Color::Red);
|
||||
assert_eq!(block.style, Style::new().red());
|
||||
|
||||
// auto-convert from (Color, Color)
|
||||
let block = Block::default().style((Color::Red, Color::Blue));
|
||||
assert_eq!(block.style, Style::new().red().on_blue());
|
||||
|
||||
// auto-convert from Modifier
|
||||
let block = Block::default().style(Modifier::BOLD | Modifier::ITALIC);
|
||||
assert_eq!(block.style, Style::new().bold().italic());
|
||||
|
||||
// auto-convert from (Modifier, Modifier)
|
||||
let block = Block::default().style((Modifier::BOLD | Modifier::ITALIC, Modifier::DIM));
|
||||
assert_eq!(block.style, Style::new().bold().italic().not_dim());
|
||||
|
||||
// auto-convert from (Color, Modifier)
|
||||
let block = Block::default().style((Color::Red, Modifier::BOLD));
|
||||
assert_eq!(block.style, Style::new().red().bold());
|
||||
|
||||
// auto-convert from (Color, Color, Modifier)
|
||||
let block = Block::default().style((Color::Red, Color::Blue, Modifier::BOLD));
|
||||
assert_eq!(block.style, Style::new().red().on_blue().bold());
|
||||
|
||||
// auto-convert from (Color, Color, Modifier, Modifier)
|
||||
let block = Block::default().style((
|
||||
Color::Red,
|
||||
Color::Blue,
|
||||
Modifier::BOLD | Modifier::ITALIC,
|
||||
Modifier::DIM,
|
||||
));
|
||||
assert_eq!(
|
||||
block.style,
|
||||
Style::new().red().on_blue().bold().italic().not_dim()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_be_stylized() {
|
||||
let block = Block::default().black().on_white().bold().not_dim();
|
||||
|
||||
@@ -13,18 +13,15 @@ use std::collections::HashMap;
|
||||
use time::{Date, Duration, OffsetDateTime};
|
||||
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::Style,
|
||||
text::Span,
|
||||
prelude::*,
|
||||
widgets::{Block, Widget},
|
||||
};
|
||||
|
||||
/// Display a month calendar for the month containing `display_date`
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Monthly<'a, S: DateStyler> {
|
||||
pub struct Monthly<'a, DS: DateStyler> {
|
||||
display_date: Date,
|
||||
events: S,
|
||||
events: DS,
|
||||
show_surrounding: Option<Style>,
|
||||
show_weekday: Option<Style>,
|
||||
show_month: Option<Style>,
|
||||
@@ -32,9 +29,9 @@ pub struct Monthly<'a, S: DateStyler> {
|
||||
block: Option<Block<'a>>,
|
||||
}
|
||||
|
||||
impl<'a, S: DateStyler> Monthly<'a, S> {
|
||||
impl<'a, DS: DateStyler> Monthly<'a, DS> {
|
||||
/// Construct a calendar for the `display_date` and highlight the `events`
|
||||
pub fn new(display_date: Date, events: S) -> Self {
|
||||
pub fn new(display_date: Date, events: DS) -> Self {
|
||||
Self {
|
||||
display_date,
|
||||
events,
|
||||
@@ -49,32 +46,44 @@ impl<'a, S: DateStyler> Monthly<'a, S> {
|
||||
/// Fill the calendar slots for days not in the current month also, this causes each line to be
|
||||
/// completely filled. If there is an event style for a date, this style will be patched with
|
||||
/// the event's style
|
||||
pub fn show_surrounding(mut self, style: Style) -> Self {
|
||||
self.show_surrounding = Some(style);
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
pub fn show_surrounding<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.show_surrounding = Some(style.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Display a header containing weekday abbreviations
|
||||
pub fn show_weekdays_header(mut self, style: Style) -> Self {
|
||||
self.show_weekday = Some(style);
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
pub fn show_weekdays_header<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.show_weekday = Some(style.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Display a header containing the month and year
|
||||
pub fn show_month_header(mut self, style: Style) -> Self {
|
||||
self.show_month = Some(style);
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
pub fn show_month_header<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.show_month = Some(style.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// How to render otherwise unstyled dates
|
||||
pub fn default_style(mut self, s: Style) -> Self {
|
||||
self.default_style = s;
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
pub fn default_style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.default_style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Render the calendar within a [Block]
|
||||
pub fn block(mut self, b: Block<'a>) -> Self {
|
||||
self.block = Some(b);
|
||||
pub fn block(mut self, block: Block<'a>) -> Self {
|
||||
self.block = Some(block);
|
||||
self
|
||||
}
|
||||
|
||||
@@ -108,7 +117,7 @@ impl<'a, S: DateStyler> Monthly<'a, S> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, S: DateStyler> Widget for Monthly<'a, S> {
|
||||
impl<'a, DS: DateStyler> Widget for Monthly<'a, DS> {
|
||||
fn render(mut self, area: Rect, buf: &mut Buffer) {
|
||||
// Block is used for borders and such
|
||||
// Draw that first, and use the blank area inside the block for our own purposes
|
||||
@@ -178,16 +187,22 @@ pub struct CalendarEventStore(pub HashMap<Date, Style>);
|
||||
|
||||
impl CalendarEventStore {
|
||||
/// Construct a store that has the current date styled.
|
||||
pub fn today(style: Style) -> Self {
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
pub fn today<S: Into<Style>>(style: S) -> Self {
|
||||
let mut res = Self::default();
|
||||
res.add(OffsetDateTime::now_local().unwrap().date(), style);
|
||||
res.add(OffsetDateTime::now_local().unwrap().date(), style.into());
|
||||
res
|
||||
}
|
||||
|
||||
/// Add a date and style to the store
|
||||
pub fn add(&mut self, date: Date, style: Style) {
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
pub fn add<S: Into<Style>>(&mut self, date: Date, style: S) {
|
||||
// to simplify style nonsense, last write wins
|
||||
let _ = self.0.insert(date, style);
|
||||
let _ = self.0.insert(date, style.into());
|
||||
}
|
||||
|
||||
/// Helper for trait impls
|
||||
|
||||
@@ -457,7 +457,13 @@ impl<'a> Context<'a> {
|
||||
/// ```
|
||||
/// use ratatui::{prelude::*, widgets::canvas::*};
|
||||
///
|
||||
/// let ctx = Context::new(100, 100, [-180.0, 180.0], [-90.0, 90.0], symbols::Marker::Braille);
|
||||
/// let ctx = Context::new(
|
||||
/// 100,
|
||||
/// 100,
|
||||
/// [-180.0, 180.0],
|
||||
/// [-90.0, 90.0],
|
||||
/// symbols::Marker::Braille,
|
||||
/// );
|
||||
/// ```
|
||||
pub fn new(
|
||||
width: u16,
|
||||
@@ -554,7 +560,10 @@ impl<'a> Context<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::{style::Color, widgets::{*, canvas::*}};
|
||||
/// use ratatui::{
|
||||
/// style::Color,
|
||||
/// widgets::{canvas::*, *},
|
||||
/// };
|
||||
///
|
||||
/// Canvas::default()
|
||||
/// .block(Block::default().title("Canvas").borders(Borders::ALL))
|
||||
@@ -563,7 +572,7 @@ impl<'a> Context<'a> {
|
||||
/// .paint(|ctx| {
|
||||
/// ctx.draw(&Map {
|
||||
/// resolution: MapResolution::High,
|
||||
/// color: Color::White
|
||||
/// color: Color::White,
|
||||
/// });
|
||||
/// ctx.layer();
|
||||
/// ctx.draw(&Line {
|
||||
@@ -578,7 +587,7 @@ impl<'a> Context<'a> {
|
||||
/// y: 20.0,
|
||||
/// width: 10.0,
|
||||
/// height: 10.0,
|
||||
/// color: Color::Red
|
||||
/// color: Color::Red,
|
||||
/// });
|
||||
/// });
|
||||
/// ```
|
||||
@@ -666,10 +675,18 @@ where
|
||||
/// ```
|
||||
/// use ratatui::{prelude::*, widgets::canvas::*};
|
||||
///
|
||||
/// Canvas::default().marker(symbols::Marker::Braille).paint(|ctx| {});
|
||||
/// Canvas::default().marker(symbols::Marker::HalfBlock).paint(|ctx| {});
|
||||
/// Canvas::default().marker(symbols::Marker::Dot).paint(|ctx| {});
|
||||
/// Canvas::default().marker(symbols::Marker::Block).paint(|ctx| {});
|
||||
/// Canvas::default()
|
||||
/// .marker(symbols::Marker::Braille)
|
||||
/// .paint(|ctx| {});
|
||||
/// Canvas::default()
|
||||
/// .marker(symbols::Marker::HalfBlock)
|
||||
/// .paint(|ctx| {});
|
||||
/// Canvas::default()
|
||||
/// .marker(symbols::Marker::Dot)
|
||||
/// .paint(|ctx| {});
|
||||
/// Canvas::default()
|
||||
/// .marker(symbols::Marker::Block)
|
||||
/// .paint(|ctx| {});
|
||||
/// ```
|
||||
pub fn marker(mut self, marker: symbols::Marker) -> Canvas<'a, F> {
|
||||
self.marker = marker;
|
||||
|
||||
@@ -128,7 +128,7 @@ mod tests {
|
||||
|
||||
let mut expected = Buffer::with_lines(expected_lines);
|
||||
for cell in expected.content.iter_mut() {
|
||||
if cell.symbol == "•" {
|
||||
if cell.symbol() == "•" {
|
||||
cell.set_style(Style::new().red());
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -66,6 +66,7 @@ impl<'a> Gauge<'a> {
|
||||
///
|
||||
/// The gauge is rendered in the inner portion of the block once space for borders and padding
|
||||
/// is reserved. Styles set on the block do **not** affect the bar itself.
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn block(mut self, block: Block<'a>) -> Gauge<'a> {
|
||||
self.block = Some(block);
|
||||
self
|
||||
@@ -80,6 +81,7 @@ impl<'a> Gauge<'a> {
|
||||
/// # See also
|
||||
///
|
||||
/// See [`Gauge::ratio`] to set from a float.
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn percent(mut self, percent: u16) -> Gauge<'a> {
|
||||
assert!(
|
||||
percent <= 100,
|
||||
@@ -101,6 +103,7 @@ impl<'a> Gauge<'a> {
|
||||
/// # See also
|
||||
///
|
||||
/// See [`Gauge::percent`] to set from a percentage.
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn ratio(mut self, ratio: f64) -> Gauge<'a> {
|
||||
assert!(
|
||||
(0.0..=1.0).contains(&ratio),
|
||||
@@ -114,6 +117,7 @@ impl<'a> Gauge<'a> {
|
||||
///
|
||||
/// For a left-aligned label, see [`LineGauge`].
|
||||
/// If the label is not defined, it is the percentage filled.
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn label<T>(mut self, label: T) -> Gauge<'a>
|
||||
where
|
||||
T: Into<Span<'a>>,
|
||||
@@ -124,16 +128,24 @@ impl<'a> Gauge<'a> {
|
||||
|
||||
/// Sets the widget style.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// This will style the block (if any non-styled) and background of the widget (everything
|
||||
/// except the bar itself). [`Block`] style set with [`Gauge::block`] takes precedence.
|
||||
pub fn style(mut self, style: Style) -> Gauge<'a> {
|
||||
self.style = style;
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> Gauge<'a> {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the bar.
|
||||
pub fn gauge_style(mut self, style: Style) -> Gauge<'a> {
|
||||
self.gauge_style = style;
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn gauge_style<S: Into<Style>>(mut self, style: S) -> Gauge<'a> {
|
||||
self.gauge_style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
@@ -142,6 +154,7 @@ impl<'a> Gauge<'a> {
|
||||
/// This enables the use of
|
||||
/// [unicode block characters](https://en.wikipedia.org/wiki/Block_Elements).
|
||||
/// This is useful to display a higher precision bar (8 extra fractional parts per cell).
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn use_unicode(mut self, unicode: bool) -> Gauge<'a> {
|
||||
self.use_unicode = unicode;
|
||||
self
|
||||
@@ -265,6 +278,7 @@ pub struct LineGauge<'a> {
|
||||
|
||||
impl<'a> LineGauge<'a> {
|
||||
/// Surrounds the `LineGauge` with a [`Block`].
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn block(mut self, block: Block<'a>) -> Self {
|
||||
self.block = Some(block);
|
||||
self
|
||||
@@ -278,6 +292,7 @@ impl<'a> LineGauge<'a> {
|
||||
/// # Panics
|
||||
///
|
||||
/// This method panics if `ratio` is **not** between 0 and 1 inclusively.
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn ratio(mut self, ratio: f64) -> Self {
|
||||
assert!(
|
||||
(0.0..=1.0).contains(&ratio),
|
||||
@@ -294,6 +309,7 @@ impl<'a> LineGauge<'a> {
|
||||
/// See [`symbols::line::Set`] for more information. Predefined sets are also available, see
|
||||
/// [`NORMAL`](symbols::line::NORMAL), [`DOUBLE`](symbols::line::DOUBLE) and
|
||||
/// [`THICK`](symbols::line::THICK).
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn line_set(mut self, set: symbols::line::Set) -> Self {
|
||||
self.line_set = set;
|
||||
self
|
||||
@@ -313,16 +329,24 @@ impl<'a> LineGauge<'a> {
|
||||
|
||||
/// Sets the widget style.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// This will style everything except the bar itself, so basically the block (if any) and
|
||||
/// background.
|
||||
pub fn style(mut self, style: Style) -> Self {
|
||||
self.style = style;
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the bar.
|
||||
pub fn gauge_style(mut self, style: Style) -> Self {
|
||||
self.gauge_style = style;
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn gauge_style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.gauge_style = style.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -394,7 +418,7 @@ impl<'a> Styled for Gauge<'a> {
|
||||
self.style
|
||||
}
|
||||
|
||||
fn set_style(self, style: Style) -> Self::Item {
|
||||
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
|
||||
self.style(style)
|
||||
}
|
||||
}
|
||||
@@ -406,7 +430,7 @@ impl<'a> Styled for LineGauge<'a> {
|
||||
self.style
|
||||
}
|
||||
|
||||
fn set_style(self, style: Style) -> Self::Item {
|
||||
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
|
||||
self.style(style)
|
||||
}
|
||||
}
|
||||
@@ -419,19 +443,19 @@ mod tests {
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn gauge_invalid_percentage() {
|
||||
Gauge::default().percent(110);
|
||||
let _ = Gauge::default().percent(110);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn gauge_invalid_ratio_upper_bound() {
|
||||
Gauge::default().ratio(1.1);
|
||||
let _ = Gauge::default().ratio(1.1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn gauge_invalid_ratio_lower_bound() {
|
||||
Gauge::default().ratio(-0.5);
|
||||
let _ = Gauge::default().ratio(-0.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,10 @@
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Rect},
|
||||
style::{Style, Styled},
|
||||
text::{StyledGrapheme, Text},
|
||||
prelude::*,
|
||||
text::StyledGrapheme,
|
||||
widgets::{
|
||||
reflow::{LineComposer, LineTruncator, WordWrapper},
|
||||
reflow::{LineComposer, LineTruncator, WordWrapper, WrappedLine},
|
||||
Block, Widget,
|
||||
},
|
||||
};
|
||||
@@ -29,16 +27,14 @@ fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Alignment)
|
||||
/// let text = vec![
|
||||
/// Line::from(vec![
|
||||
/// Span::raw("First"),
|
||||
/// Span::styled("line",Style::new().green().italic()),
|
||||
/// Span::styled("line", Style::new().green().italic()),
|
||||
/// ".".into(),
|
||||
/// ]),
|
||||
/// Line::from("Second line".red()),
|
||||
/// "Third line".into(),
|
||||
/// ];
|
||||
/// Paragraph::new(text)
|
||||
/// .block(Block::new()
|
||||
/// .title("Paragraph")
|
||||
/// .borders(Borders::ALL))
|
||||
/// .block(Block::new().title("Paragraph").borders(Borders::ALL))
|
||||
/// .style(Style::new().white().on_black())
|
||||
/// .alignment(Alignment::Center)
|
||||
/// .wrap(Wrap { trim: true });
|
||||
@@ -66,9 +62,11 @@ pub struct Paragraph<'a> {
|
||||
/// ```
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
///
|
||||
/// let bullet_points = Text::from(r#"Some indented points:
|
||||
/// let bullet_points = Text::from(
|
||||
/// r#"Some indented points:
|
||||
/// - First thing goes here and is long so that it wraps
|
||||
/// - Here is another point that is long enough to wrap"#);
|
||||
/// - Here is another point that is long enough to wrap"#,
|
||||
/// );
|
||||
///
|
||||
/// // With leading spaces trimmed (window width of 30 chars):
|
||||
/// Paragraph::new(bullet_points.clone()).wrap(Wrap { trim: true });
|
||||
@@ -108,10 +106,8 @@ impl<'a> Paragraph<'a> {
|
||||
/// let paragraph = Paragraph::new("Hello, world!");
|
||||
/// let paragraph = Paragraph::new(String::from("Hello, world!"));
|
||||
/// let paragraph = Paragraph::new(Text::raw("Hello, world!"));
|
||||
/// let paragraph = Paragraph::new(
|
||||
/// Text::styled("Hello, world!", Style::default()));
|
||||
/// let paragraph = Paragraph::new(
|
||||
/// Line::from(vec!["Hello, ".into(), "world!".red()]));
|
||||
/// let paragraph = Paragraph::new(Text::styled("Hello, world!", Style::default()));
|
||||
/// let paragraph = Paragraph::new(Line::from(vec!["Hello, ".into(), "world!".red()]));
|
||||
/// ```
|
||||
pub fn new<T>(text: T) -> Paragraph<'a>
|
||||
where
|
||||
@@ -134,10 +130,9 @@ impl<'a> Paragraph<'a> {
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let paragraph = Paragraph::new("Hello, world!")
|
||||
/// .block(Block::default()
|
||||
/// .title("Paragraph")
|
||||
/// .borders(Borders::ALL));
|
||||
/// .block(Block::default().title("Paragraph").borders(Borders::ALL));
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn block(mut self, block: Block<'a>) -> Paragraph<'a> {
|
||||
self.block = Some(block);
|
||||
self
|
||||
@@ -145,6 +140,9 @@ impl<'a> Paragraph<'a> {
|
||||
|
||||
/// Sets the style of the entire widget.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// This applies to the entire widget, including the block if one is present. Any style set on
|
||||
/// the block or text will be added to this style.
|
||||
///
|
||||
@@ -152,11 +150,11 @@ impl<'a> Paragraph<'a> {
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let paragraph = Paragraph::new("Hello, world!")
|
||||
/// .style(Style::new().red().on_white());
|
||||
/// let paragraph = Paragraph::new("Hello, world!").style(Style::new().red().on_white());
|
||||
/// ```
|
||||
pub fn style(mut self, style: Style) -> Paragraph<'a> {
|
||||
self.style = style;
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> Paragraph<'a> {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
@@ -168,9 +166,9 @@ impl<'a> Paragraph<'a> {
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let paragraph = Paragraph::new("Hello, world!")
|
||||
/// .wrap(Wrap { trim: true });
|
||||
/// let paragraph = Paragraph::new("Hello, world!").wrap(Wrap { trim: true });
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn wrap(mut self, wrap: Wrap) -> Paragraph<'a> {
|
||||
self.wrap = Some(wrap);
|
||||
self
|
||||
@@ -187,6 +185,7 @@ impl<'a> Paragraph<'a> {
|
||||
///
|
||||
/// For more information about future scrolling design and concerns, see [RFC: Design of
|
||||
/// Scrollable Widgets](https://github.com/ratatui-org/ratatui/issues/174) on GitHub.
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn scroll(mut self, offset: (Vertical, Horizontal)) -> Paragraph<'a> {
|
||||
self.scroll = offset;
|
||||
self
|
||||
@@ -201,13 +200,84 @@ impl<'a> Paragraph<'a> {
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let paragraph = Paragraph::new("Hello World")
|
||||
/// .alignment(Alignment::Center);
|
||||
/// let paragraph = Paragraph::new("Hello World").alignment(Alignment::Center);
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn alignment(mut self, alignment: Alignment) -> Paragraph<'a> {
|
||||
self.alignment = alignment;
|
||||
self
|
||||
}
|
||||
|
||||
/// Calculates the number of lines needed to fully render.
|
||||
///
|
||||
/// Given a max line width, this method calculates the number of lines that a paragraph will
|
||||
/// need in order to be fully rendered. For paragraphs that do not use wrapping, this count is
|
||||
/// simply the number of lines present in the paragraph.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```ignore
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let paragraph = Paragraph::new("Hello World")
|
||||
/// .wrap(Wrap { trim: false });
|
||||
/// assert_eq!(paragraph.line_count(20), 1);
|
||||
/// assert_eq!(paragraph.line_count(10), 2);
|
||||
/// ```
|
||||
#[stability::unstable(
|
||||
feature = "rendered-line-info",
|
||||
reason = "The design for text wrapping is not stable and might affect this API.",
|
||||
issue = "https://github.com/ratatui-org/ratatui/issues/293"
|
||||
)]
|
||||
pub fn line_count(&self, width: u16) -> usize {
|
||||
if width < 1 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if let Some(Wrap { trim }) = self.wrap {
|
||||
let styled = self.text.lines.iter().map(|line| {
|
||||
let graphemes = line
|
||||
.spans
|
||||
.iter()
|
||||
.flat_map(|span| span.styled_graphemes(self.style));
|
||||
let alignment = line.alignment.unwrap_or(self.alignment);
|
||||
(graphemes, alignment)
|
||||
});
|
||||
let mut line_composer = WordWrapper::new(styled, width, trim);
|
||||
let mut count = 0;
|
||||
while line_composer.next_line().is_some() {
|
||||
count += 1;
|
||||
}
|
||||
count
|
||||
} else {
|
||||
self.text.lines.len()
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculates the shortest line width needed to avoid any word being wrapped or truncated.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```ignore
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let paragraph = Paragraph::new("Hello World");
|
||||
/// assert_eq!(paragraph.line_width(), 11);
|
||||
///
|
||||
/// let paragraph = Paragraph::new("Hello World\nhi\nHello World!!!");
|
||||
/// assert_eq!(paragraph.line_width(), 14);
|
||||
/// ```
|
||||
#[stability::unstable(
|
||||
feature = "rendered-line-info",
|
||||
reason = "The design for text wrapping is not stable and might affect this API.",
|
||||
issue = "https://github.com/ratatui-org/ratatui/issues/293"
|
||||
)]
|
||||
pub fn line_width(&self) -> usize {
|
||||
self.text
|
||||
.lines
|
||||
.iter()
|
||||
.map(|l| l.width())
|
||||
.max()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for Paragraph<'a> {
|
||||
@@ -226,49 +296,53 @@ impl<'a> Widget for Paragraph<'a> {
|
||||
return;
|
||||
}
|
||||
|
||||
let style = self.style;
|
||||
let styled = self.text.lines.iter().map(|line| {
|
||||
(
|
||||
line.spans
|
||||
.iter()
|
||||
.flat_map(|span| span.styled_graphemes(style)),
|
||||
line.alignment.unwrap_or(self.alignment),
|
||||
)
|
||||
let graphemes = line
|
||||
.spans
|
||||
.iter()
|
||||
.flat_map(|span| span.styled_graphemes(self.style));
|
||||
let alignment = line.alignment.unwrap_or(self.alignment);
|
||||
(graphemes, alignment)
|
||||
});
|
||||
|
||||
let mut line_composer: Box<dyn LineComposer> = if let Some(Wrap { trim }) = self.wrap {
|
||||
Box::new(WordWrapper::new(styled, text_area.width, trim))
|
||||
if let Some(Wrap { trim }) = self.wrap {
|
||||
let line_composer = WordWrapper::new(styled, text_area.width, trim);
|
||||
self.render_text(line_composer, text_area, buf);
|
||||
} else {
|
||||
let mut line_composer = Box::new(LineTruncator::new(styled, text_area.width));
|
||||
let mut line_composer = LineTruncator::new(styled, text_area.width);
|
||||
line_composer.set_horizontal_offset(self.scroll.1);
|
||||
line_composer
|
||||
};
|
||||
self.render_text(line_composer, text_area, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Paragraph<'a> {
|
||||
fn render_text<C: LineComposer<'a>>(&self, mut composer: C, area: Rect, buf: &mut Buffer) {
|
||||
let mut y = 0;
|
||||
while let Some((current_line, current_line_width, current_line_alignment)) =
|
||||
line_composer.next_line()
|
||||
while let Some(WrappedLine {
|
||||
line: current_line,
|
||||
width: current_line_width,
|
||||
alignment: current_line_alignment,
|
||||
}) = composer.next_line()
|
||||
{
|
||||
if y >= self.scroll.0 {
|
||||
let mut x =
|
||||
get_line_offset(current_line_width, text_area.width, current_line_alignment);
|
||||
let mut x = get_line_offset(current_line_width, area.width, current_line_alignment);
|
||||
for StyledGrapheme { symbol, style } in current_line {
|
||||
let width = symbol.width();
|
||||
if width == 0 {
|
||||
continue;
|
||||
}
|
||||
buf.get_mut(text_area.left() + x, text_area.top() + y - self.scroll.0)
|
||||
.set_symbol(if symbol.is_empty() {
|
||||
// If the symbol is empty, the last char which rendered last time will
|
||||
// leave on the line. It's a quick fix.
|
||||
" "
|
||||
} else {
|
||||
symbol
|
||||
})
|
||||
// If the symbol is empty, the last char which rendered last time will
|
||||
// leave on the line. It's a quick fix.
|
||||
let symbol = if symbol.is_empty() { " " } else { symbol };
|
||||
buf.get_mut(area.left() + x, area.top() + y - self.scroll.0)
|
||||
.set_symbol(symbol)
|
||||
.set_style(*style);
|
||||
x += width as u16;
|
||||
}
|
||||
}
|
||||
y += 1;
|
||||
if y >= text_area.height + self.scroll.0 {
|
||||
if y >= area.height + self.scroll.0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -282,7 +356,7 @@ impl<'a> Styled for Paragraph<'a> {
|
||||
self.style
|
||||
}
|
||||
|
||||
fn set_style(self, style: Style) -> Self::Item {
|
||||
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
|
||||
self.style(style)
|
||||
}
|
||||
}
|
||||
@@ -294,7 +368,7 @@ mod test {
|
||||
backend::TestBackend,
|
||||
style::{Color, Modifier, Stylize},
|
||||
text::{Line, Span},
|
||||
widgets::Borders,
|
||||
widgets::{block::Position, Borders},
|
||||
Terminal,
|
||||
};
|
||||
|
||||
@@ -477,6 +551,20 @@ mod test {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_paragraph_with_block_with_bottom_title_and_border() {
|
||||
let block = Block::default()
|
||||
.title("Title")
|
||||
.title_position(Position::Bottom)
|
||||
.borders(Borders::BOTTOM);
|
||||
let paragraph = Paragraph::new("Hello, world!").block(block);
|
||||
|
||||
test_case(
|
||||
¶graph,
|
||||
Buffer::with_lines(vec!["Hello, world! ", "Title──────────"]),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_paragraph_with_word_wrap() {
|
||||
let text = "This is a long line of text that should wrap and contains a superultramegagigalong word.";
|
||||
@@ -792,4 +880,46 @@ mod test {
|
||||
.remove_modifier(Modifier::DIM)
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_paragraph_count_rendered_lines() {
|
||||
let paragraph = Paragraph::new("Hello World");
|
||||
assert_eq!(paragraph.line_count(20), 1);
|
||||
assert_eq!(paragraph.line_count(10), 1);
|
||||
let paragraph = Paragraph::new("Hello World").wrap(Wrap { trim: false });
|
||||
assert_eq!(paragraph.line_count(20), 1);
|
||||
assert_eq!(paragraph.line_count(10), 2);
|
||||
let paragraph = Paragraph::new("Hello World").wrap(Wrap { trim: true });
|
||||
assert_eq!(paragraph.line_count(20), 1);
|
||||
assert_eq!(paragraph.line_count(10), 2);
|
||||
|
||||
let text = "Hello World ".repeat(100);
|
||||
let paragraph = Paragraph::new(text.trim());
|
||||
assert_eq!(paragraph.line_count(11), 1);
|
||||
assert_eq!(paragraph.line_count(6), 1);
|
||||
let paragraph = paragraph.wrap(Wrap { trim: false });
|
||||
assert_eq!(paragraph.line_count(11), 100);
|
||||
assert_eq!(paragraph.line_count(6), 200);
|
||||
let paragraph = paragraph.wrap(Wrap { trim: true });
|
||||
assert_eq!(paragraph.line_count(11), 100);
|
||||
assert_eq!(paragraph.line_count(6), 200);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_paragraph_line_width() {
|
||||
let paragraph = Paragraph::new("Hello World");
|
||||
assert_eq!(paragraph.line_width(), 11);
|
||||
let paragraph = Paragraph::new("Hello World").wrap(Wrap { trim: false });
|
||||
assert_eq!(paragraph.line_width(), 11);
|
||||
let paragraph = Paragraph::new("Hello World").wrap(Wrap { trim: true });
|
||||
assert_eq!(paragraph.line_width(), 11);
|
||||
|
||||
let text = "Hello World ".repeat(100);
|
||||
let paragraph = Paragraph::new(text);
|
||||
assert_eq!(paragraph.line_width(), 1200);
|
||||
let paragraph = paragraph.wrap(Wrap { trim: false });
|
||||
assert_eq!(paragraph.line_width(), 1200);
|
||||
let paragraph = paragraph.wrap(Wrap { trim: true });
|
||||
assert_eq!(paragraph.line_width(), 1200);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,16 @@ const NBSP: &str = "\u{00a0}";
|
||||
/// Cannot implement it as Iterator since it yields slices of the internal buffer (need streaming
|
||||
/// iterators for that).
|
||||
pub trait LineComposer<'a> {
|
||||
fn next_line(&mut self) -> Option<(&[StyledGrapheme<'a>], u16, Alignment)>;
|
||||
fn next_line<'lend>(&'lend mut self) -> Option<WrappedLine<'lend, 'a>>;
|
||||
}
|
||||
|
||||
pub struct WrappedLine<'lend, 'text> {
|
||||
/// One line reflowed to the correct width
|
||||
pub line: &'lend [StyledGrapheme<'text>],
|
||||
/// The width of the line
|
||||
pub width: u16,
|
||||
/// Whether the line was aligned left or right
|
||||
pub alignment: Alignment,
|
||||
}
|
||||
|
||||
/// A state machine that wraps lines on word boundaries.
|
||||
@@ -56,7 +65,7 @@ where
|
||||
O: Iterator<Item = (I, Alignment)>,
|
||||
I: Iterator<Item = StyledGrapheme<'a>>,
|
||||
{
|
||||
fn next_line(&mut self) -> Option<(&[StyledGrapheme<'a>], u16, Alignment)> {
|
||||
fn next_line<'lend>(&'lend mut self) -> Option<WrappedLine<'lend, 'a>> {
|
||||
if self.max_line_width == 0 {
|
||||
return None;
|
||||
}
|
||||
@@ -200,7 +209,11 @@ where
|
||||
|
||||
if let Some(line) = current_line {
|
||||
self.current_line = line;
|
||||
Some((&self.current_line[..], line_width, self.current_alignment))
|
||||
Some(WrappedLine {
|
||||
line: &self.current_line[..],
|
||||
width: line_width,
|
||||
alignment: self.current_alignment,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -249,7 +262,7 @@ where
|
||||
O: Iterator<Item = (I, Alignment)>,
|
||||
I: Iterator<Item = StyledGrapheme<'a>>,
|
||||
{
|
||||
fn next_line(&mut self) -> Option<(&[StyledGrapheme<'a>], u16, Alignment)> {
|
||||
fn next_line<'lend>(&'lend mut self) -> Option<WrappedLine<'lend, 'a>> {
|
||||
if self.max_line_width == 0 {
|
||||
return None;
|
||||
}
|
||||
@@ -296,11 +309,11 @@ where
|
||||
if lines_exhausted {
|
||||
None
|
||||
} else {
|
||||
Some((
|
||||
&self.current_line[..],
|
||||
current_line_width,
|
||||
current_alignment,
|
||||
))
|
||||
Some(WrappedLine {
|
||||
line: &self.current_line[..],
|
||||
width: current_line_width,
|
||||
alignment: current_alignment,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -360,7 +373,12 @@ mod test {
|
||||
let mut lines = vec![];
|
||||
let mut widths = vec![];
|
||||
let mut alignments = vec![];
|
||||
while let Some((styled, width, alignment)) = composer.next_line() {
|
||||
while let Some(WrappedLine {
|
||||
line: styled,
|
||||
width,
|
||||
alignment,
|
||||
}) = composer.next_line()
|
||||
{
|
||||
let line = styled
|
||||
.iter()
|
||||
.map(|StyledGrapheme { symbol, .. }| *symbol)
|
||||
|
||||
@@ -2,9 +2,7 @@ use strum::{Display, EnumString};
|
||||
|
||||
use super::StatefulWidget;
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::Style,
|
||||
prelude::*,
|
||||
symbols::scrollbar::{Set, DOUBLE_HORIZONTAL, DOUBLE_VERTICAL},
|
||||
};
|
||||
|
||||
@@ -62,18 +60,21 @@ impl ScrollbarState {
|
||||
}
|
||||
}
|
||||
/// Sets the scroll position of the scrollbar and returns the modified ScrollbarState.
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn position(mut self, position: usize) -> Self {
|
||||
self.position = position;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the length of the scrollable content and returns the modified ScrollbarState.
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn content_length(mut self, content_length: usize) -> Self {
|
||||
self.content_length = content_length;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the length of the viewport content and returns the modified ScrollbarState.
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn viewport_content_length(mut self, viewport_content_length: usize) -> Self {
|
||||
self.viewport_content_length = viewport_content_length;
|
||||
self
|
||||
@@ -147,7 +148,11 @@ pub enum ScrollbarOrientation {
|
||||
///
|
||||
/// let vertical_scroll = 0; // from app state
|
||||
///
|
||||
/// let items = vec![Line::from("Item 1"), Line::from("Item 2"), Line::from("Item 3")];
|
||||
/// let items = vec![
|
||||
/// Line::from("Item 1"),
|
||||
/// Line::from("Item 2"),
|
||||
/// Line::from("Item 3"),
|
||||
/// ];
|
||||
/// let paragraph = Paragraph::new(items.clone())
|
||||
/// .scroll((vertical_scroll as u16, 0))
|
||||
/// .block(Block::new().borders(Borders::RIGHT)); // to show a background for the scrollbar
|
||||
@@ -160,12 +165,14 @@ pub enum ScrollbarOrientation {
|
||||
///
|
||||
/// let area = frame.size();
|
||||
/// frame.render_widget(paragraph, area);
|
||||
/// frame.render_stateful_widget(scrollbar,
|
||||
/// frame.render_stateful_widget(
|
||||
/// scrollbar,
|
||||
/// area.inner(&Margin {
|
||||
/// vertical: 1,
|
||||
/// horizontal: 0,
|
||||
/// }), // using a inner vertical margin of 1 unit makes the scrollbar inside the block
|
||||
/// &mut scrollbar_state);
|
||||
/// &mut scrollbar_state,
|
||||
/// );
|
||||
/// # }
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
@@ -204,6 +211,7 @@ impl<'a> Scrollbar<'a> {
|
||||
|
||||
/// Sets the orientation of the scrollbar.
|
||||
/// Resets the symbols to [`DOUBLE_VERTICAL`] or [`DOUBLE_HORIZONTAL`] based on orientation
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn orientation(mut self, orientation: ScrollbarOrientation) -> Self {
|
||||
self.orientation = orientation;
|
||||
let set = if self.is_vertical() {
|
||||
@@ -215,56 +223,77 @@ impl<'a> Scrollbar<'a> {
|
||||
}
|
||||
|
||||
/// Sets the orientation and symbols for the scrollbar from a [`Set`].
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn orientation_and_symbol(mut self, orientation: ScrollbarOrientation, set: Set) -> Self {
|
||||
self.orientation = orientation;
|
||||
self.symbols(set)
|
||||
}
|
||||
|
||||
/// Sets the symbol that represents the thumb of the scrollbar.
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn thumb_symbol(mut self, thumb_symbol: &'a str) -> Self {
|
||||
self.thumb_symbol = thumb_symbol;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style that represents the thumb of the scrollbar.
|
||||
pub fn thumb_style(mut self, thumb_style: Style) -> Self {
|
||||
self.thumb_style = thumb_style;
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn thumb_style<S: Into<Style>>(mut self, thumb_style: S) -> Self {
|
||||
self.thumb_style = thumb_style.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the symbol that represents the track of the scrollbar.
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn track_symbol(mut self, track_symbol: Option<&'a str>) -> Self {
|
||||
self.track_symbol = track_symbol;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style that is used for the track of the scrollbar.
|
||||
pub fn track_style(mut self, track_style: Style) -> Self {
|
||||
self.track_style = track_style;
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn track_style<S: Into<Style>>(mut self, track_style: S) -> Self {
|
||||
self.track_style = track_style.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the symbol that represents the beginning of the scrollbar.
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn begin_symbol(mut self, begin_symbol: Option<&'a str>) -> Self {
|
||||
self.begin_symbol = begin_symbol;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style that is used for the beginning of the scrollbar.
|
||||
pub fn begin_style(mut self, begin_style: Style) -> Self {
|
||||
self.begin_style = begin_style;
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn begin_style<S: Into<Style>>(mut self, begin_style: S) -> Self {
|
||||
self.begin_style = begin_style.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the symbol that represents the end of the scrollbar.
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn end_symbol(mut self, end_symbol: Option<&'a str>) -> Self {
|
||||
self.end_symbol = end_symbol;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style that is used for the end of the scrollbar.
|
||||
pub fn end_style(mut self, end_style: Style) -> Self {
|
||||
self.end_style = end_style;
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn end_style<S: Into<Style>>(mut self, end_style: S) -> Self {
|
||||
self.end_style = end_style.into();
|
||||
self
|
||||
}
|
||||
|
||||
@@ -281,6 +310,7 @@ impl<'a> Scrollbar<'a> {
|
||||
///
|
||||
/// Only sets begin_symbol, end_symbol and track_symbol if they already contain a value.
|
||||
/// If they were set to `None` explicitly, this function will respect that choice.
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn symbols(mut self, symbol: Set) -> Self {
|
||||
self.thumb_symbol = symbol.thumb;
|
||||
if self.track_symbol.is_some() {
|
||||
@@ -296,6 +326,10 @@ impl<'a> Scrollbar<'a> {
|
||||
}
|
||||
|
||||
/// Sets the style used for the various parts of the scrollbar from a [`Style`].
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// ```text
|
||||
/// <--▮------->
|
||||
/// ^ ^ ^ ^
|
||||
@@ -304,7 +338,9 @@ impl<'a> Scrollbar<'a> {
|
||||
/// │ └──────── thumb
|
||||
/// └─────────── begin
|
||||
/// ```
|
||||
pub fn style(mut self, style: Style) -> Self {
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
let style = style.into();
|
||||
self.track_style = style;
|
||||
self.thumb_style = style;
|
||||
self.begin_style = style;
|
||||
|
||||
@@ -1,17 +1,27 @@
|
||||
#![warn(missing_docs)]
|
||||
use std::cmp::min;
|
||||
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::{Style, Styled},
|
||||
symbols,
|
||||
prelude::*,
|
||||
widgets::{Block, Widget},
|
||||
};
|
||||
|
||||
/// Widget to render a sparkline over one or more lines.
|
||||
///
|
||||
/// You can create a `Sparkline` using [`Sparkline::default`].
|
||||
///
|
||||
/// `Sparkline` can be styled either using [`Sparkline::style`] or preferably using the methods
|
||||
/// provided by the [`Stylize`](crate::style::Stylize) trait.
|
||||
///
|
||||
/// # Setter methods
|
||||
///
|
||||
/// - [`Sparkline::block`] wraps the sparkline in a [`Block`]
|
||||
/// - [`Sparkline::data`] defines the dataset, you'll almost always want to use it
|
||||
/// - [`Sparkline::max`] sets the maximum value of bars
|
||||
/// - [`Sparkline::direction`] sets the render direction
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
@@ -21,7 +31,8 @@ use crate::{
|
||||
/// .block(Block::default().title("Sparkline").borders(Borders::ALL))
|
||||
/// .data(&[0, 2, 3, 4, 1, 4, 10])
|
||||
/// .max(5)
|
||||
/// .style(Style::default().fg(Color::Red).bg(Color::White));
|
||||
/// .direction(RenderDirection::RightToLeft)
|
||||
/// .style(Style::default().red().on_white());
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub struct Sparkline<'a> {
|
||||
@@ -40,10 +51,15 @@ pub struct Sparkline<'a> {
|
||||
direction: RenderDirection,
|
||||
}
|
||||
|
||||
/// Defines the direction in which sparkline will be rendered.
|
||||
///
|
||||
/// See [`Sparkline::direction`].
|
||||
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub enum RenderDirection {
|
||||
/// The first value is on the left, going to the right
|
||||
#[default]
|
||||
LeftToRight,
|
||||
/// The first value is on the right, going to the left
|
||||
RightToLeft,
|
||||
}
|
||||
|
||||
@@ -61,31 +77,67 @@ impl<'a> Default for Sparkline<'a> {
|
||||
}
|
||||
|
||||
impl<'a> Sparkline<'a> {
|
||||
/// Wraps the sparkline with the given `block`.
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn block(mut self, block: Block<'a>) -> Sparkline<'a> {
|
||||
self.block = Some(block);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn style(mut self, style: Style) -> Sparkline<'a> {
|
||||
self.style = style;
|
||||
/// Sets the style of the entire widget.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// The foreground corresponds to the bars while the background is everything else.
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> Sparkline<'a> {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the dataset for the sparkline.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// # fn ui(frame: &mut Frame) {
|
||||
/// # let area = Rect::default();
|
||||
/// let sparkline = Sparkline::default().data(&[1, 2, 3]);
|
||||
/// frame.render_widget(sparkline, area);
|
||||
/// # }
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn data(mut self, data: &'a [u64]) -> Sparkline<'a> {
|
||||
self.data = data;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the maximum value of bars.
|
||||
///
|
||||
/// Every bar will be scaled accordingly. If no max is given, this will be the max in the
|
||||
/// dataset.
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn max(mut self, max: u64) -> Sparkline<'a> {
|
||||
self.max = Some(max);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the characters used to display the bars.
|
||||
///
|
||||
/// Can be [`symbols::bar::THREE_LEVELS`], [`symbols::bar::NINE_LEVELS`] (default) or a custom
|
||||
/// [`Set`](symbols::bar::Set).
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn bar_set(mut self, bar_set: symbols::bar::Set) -> Sparkline<'a> {
|
||||
self.bar_set = bar_set;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the direction of the sparkline.
|
||||
///
|
||||
/// [`RenderDirection::LeftToRight`] by default.
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn direction(mut self, direction: RenderDirection) -> Sparkline<'a> {
|
||||
self.direction = direction;
|
||||
self
|
||||
@@ -99,7 +151,7 @@ impl<'a> Styled for Sparkline<'a> {
|
||||
self.style
|
||||
}
|
||||
|
||||
fn set_style(self, style: Style) -> Self::Item {
|
||||
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
|
||||
self.style(style)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,166 +1,17 @@
|
||||
#![warn(missing_docs)]
|
||||
|
||||
use strum::{Display, EnumString};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Constraint, Direction, Layout, Rect, SegmentSize},
|
||||
style::{Style, Styled},
|
||||
text::Text,
|
||||
widgets::{Block, StatefulWidget, Widget},
|
||||
};
|
||||
mod cell;
|
||||
mod row;
|
||||
#[allow(clippy::module_inception)]
|
||||
mod table;
|
||||
mod table_state;
|
||||
|
||||
/// A [`Cell`] contains the [`Text`] to be displayed in a [`Row`] of a [`Table`].
|
||||
///
|
||||
/// It can be created from anything that can be converted to a [`Text`].
|
||||
/// ```rust
|
||||
/// use std::borrow::Cow;
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
///
|
||||
/// Cell::from("simple string");
|
||||
///
|
||||
/// Cell::from(Span::from("span"));
|
||||
///
|
||||
/// Cell::from(Line::from(vec![
|
||||
/// Span::raw("a vec of "),
|
||||
/// Span::styled("spans", Style::default().add_modifier(Modifier::BOLD))
|
||||
/// ]));
|
||||
///
|
||||
/// Cell::from(Text::from("a text"));
|
||||
///
|
||||
/// Cell::from(Text::from(Cow::Borrowed("hello")));
|
||||
/// ```
|
||||
///
|
||||
/// You can apply a [`Style`] on the entire [`Cell`] using [`Cell::style`] or rely on the styling
|
||||
/// capabilities of [`Text`].
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Cell<'a> {
|
||||
content: Text<'a>,
|
||||
style: Style,
|
||||
}
|
||||
|
||||
impl<'a> Cell<'a> {
|
||||
/// Set the `Style` of this cell.
|
||||
pub fn style(mut self, style: Style) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> From<T> for Cell<'a>
|
||||
where
|
||||
T: Into<Text<'a>>,
|
||||
{
|
||||
fn from(content: T) -> Cell<'a> {
|
||||
Cell {
|
||||
content: content.into(),
|
||||
style: Style::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Styled for Cell<'a> {
|
||||
type Item = Cell<'a>;
|
||||
|
||||
fn style(&self) -> Style {
|
||||
self.style
|
||||
}
|
||||
|
||||
fn set_style(self, style: Style) -> Self::Item {
|
||||
self.style(style)
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds data to be displayed in a [`Table`] widget.
|
||||
///
|
||||
/// A [`Row`] is a collection of cells. It can be created from simple strings:
|
||||
/// ```rust
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
///
|
||||
/// Row::new(vec!["Cell1", "Cell2", "Cell3"]);
|
||||
/// ```
|
||||
///
|
||||
/// But if you need a bit more control over individual cells, you can explicitly create [`Cell`]s:
|
||||
/// ```rust
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
///
|
||||
/// Row::new(vec![
|
||||
/// Cell::from("Cell1"),
|
||||
/// Cell::from("Cell2").style(Style::default().fg(Color::Yellow)),
|
||||
/// ]);
|
||||
/// ```
|
||||
///
|
||||
/// You can also construct a row from any type that can be converted into [`Text`]:
|
||||
/// ```rust
|
||||
/// use std::borrow::Cow;
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
///
|
||||
/// Row::new(vec![
|
||||
/// Cow::Borrowed("hello"),
|
||||
/// Cow::Owned("world".to_uppercase()),
|
||||
/// ]);
|
||||
/// ```
|
||||
///
|
||||
/// By default, a row has a height of 1 but you can change this using [`Row::height`].
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Row<'a> {
|
||||
cells: Vec<Cell<'a>>,
|
||||
height: u16,
|
||||
style: Style,
|
||||
bottom_margin: u16,
|
||||
}
|
||||
|
||||
impl<'a> Row<'a> {
|
||||
/// Creates a new [`Row`] from an iterator where items can be converted to a [`Cell`].
|
||||
pub fn new<T>(cells: T) -> Self
|
||||
where
|
||||
T: IntoIterator,
|
||||
T::Item: Into<Cell<'a>>,
|
||||
{
|
||||
Self {
|
||||
height: 1,
|
||||
cells: cells.into_iter().map(Into::into).collect(),
|
||||
style: Style::default(),
|
||||
bottom_margin: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the fixed height of the [`Row`]. Any [`Cell`] whose content has more lines than this
|
||||
/// height will see its content truncated.
|
||||
pub fn height(mut self, height: u16) -> Self {
|
||||
self.height = height;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the [`Style`] of the entire row. This [`Style`] can be overridden by the [`Style`] of a
|
||||
/// any individual [`Cell`] or event by their [`Text`] content.
|
||||
pub fn style(mut self, style: Style) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the bottom margin. By default, the bottom margin is `0`.
|
||||
pub fn bottom_margin(mut self, margin: u16) -> Self {
|
||||
self.bottom_margin = margin;
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the total height of the row.
|
||||
fn total_height(&self) -> u16 {
|
||||
self.height.saturating_add(self.bottom_margin)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Styled for Row<'a> {
|
||||
type Item = Row<'a>;
|
||||
|
||||
fn style(&self) -> Style {
|
||||
self.style
|
||||
}
|
||||
|
||||
fn set_style(self, style: Style) -> Self::Item {
|
||||
self.style(style)
|
||||
}
|
||||
}
|
||||
pub use cell::Cell;
|
||||
pub use row::Row;
|
||||
pub use table::Table;
|
||||
pub use table_state::TableState;
|
||||
|
||||
/// This option allows the user to configure the "highlight symbol" column width spacing
|
||||
#[derive(Debug, Display, EnumString, PartialEq, Eq, Clone, Default, Hash)]
|
||||
@@ -170,12 +21,14 @@ pub enum HighlightSpacing {
|
||||
/// With this variant, the column for the selection symbol will always be allocated, and so the
|
||||
/// table will never change size, regardless of if a row is selected or not
|
||||
Always,
|
||||
|
||||
/// Only add spacing for the selection symbol column if a row is selected
|
||||
///
|
||||
/// With this variant, the column for the selection symbol will only be allocated if there is a
|
||||
/// selection, causing the table to shift if selected / unselected
|
||||
#[default]
|
||||
WhenSelected,
|
||||
|
||||
/// Never add spacing to the selection symbol column, regardless of whether something is
|
||||
/// selected or not
|
||||
///
|
||||
@@ -184,659 +37,23 @@ pub enum HighlightSpacing {
|
||||
}
|
||||
|
||||
impl HighlightSpacing {
|
||||
/// Determine if a selection should be done, based on variant
|
||||
/// Input "selection_state" should be similar to `state.selected.is_some()`
|
||||
pub fn should_add(&self, selection_state: bool) -> bool {
|
||||
/// Determine if a selection column should be displayed
|
||||
///
|
||||
/// has_selection: true if a row is selected in the table
|
||||
///
|
||||
/// Returns true if a selection column should be displayed
|
||||
pub(crate) fn should_add(&self, has_selection: bool) -> bool {
|
||||
match self {
|
||||
HighlightSpacing::Always => true,
|
||||
HighlightSpacing::WhenSelected => selection_state,
|
||||
HighlightSpacing::WhenSelected => has_selection,
|
||||
HighlightSpacing::Never => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A widget to display data in formatted columns.
|
||||
///
|
||||
/// It is a collection of [`Row`]s, themselves composed of [`Cell`]s:
|
||||
/// ```rust
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
///
|
||||
/// Table::new(vec![
|
||||
/// // Row can be created from simple strings.
|
||||
/// Row::new(vec!["Row11", "Row12", "Row13"]),
|
||||
/// // You can style the entire row.
|
||||
/// Row::new(vec!["Row21", "Row22", "Row23"]).style(Style::default().fg(Color::Blue)),
|
||||
/// // If you need more control over the styling you may need to create Cells directly
|
||||
/// Row::new(vec![
|
||||
/// Cell::from("Row31"),
|
||||
/// Cell::from("Row32").style(Style::default().fg(Color::Yellow)),
|
||||
/// Cell::from(Line::from(vec![
|
||||
/// Span::raw("Row"),
|
||||
/// Span::styled("33", Style::default().fg(Color::Green))
|
||||
/// ])),
|
||||
/// ]),
|
||||
/// // If a Row need to display some content over multiple lines, you just have to change
|
||||
/// // its height.
|
||||
/// Row::new(vec![
|
||||
/// Cell::from("Row\n41"),
|
||||
/// Cell::from("Row\n42"),
|
||||
/// Cell::from("Row\n43"),
|
||||
/// ]).height(2),
|
||||
/// ])
|
||||
/// // You can set the style of the entire Table.
|
||||
/// .style(Style::default().fg(Color::White))
|
||||
/// // It has an optional header, which is simply a Row always visible at the top.
|
||||
/// .header(
|
||||
/// Row::new(vec!["Col1", "Col2", "Col3"])
|
||||
/// .style(Style::default().fg(Color::Yellow))
|
||||
/// // If you want some space between the header and the rest of the rows, you can always
|
||||
/// // specify some margin at the bottom.
|
||||
/// .bottom_margin(1)
|
||||
/// )
|
||||
/// // As any other widget, a Table can be wrapped in a Block.
|
||||
/// .block(Block::default().title("Table"))
|
||||
/// // Columns widths are constrained in the same way as Layout...
|
||||
/// .widths(&[Constraint::Length(5), Constraint::Length(5), Constraint::Length(10)])
|
||||
/// // ...and they can be separated by a fixed spacing.
|
||||
/// .column_spacing(1)
|
||||
/// // If you wish to highlight a row in any specific way when it is selected...
|
||||
/// .highlight_style(Style::default().add_modifier(Modifier::BOLD))
|
||||
/// // ...and potentially show a symbol in front of the selection.
|
||||
/// .highlight_symbol(">>");
|
||||
/// ```
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Table<'a> {
|
||||
/// A block to wrap the widget in
|
||||
block: Option<Block<'a>>,
|
||||
/// Base style for the widget
|
||||
style: Style,
|
||||
/// Width constraints for each column
|
||||
widths: &'a [Constraint],
|
||||
/// Space between each column
|
||||
column_spacing: u16,
|
||||
/// Style used to render the selected row
|
||||
highlight_style: Style,
|
||||
/// Symbol in front of the selected rom
|
||||
highlight_symbol: Option<&'a str>,
|
||||
/// Optional header
|
||||
header: Option<Row<'a>>,
|
||||
/// Data to display in each row
|
||||
rows: Vec<Row<'a>>,
|
||||
/// Decides when to allocate spacing for the row selection
|
||||
highlight_spacing: HighlightSpacing,
|
||||
}
|
||||
|
||||
impl<'a> Table<'a> {
|
||||
/// Creates a new [`Table`] widget with the given rows.
|
||||
///
|
||||
/// The `rows` parameter is a Vector of [`Row`], this holds the data to be displayed by the
|
||||
/// table
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let table = Table::new(vec![
|
||||
/// Row::new(vec![
|
||||
/// Cell::from("Cell1"),
|
||||
/// Cell::from("Cell2")
|
||||
/// ]),
|
||||
/// Row::new(vec![
|
||||
/// Cell::from("Cell3"),
|
||||
/// Cell::from("Cell4")
|
||||
/// ]),
|
||||
/// ]);
|
||||
/// ```
|
||||
pub fn new<T>(rows: T) -> Self
|
||||
where
|
||||
T: IntoIterator<Item = Row<'a>>,
|
||||
{
|
||||
Self {
|
||||
block: None,
|
||||
style: Style::default(),
|
||||
widths: &[],
|
||||
column_spacing: 1,
|
||||
highlight_style: Style::default(),
|
||||
highlight_symbol: None,
|
||||
header: None,
|
||||
rows: rows.into_iter().collect(),
|
||||
highlight_spacing: HighlightSpacing::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a custom block around a [`Table`] widget.
|
||||
///
|
||||
/// The `block` parameter is of type [`Block`]. This holds the specified block to be
|
||||
/// created around the [`Table`]
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let table = Table::new(vec![
|
||||
/// Row::new(vec![
|
||||
/// Cell::from("Cell1"),
|
||||
/// Cell::from("Cell2")
|
||||
/// ]),
|
||||
/// Row::new(vec![
|
||||
/// Cell::from("Cell3"),
|
||||
/// Cell::from("Cell4")
|
||||
/// ]),
|
||||
/// ]).block(Block::default().title("Table"));
|
||||
/// ```
|
||||
pub fn block(mut self, block: Block<'a>) -> Self {
|
||||
self.block = Some(block);
|
||||
self
|
||||
}
|
||||
|
||||
/// Creates a header for a [`Table`] widget.
|
||||
///
|
||||
/// The `header` parameter is of type [`Row`] and this holds the cells to be displayed at the
|
||||
/// top of the [`Table`]
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let table = Table::new(vec![
|
||||
/// Row::new(vec![
|
||||
/// Cell::from("Cell1"),
|
||||
/// Cell::from("Cell2")
|
||||
/// ])
|
||||
/// ]).header(
|
||||
/// Row::new(vec![
|
||||
/// Cell::from("Header Cell 1"),
|
||||
/// Cell::from("Header Cell 2")
|
||||
/// ])
|
||||
/// );
|
||||
/// ```
|
||||
pub fn header(mut self, header: Row<'a>) -> Self {
|
||||
self.header = Some(header);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn widths(mut self, widths: &'a [Constraint]) -> Self {
|
||||
let between_0_and_100 = |&w| match w {
|
||||
Constraint::Percentage(p) => p <= 100,
|
||||
_ => true,
|
||||
};
|
||||
assert!(
|
||||
widths.iter().all(between_0_and_100),
|
||||
"Percentages should be between 0 and 100 inclusively."
|
||||
);
|
||||
self.widths = widths;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn style(mut self, style: Style) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn highlight_symbol(mut self, highlight_symbol: &'a str) -> Self {
|
||||
self.highlight_symbol = Some(highlight_symbol);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn highlight_style(mut self, highlight_style: Style) -> Self {
|
||||
self.highlight_style = highlight_style;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set when to show the highlight spacing
|
||||
///
|
||||
/// See [`HighlightSpacing`] about which variant affects spacing in which way
|
||||
pub fn highlight_spacing(mut self, value: HighlightSpacing) -> Self {
|
||||
self.highlight_spacing = value;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn column_spacing(mut self, spacing: u16) -> Self {
|
||||
self.column_spacing = spacing;
|
||||
self
|
||||
}
|
||||
|
||||
/// Get all offsets and widths of all user specified columns
|
||||
/// Returns (x, width)
|
||||
fn get_columns_widths(&self, max_width: u16, selection_width: u16) -> Vec<(u16, u16)> {
|
||||
let mut constraints = Vec::with_capacity(self.widths.len() * 2 + 1);
|
||||
constraints.push(Constraint::Length(selection_width));
|
||||
for constraint in self.widths {
|
||||
constraints.push(*constraint);
|
||||
constraints.push(Constraint::Length(self.column_spacing));
|
||||
}
|
||||
if !self.widths.is_empty() {
|
||||
constraints.pop();
|
||||
}
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(constraints)
|
||||
.segment_size(SegmentSize::None)
|
||||
.split(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: max_width,
|
||||
height: 1,
|
||||
});
|
||||
chunks
|
||||
.iter()
|
||||
.skip(1)
|
||||
.step_by(2)
|
||||
.map(|c| (c.x, c.width))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn get_row_bounds(
|
||||
&self,
|
||||
selected: Option<usize>,
|
||||
offset: usize,
|
||||
max_height: u16,
|
||||
) -> (usize, usize) {
|
||||
let offset = offset.min(self.rows.len().saturating_sub(1));
|
||||
let mut start = offset;
|
||||
let mut end = offset;
|
||||
let mut height = 0;
|
||||
for item in self.rows.iter().skip(offset) {
|
||||
if height + item.height > max_height {
|
||||
break;
|
||||
}
|
||||
height += item.total_height();
|
||||
end += 1;
|
||||
}
|
||||
|
||||
let selected = selected.unwrap_or(0).min(self.rows.len() - 1);
|
||||
while selected >= end {
|
||||
height = height.saturating_add(self.rows[end].total_height());
|
||||
end += 1;
|
||||
while height > max_height {
|
||||
height = height.saturating_sub(self.rows[start].total_height());
|
||||
start += 1;
|
||||
}
|
||||
}
|
||||
while selected < start {
|
||||
start -= 1;
|
||||
height = height.saturating_add(self.rows[start].total_height());
|
||||
while height > max_height {
|
||||
end -= 1;
|
||||
height = height.saturating_sub(self.rows[end].total_height());
|
||||
}
|
||||
}
|
||||
(start, end)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Styled for Table<'a> {
|
||||
type Item = Table<'a>;
|
||||
|
||||
fn style(&self) -> Style {
|
||||
self.style
|
||||
}
|
||||
|
||||
fn set_style(self, style: Style) -> Self::Item {
|
||||
self.style(style)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct TableState {
|
||||
offset: usize,
|
||||
selected: Option<usize>,
|
||||
}
|
||||
|
||||
impl TableState {
|
||||
pub fn offset(&self) -> usize {
|
||||
self.offset
|
||||
}
|
||||
|
||||
pub fn offset_mut(&mut self) -> &mut usize {
|
||||
&mut self.offset
|
||||
}
|
||||
|
||||
pub fn with_selected(mut self, selected: Option<usize>) -> Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_offset(mut self, offset: usize) -> Self {
|
||||
self.offset = offset;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn selected(&self) -> Option<usize> {
|
||||
self.selected
|
||||
}
|
||||
|
||||
pub fn select(&mut self, index: Option<usize>) {
|
||||
self.selected = index;
|
||||
if index.is_none() {
|
||||
self.offset = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> StatefulWidget for Table<'a> {
|
||||
type State = TableState;
|
||||
|
||||
fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||
if area.area() == 0 {
|
||||
return;
|
||||
}
|
||||
buf.set_style(area, self.style);
|
||||
let table_area = match self.block.take() {
|
||||
Some(b) => {
|
||||
let inner_area = b.inner(area);
|
||||
b.render(area, buf);
|
||||
inner_area
|
||||
}
|
||||
None => area,
|
||||
};
|
||||
|
||||
let selection_width = if self.highlight_spacing.should_add(state.selected.is_some()) {
|
||||
self.highlight_symbol.map_or(0, |s| s.width() as u16)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let columns_widths = self.get_columns_widths(table_area.width, selection_width);
|
||||
let highlight_symbol = self.highlight_symbol.unwrap_or("");
|
||||
let mut current_height = 0;
|
||||
let mut rows_height = table_area.height;
|
||||
|
||||
// Draw header
|
||||
if let Some(ref header) = self.header {
|
||||
let max_header_height = table_area.height.min(header.total_height());
|
||||
buf.set_style(
|
||||
Rect {
|
||||
x: table_area.left(),
|
||||
y: table_area.top(),
|
||||
width: table_area.width,
|
||||
height: table_area.height.min(header.height),
|
||||
},
|
||||
header.style,
|
||||
);
|
||||
let inner_offset = table_area.left();
|
||||
for ((x, width), cell) in columns_widths.iter().zip(header.cells.iter()) {
|
||||
render_cell(
|
||||
buf,
|
||||
cell,
|
||||
Rect {
|
||||
x: inner_offset + x,
|
||||
y: table_area.top(),
|
||||
width: *width,
|
||||
height: max_header_height,
|
||||
},
|
||||
);
|
||||
}
|
||||
current_height += max_header_height;
|
||||
rows_height = rows_height.saturating_sub(max_header_height);
|
||||
}
|
||||
|
||||
// Draw rows
|
||||
if self.rows.is_empty() {
|
||||
return;
|
||||
}
|
||||
let (start, end) = self.get_row_bounds(state.selected, state.offset, rows_height);
|
||||
state.offset = start;
|
||||
for (i, table_row) in self
|
||||
.rows
|
||||
.iter_mut()
|
||||
.enumerate()
|
||||
.skip(state.offset)
|
||||
.take(end - start)
|
||||
{
|
||||
let (row, inner_offset) = (table_area.top() + current_height, table_area.left());
|
||||
current_height += table_row.total_height();
|
||||
let table_row_area = Rect {
|
||||
x: inner_offset,
|
||||
y: row,
|
||||
width: table_area.width,
|
||||
height: table_row.height,
|
||||
};
|
||||
buf.set_style(table_row_area, table_row.style);
|
||||
let is_selected = state.selected.map_or(false, |s| s == i);
|
||||
if selection_width > 0 && is_selected {
|
||||
// this should in normal cases be safe, because "get_columns_widths" allocates
|
||||
// "highlight_symbol.width()" space but "get_columns_widths"
|
||||
// currently does not bind it to max table.width()
|
||||
buf.set_stringn(
|
||||
inner_offset,
|
||||
row,
|
||||
highlight_symbol,
|
||||
table_area.width as usize,
|
||||
table_row.style,
|
||||
);
|
||||
};
|
||||
for ((x, width), cell) in columns_widths.iter().zip(table_row.cells.iter()) {
|
||||
render_cell(
|
||||
buf,
|
||||
cell,
|
||||
Rect {
|
||||
x: inner_offset + x,
|
||||
y: row,
|
||||
width: *width,
|
||||
height: table_row.height,
|
||||
},
|
||||
);
|
||||
}
|
||||
if is_selected {
|
||||
buf.set_style(table_row_area, self.highlight_style);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_cell(buf: &mut Buffer, cell: &Cell, area: Rect) {
|
||||
buf.set_style(area, cell.style);
|
||||
for (i, line) in cell.content.lines.iter().enumerate() {
|
||||
if i as u16 >= area.height {
|
||||
break;
|
||||
}
|
||||
|
||||
let x_offset = match line.alignment {
|
||||
Some(Alignment::Center) => (area.width / 2).saturating_sub(line.width() as u16 / 2),
|
||||
Some(Alignment::Right) => area.width.saturating_sub(line.width() as u16),
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
buf.set_line(area.x + x_offset, area.y + i as u16, line, area.width);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for Table<'a> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let mut state = TableState::default();
|
||||
StatefulWidget::render(self, area, buf, &mut state);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::vec;
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
layout::Constraint::*,
|
||||
style::{Color, Modifier, Style, Stylize},
|
||||
text::Line,
|
||||
};
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn table_invalid_percentages() {
|
||||
Table::new(vec![]).widths(&[Constraint::Percentage(110)]);
|
||||
}
|
||||
|
||||
// test how constraints interact with table column width allocation
|
||||
mod table_column_widths {
|
||||
use super::*;
|
||||
|
||||
/// Construct a a new table with the given constraints, available and selection widths and
|
||||
/// tests that the widths match the expected list of (x, width) tuples.
|
||||
#[track_caller]
|
||||
fn test(
|
||||
constraints: &[Constraint],
|
||||
available_width: u16,
|
||||
selection_width: u16,
|
||||
expected: &[(u16, u16)],
|
||||
) {
|
||||
let table = Table::new(vec![]).widths(constraints);
|
||||
|
||||
let widths = table.get_columns_widths(available_width, selection_width);
|
||||
assert_eq!(widths, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn length_constraint() {
|
||||
// without selection, more than needed width
|
||||
test(&[Length(4), Length(4)], 20, 0, &[(0, 4), (5, 4)]);
|
||||
|
||||
// with selection, more than needed width
|
||||
test(&[Length(4), Length(4)], 20, 3, &[(3, 4), (8, 4)]);
|
||||
|
||||
// without selection, less than needed width
|
||||
test(&[Length(4), Length(4)], 7, 0, &[(0, 4), (5, 2)]);
|
||||
|
||||
// with selection, less than needed width
|
||||
test(&[Length(4), Length(4)], 7, 3, &[(3, 4), (7, 0)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_constraint() {
|
||||
// without selection, more than needed width
|
||||
test(&[Max(4), Max(4)], 20, 0, &[(0, 4), (5, 4)]);
|
||||
|
||||
// with selection, more than needed width
|
||||
test(&[Max(4), Max(4)], 20, 3, &[(3, 4), (8, 4)]);
|
||||
|
||||
// without selection, less than needed width
|
||||
test(&[Max(4), Max(4)], 7, 0, &[(0, 4), (5, 2)]);
|
||||
|
||||
// with selection, less than needed width
|
||||
test(&[Max(4), Max(4)], 7, 3, &[(3, 3), (7, 0)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn min_constraint() {
|
||||
// in its currently stage, the "Min" constraint does not grow to use the possible
|
||||
// available length and enabling "expand_to_fill" will just stretch the last
|
||||
// constraint and not split it with all available constraints
|
||||
|
||||
// without selection, more than needed width
|
||||
test(&[Min(4), Min(4)], 20, 0, &[(0, 4), (5, 4)]);
|
||||
|
||||
// with selection, more than needed width
|
||||
test(&[Min(4), Min(4)], 20, 3, &[(3, 4), (8, 4)]);
|
||||
|
||||
// without selection, less than needed width
|
||||
// allocates no spacer
|
||||
test(&[Min(4), Min(4)], 7, 0, &[(0, 4), (4, 3)]);
|
||||
|
||||
// with selection, less than needed width
|
||||
// allocates no selection and no spacer
|
||||
test(&[Min(4), Min(4)], 7, 3, &[(0, 4), (4, 3)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn percentage_constraint() {
|
||||
// without selection, more than needed width
|
||||
test(&[Percentage(30), Percentage(30)], 20, 0, &[(0, 6), (7, 6)]);
|
||||
|
||||
// with selection, more than needed width
|
||||
test(&[Percentage(30), Percentage(30)], 20, 3, &[(3, 6), (10, 6)]);
|
||||
|
||||
// without selection, less than needed width
|
||||
// rounds from positions: [0.0, 0.0, 2.1, 3.1, 5.2, 7.0]
|
||||
test(&[Percentage(30), Percentage(30)], 7, 0, &[(0, 2), (3, 2)]);
|
||||
|
||||
// with selection, less than needed width
|
||||
// rounds from positions: [0.0, 3.0, 5.1, 6.1, 7.0, 7.0]
|
||||
test(&[Percentage(30), Percentage(30)], 7, 3, &[(3, 2), (6, 1)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ratio_constraint() {
|
||||
// without selection, more than needed width
|
||||
// rounds from positions: [0.00, 0.00, 6.67, 7.67, 14.33]
|
||||
test(&[Ratio(1, 3), Ratio(1, 3)], 20, 0, &[(0, 7), (8, 6)]);
|
||||
|
||||
// with selection, more than needed width
|
||||
// rounds from positions: [0.00, 3.00, 10.67, 17.33, 20.00]
|
||||
test(&[Ratio(1, 3), Ratio(1, 3)], 20, 3, &[(3, 7), (11, 6)]);
|
||||
|
||||
// without selection, less than needed width
|
||||
// rounds from positions: [0.00, 2.33, 3.33, 5.66, 7.00]
|
||||
test(&[Ratio(1, 3), Ratio(1, 3)], 7, 0, &[(0, 2), (3, 3)]);
|
||||
|
||||
// with selection, less than needed width
|
||||
// rounds from positions: [0.00, 3.00, 5.33, 6.33, 7.00, 7.00]
|
||||
test(&[Ratio(1, 3), Ratio(1, 3)], 7, 3, &[(3, 2), (6, 1)]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_table_with_alignment() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 3));
|
||||
let table = Table::new(vec![
|
||||
Row::new(vec![Line::from("Left").alignment(Alignment::Left)]),
|
||||
Row::new(vec![Line::from("Center").alignment(Alignment::Center)]),
|
||||
Row::new(vec![Line::from("Right").alignment(Alignment::Right)]),
|
||||
])
|
||||
.widths(&[Percentage(100)]);
|
||||
|
||||
Widget::render(table, Rect::new(0, 0, 20, 3), &mut buf);
|
||||
|
||||
let expected = Buffer::with_lines(vec![
|
||||
"Left ",
|
||||
" Center ",
|
||||
" Right",
|
||||
]);
|
||||
|
||||
assert_eq!(buf, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cell_can_be_stylized() {
|
||||
assert_eq!(
|
||||
Cell::from("").black().on_white().bold().not_dim().style,
|
||||
Style::default()
|
||||
.fg(Color::Black)
|
||||
.bg(Color::White)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.remove_modifier(Modifier::DIM)
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn row_can_be_stylized() {
|
||||
assert_eq!(
|
||||
Row::new(vec![Cell::from("")])
|
||||
.black()
|
||||
.on_white()
|
||||
.bold()
|
||||
.not_italic()
|
||||
.style,
|
||||
Style::default()
|
||||
.fg(Color::Black)
|
||||
.bg(Color::White)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.remove_modifier(Modifier::ITALIC)
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_can_be_stylized() {
|
||||
assert_eq!(
|
||||
Table::new(vec![Row::new(vec![Cell::from("")])])
|
||||
.black()
|
||||
.on_white()
|
||||
.bold()
|
||||
.not_crossed_out()
|
||||
.style,
|
||||
Style::default()
|
||||
.fg(Color::Black)
|
||||
.bg(Color::White)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.remove_modifier(Modifier::CROSSED_OUT)
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn highlight_spacing_to_string() {
|
||||
|
||||
215
src/widgets/table/cell.rs
Normal file
215
src/widgets/table/cell.rs
Normal file
@@ -0,0 +1,215 @@
|
||||
use crate::prelude::*;
|
||||
|
||||
/// A [`Cell`] contains the [`Text`] to be displayed in a [`Row`] of a [`Table`].
|
||||
///
|
||||
/// You can apply a [`Style`] to the [`Cell`] using [`Cell::style`]. This will set the style for the
|
||||
/// entire area of the cell. Any [`Style`] set on the [`Text`] content will be combined with the
|
||||
/// [`Style`] of the [`Cell`] by adding the [`Style`] of the [`Text`] content to the [`Style`] of
|
||||
/// the [`Cell`]. Styles set on the text content will only affect the content.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// You can create a `Cell` from anything that can be converted to a [`Text`].
|
||||
///
|
||||
/// ```rust
|
||||
/// use std::borrow::Cow;
|
||||
///
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
///
|
||||
/// Cell::from("simple string");
|
||||
/// Cell::from(Span::from("span"));
|
||||
/// Cell::from(Line::from(vec![
|
||||
/// Span::raw("a vec of "),
|
||||
/// Span::styled("spans", Style::default().add_modifier(Modifier::BOLD)),
|
||||
/// ]));
|
||||
/// Cell::from(Text::from("a text"));
|
||||
/// Cell::from(Text::from(Cow::Borrowed("hello")));
|
||||
/// ```
|
||||
///
|
||||
/// `Cell` implements [`Styled`] which means you can use style shorthands from the [`Stylize`] trait
|
||||
/// to set the style of the cell concisely.
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
/// Cell::new("Cell 1").red().italic();
|
||||
/// ```
|
||||
///
|
||||
/// [`Row`]: super::Row
|
||||
/// [`Table`]: super::Table
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Cell<'a> {
|
||||
content: Text<'a>,
|
||||
style: Style,
|
||||
}
|
||||
|
||||
impl<'a> Cell<'a> {
|
||||
/// Creates a new [`Cell`]
|
||||
///
|
||||
/// The `content` parameter accepts any value that can be converted into a [`Text`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// Cell::new("simple string");
|
||||
/// Cell::new(Span::from("span"));
|
||||
/// Cell::new(Line::from(vec![
|
||||
/// Span::raw("a vec of "),
|
||||
/// Span::styled("spans", Style::default().add_modifier(Modifier::BOLD)),
|
||||
/// ]));
|
||||
/// Cell::new(Text::from("a text"));
|
||||
/// ```
|
||||
pub fn new<T>(content: T) -> Self
|
||||
where
|
||||
T: Into<Text<'a>>,
|
||||
{
|
||||
Self {
|
||||
content: content.into(),
|
||||
style: Style::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the content of the [`Cell`]
|
||||
///
|
||||
/// The `content` parameter accepts any value that can be converted into a [`Text`].
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// Cell::default().content("simple string");
|
||||
/// Cell::default().content(Span::from("span"));
|
||||
/// Cell::default().content(Line::from(vec![
|
||||
/// Span::raw("a vec of "),
|
||||
/// Span::styled("spans", Style::new().bold()),
|
||||
/// ]));
|
||||
/// Cell::default().content(Text::from("a text"));
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn content<T>(mut self, content: T) -> Self
|
||||
where
|
||||
T: Into<Text<'a>>,
|
||||
{
|
||||
self.content = content.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the `Style` of this cell
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// This `Style` will override the `Style` of the [`Row`] and can be overridden by the `Style`
|
||||
/// of the [`Text`] content.
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// Cell::new("Cell 1").style(Style::new().red().italic());
|
||||
/// ```
|
||||
///
|
||||
/// `Cell` also implements the [`Styled`] trait, which means you can use style shorthands from
|
||||
/// the [`Stylize`] trait to set the style of the widget more concisely.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// Cell::new("Cell 1").red().italic();
|
||||
/// ```
|
||||
///
|
||||
/// [`Row`]: super::Row
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Cell<'_> {
|
||||
pub(crate) fn render(&self, area: Rect, buf: &mut Buffer) {
|
||||
buf.set_style(area, self.style);
|
||||
for (i, line) in self.content.lines.iter().enumerate() {
|
||||
if i as u16 >= area.height {
|
||||
break;
|
||||
}
|
||||
|
||||
let x_offset = match line.alignment {
|
||||
Some(Alignment::Center) => (area.width / 2).saturating_sub(line.width() as u16 / 2),
|
||||
Some(Alignment::Right) => area.width.saturating_sub(line.width() as u16),
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
let x = area.x + x_offset;
|
||||
if x >= area.right() {
|
||||
continue;
|
||||
}
|
||||
|
||||
buf.set_line(x, area.y + i as u16, line, area.width);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> From<T> for Cell<'a>
|
||||
where
|
||||
T: Into<Text<'a>>,
|
||||
{
|
||||
fn from(content: T) -> Cell<'a> {
|
||||
Cell {
|
||||
content: content.into(),
|
||||
style: Style::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Styled for Cell<'a> {
|
||||
type Item = Cell<'a>;
|
||||
|
||||
fn style(&self) -> Style {
|
||||
self.style
|
||||
}
|
||||
|
||||
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
|
||||
self.style(style)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::style::{Color, Modifier, Style, Stylize};
|
||||
|
||||
#[test]
|
||||
fn new() {
|
||||
let cell = Cell::new("");
|
||||
assert_eq!(cell.content, Text::from(""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn content() {
|
||||
let cell = Cell::default().content("");
|
||||
assert_eq!(cell.content, Text::from(""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn style() {
|
||||
let style = Style::default().red().italic();
|
||||
let cell = Cell::default().style(style);
|
||||
assert_eq!(cell.style, style);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stylize() {
|
||||
assert_eq!(
|
||||
Cell::from("").black().on_white().bold().not_dim().style,
|
||||
Style::default()
|
||||
.fg(Color::Black)
|
||||
.bg(Color::White)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.remove_modifier(Modifier::DIM)
|
||||
)
|
||||
}
|
||||
}
|
||||
300
src/widgets/table/row.rs
Normal file
300
src/widgets/table/row.rs
Normal file
@@ -0,0 +1,300 @@
|
||||
use super::*;
|
||||
use crate::prelude::*;
|
||||
|
||||
/// A single row of data to be displayed in a [`Table`] widget.
|
||||
///
|
||||
/// A `Row` is a collection of [`Cell`]s.
|
||||
///
|
||||
/// By default, a row has a height of 1 but you can change this using [`Row::height`].
|
||||
///
|
||||
/// You can set the style of the entire row using [`Row::style`]. This [`Style`] will be combined
|
||||
/// with the [`Style`] of each individual [`Cell`] by adding the [`Style`] of the [`Cell`] to the
|
||||
/// [`Style`] of the [`Row`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// You can create `Row`s from simple strings.
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
///
|
||||
/// Row::new(vec!["Cell1", "Cell2", "Cell3"]);
|
||||
/// ```
|
||||
///
|
||||
/// If you need a bit more control over individual cells, you can explicitly create [`Cell`]s:
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
///
|
||||
/// Row::new(vec![
|
||||
/// Cell::from("Cell1"),
|
||||
/// Cell::from("Cell2").style(Style::default().fg(Color::Yellow)),
|
||||
/// ]);
|
||||
/// ```
|
||||
///
|
||||
/// You can also construct a row from any type that can be converted into [`Text`]:
|
||||
///
|
||||
/// ```rust
|
||||
/// use std::borrow::Cow;
|
||||
///
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
///
|
||||
/// Row::new(vec![
|
||||
/// Cow::Borrowed("hello"),
|
||||
/// Cow::Owned("world".to_uppercase()),
|
||||
/// ]);
|
||||
/// ```
|
||||
///
|
||||
/// `Row` implements [`Styled`] which means you can use style shorthands from the [`Stylize`] trait
|
||||
/// to set the style of the row concisely.
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
/// let cells = vec!["Cell1", "Cell2", "Cell3"];
|
||||
/// Row::new(cells).red().italic();
|
||||
/// ```
|
||||
///
|
||||
/// [`Table`]: super::Table
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Row<'a> {
|
||||
pub(crate) cells: Vec<Cell<'a>>,
|
||||
pub(crate) height: u16,
|
||||
pub(crate) top_margin: u16,
|
||||
pub(crate) bottom_margin: u16,
|
||||
pub(crate) style: Style,
|
||||
}
|
||||
|
||||
impl<'a> Row<'a> {
|
||||
/// Creates a new [`Row`]
|
||||
///
|
||||
/// The `cells` parameter accepts any value that can be converted into an iterator of anything
|
||||
/// that can be converted into a [`Cell`] (e.g. `Vec<&str>`, `&[Cell<'a>]`, `Vec<String>`, etc.)
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let row = Row::new(vec!["Cell 1", "Cell 2", "Cell 3"]);
|
||||
/// let row = Row::new(vec![
|
||||
/// Cell::new("Cell 1"),
|
||||
/// Cell::new("Cell 2"),
|
||||
/// Cell::new("Cell 3"),
|
||||
/// ]);
|
||||
/// ```
|
||||
pub fn new<T>(cells: T) -> Self
|
||||
where
|
||||
T: IntoIterator,
|
||||
T::Item: Into<Cell<'a>>,
|
||||
{
|
||||
Self {
|
||||
cells: cells.into_iter().map(Into::into).collect(),
|
||||
height: 1,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the cells of the [`Row`]
|
||||
///
|
||||
/// The `cells` parameter accepts any value that can be converted into an iterator of anything
|
||||
/// that can be converted into a [`Cell`] (e.g. `Vec<&str>`, `&[Cell<'a>]`, `Vec<String>`, etc.)
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let row = Row::default().cells(vec!["Cell 1", "Cell 2", "Cell 3"]);
|
||||
/// let row = Row::default().cells(vec![
|
||||
/// Cell::new("Cell 1"),
|
||||
/// Cell::new("Cell 2"),
|
||||
/// Cell::new("Cell 3"),
|
||||
/// ]);
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn cells<T>(mut self, cells: T) -> Self
|
||||
where
|
||||
T: IntoIterator,
|
||||
T::Item: Into<Cell<'a>>,
|
||||
{
|
||||
self.cells = cells.into_iter().map(Into::into).collect();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the fixed height of the [`Row`]
|
||||
///
|
||||
/// Any [`Cell`] whose content has more lines than this height will see its content truncated.
|
||||
///
|
||||
/// By default, the height is `1`.
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let cells = vec!["Cell 1\nline 2", "Cell 2", "Cell 3"];
|
||||
/// let row = Row::new(cells).height(2);
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn height(mut self, height: u16) -> Self {
|
||||
self.height = height;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the top margin. By default, the top margin is `0`.
|
||||
///
|
||||
/// The top margin is the number of blank lines to be displayed before the row.
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// # let cells = vec!["Cell 1", "Cell 2", "Cell 3"];
|
||||
/// let row = Row::default().top_margin(1);
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn top_margin(mut self, margin: u16) -> Self {
|
||||
self.top_margin = margin;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the bottom margin. By default, the bottom margin is `0`.
|
||||
///
|
||||
/// The bottom margin is the number of blank lines to be displayed after the row.
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// # let cells = vec!["Cell 1", "Cell 2", "Cell 3"];
|
||||
/// let row = Row::default().bottom_margin(1);
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn bottom_margin(mut self, margin: u16) -> Self {
|
||||
self.bottom_margin = margin;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the [`Style`] of the entire row
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// This [`Style`] can be overridden by the [`Style`] of a any individual [`Cell`] or by their
|
||||
/// [`Text`] content.
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let cells = vec!["Cell 1", "Cell 2", "Cell 3"];
|
||||
/// let row = Row::new(cells).style(Style::new().red().italic());
|
||||
/// ```
|
||||
///
|
||||
/// `Row` also implements the [`Styled`] trait, which means you can use style shorthands from
|
||||
/// the [`Stylize`] trait to set the style of the widget more concisely.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let cells = vec!["Cell 1", "Cell 2", "Cell 3"];
|
||||
/// let row = Row::new(cells).red().italic();
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// private methods for rendering
|
||||
impl Row<'_> {
|
||||
/// Returns the total height of the row.
|
||||
pub(crate) fn height_with_margin(&self) -> u16 {
|
||||
self.height
|
||||
.saturating_add(self.top_margin)
|
||||
.saturating_add(self.bottom_margin)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Styled for Row<'a> {
|
||||
type Item = Row<'a>;
|
||||
|
||||
fn style(&self) -> Style {
|
||||
self.style
|
||||
}
|
||||
|
||||
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
|
||||
self.style(style)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::vec;
|
||||
|
||||
use super::*;
|
||||
use crate::style::{Color, Modifier, Style, Stylize};
|
||||
|
||||
#[test]
|
||||
fn new() {
|
||||
let cells = vec![Cell::from("")];
|
||||
let row = Row::new(cells.clone());
|
||||
assert_eq!(row.cells, cells);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cells() {
|
||||
let cells = vec![Cell::from("")];
|
||||
let row = Row::default().cells(cells.clone());
|
||||
assert_eq!(row.cells, cells);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn height() {
|
||||
let row = Row::default().height(2);
|
||||
assert_eq!(row.height, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn top_margin() {
|
||||
let row = Row::default().top_margin(1);
|
||||
assert_eq!(row.top_margin, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bottom_margin() {
|
||||
let row = Row::default().bottom_margin(1);
|
||||
assert_eq!(row.bottom_margin, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn style() {
|
||||
let style = Style::default().red().italic();
|
||||
let row = Row::default().style(style);
|
||||
assert_eq!(row.style, style);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stylize() {
|
||||
assert_eq!(
|
||||
Row::new(vec![Cell::from("")])
|
||||
.black()
|
||||
.on_white()
|
||||
.bold()
|
||||
.not_italic()
|
||||
.style,
|
||||
Style::default()
|
||||
.fg(Color::Black)
|
||||
.bg(Color::White)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.remove_modifier(Modifier::ITALIC)
|
||||
)
|
||||
}
|
||||
}
|
||||
1423
src/widgets/table/table.rs
Normal file
1423
src/widgets/table/table.rs
Normal file
File diff suppressed because it is too large
Load Diff
238
src/widgets/table/table_state.rs
Normal file
238
src/widgets/table/table_state.rs
Normal file
@@ -0,0 +1,238 @@
|
||||
/// State of a [`Table`] widget
|
||||
///
|
||||
/// This state can be used to scroll through the rows and select one of them. When the table is
|
||||
/// rendered as a stateful widget, the selected row will be highlighted and the table will be
|
||||
/// shifted to ensure that the selected row is visible. This will modify the [`TableState`] object
|
||||
/// passed to the [`Frame::render_stateful_widget`] method.
|
||||
///
|
||||
/// The state consists of two fields:
|
||||
/// - [`offset`]: the index of the first row to be displayed
|
||||
/// - [`selected`]: the index of the selected row, which can be `None` if no row is selected
|
||||
///
|
||||
/// [`offset`]: TableState::offset()
|
||||
/// [`selected`]: TableState::selected()
|
||||
///
|
||||
/// See the [table example] and the recipe and traceroute tabs in the [demo2 example] for a more in
|
||||
/// depth example of the various configuration options and for how to handle state.
|
||||
///
|
||||
/// [table example]: https://github.com/ratatui-org/ratatui/blob/master/examples/table.rs
|
||||
/// [demo2 example]: https://github.com/ratatui-org/ratatui/blob/master/examples/demo2/
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// # fn ui(frame: &mut Frame) {
|
||||
/// # let area = Rect::default();
|
||||
/// # let rows = [Row::new(vec!["Cell1", "Cell2"])];
|
||||
/// # let widths = [Constraint::Length(5), Constraint::Length(5)];
|
||||
/// let table = Table::new(rows, widths).widths(widths);
|
||||
///
|
||||
/// // Note: TableState should be stored in your application state (not constructed in your render
|
||||
/// // method) so that the selected row is preserved across renders
|
||||
/// let mut table_state = TableState::default();
|
||||
/// *table_state.offset_mut() = 1; // display the second row and onwards
|
||||
/// table_state.select(Some(3)); // select the forth row (0-indexed)
|
||||
///
|
||||
/// frame.render_stateful_widget(table, area, &mut table_state);
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// Note that if [`Table::widths`] is not called before rendering, the rendered columns will have
|
||||
/// equal width.
|
||||
///
|
||||
/// [`Table`]: crate::widgets::Table
|
||||
/// [`Table::widths`]: crate::widgets::Table::widths
|
||||
/// [`Frame::render_stateful_widget`]: crate::Frame::render_stateful_widget
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct TableState {
|
||||
pub(crate) offset: usize,
|
||||
pub(crate) selected: Option<usize>,
|
||||
}
|
||||
|
||||
impl TableState {
|
||||
/// Creates a new [`TableState`]
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let state = TableState::new();
|
||||
/// ```
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Sets the index of the first row to be displayed
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let state = TableState::new().with_offset(1);
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn with_offset(mut self, offset: usize) -> Self {
|
||||
self.offset = offset;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the index of the selected row
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let state = TableState::new().with_selected(Some(1));
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn with_selected<T>(mut self, selected: T) -> Self
|
||||
where
|
||||
T: Into<Option<usize>>,
|
||||
{
|
||||
self.selected = selected.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Index of the first row to be displayed
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let state = TableState::new();
|
||||
/// assert_eq!(state.offset(), 0);
|
||||
/// ```
|
||||
pub fn offset(&self) -> usize {
|
||||
self.offset
|
||||
}
|
||||
|
||||
/// Mutable reference to the index of the first row to be displayed
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let mut state = TableState::default();
|
||||
/// *state.offset_mut() = 1;
|
||||
/// ```
|
||||
pub fn offset_mut(&mut self) -> &mut usize {
|
||||
&mut self.offset
|
||||
}
|
||||
|
||||
/// Index of the selected row
|
||||
///
|
||||
/// Returns `None` if no row is selected
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let state = TableState::new();
|
||||
/// assert_eq!(state.selected(), None);
|
||||
/// ```
|
||||
pub fn selected(&self) -> Option<usize> {
|
||||
self.selected
|
||||
}
|
||||
|
||||
/// Mutable reference to the index of the selected row
|
||||
///
|
||||
/// Returns `None` if no row is selected
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let mut state = TableState::default();
|
||||
/// *state.selected_mut() = Some(1);
|
||||
/// ```
|
||||
pub fn selected_mut(&mut self) -> &mut Option<usize> {
|
||||
&mut self.selected
|
||||
}
|
||||
|
||||
/// Sets the index of the selected row
|
||||
///
|
||||
/// Set to `None` if no row is selected. This will also reset the offset to `0`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let mut state = TableState::default();
|
||||
/// state.select(Some(1));
|
||||
/// ```
|
||||
pub fn select(&mut self, index: Option<usize>) {
|
||||
self.selected = index;
|
||||
if index.is_none() {
|
||||
self.offset = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn new() {
|
||||
let state = TableState::new();
|
||||
assert_eq!(state.offset, 0);
|
||||
assert_eq!(state.selected, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_offset() {
|
||||
let state = TableState::new().with_offset(1);
|
||||
assert_eq!(state.offset, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_selected() {
|
||||
let state = TableState::new().with_selected(Some(1));
|
||||
assert_eq!(state.selected, Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offset() {
|
||||
let state = TableState::new();
|
||||
assert_eq!(state.offset(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offset_mut() {
|
||||
let mut state = TableState::new();
|
||||
*state.offset_mut() = 1;
|
||||
assert_eq!(state.offset, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selected() {
|
||||
let state = TableState::new();
|
||||
assert_eq!(state.selected(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selected_mut() {
|
||||
let mut state = TableState::new();
|
||||
*state.selected_mut() = Some(1);
|
||||
assert_eq!(state.selected, Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select() {
|
||||
let mut state = TableState::new();
|
||||
state.select(Some(1));
|
||||
assert_eq!(state.selected, Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_none() {
|
||||
let mut state = TableState::new().with_selected(Some(1));
|
||||
state.select(None);
|
||||
assert_eq!(state.selected, None);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,19 @@
|
||||
#![deny(missing_docs)]
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::{Style, Styled},
|
||||
symbols,
|
||||
text::{Line, Span},
|
||||
prelude::*,
|
||||
widgets::{Block, Widget},
|
||||
};
|
||||
|
||||
const DEFAULT_HIGHLIGHT_STYLE: Style = Style::new().add_modifier(Modifier::REVERSED);
|
||||
|
||||
/// A widget that displays a horizontal set of Tabs with a single tab selected.
|
||||
///
|
||||
/// Each tab title is stored as a [`Line`] which can be individually styled. The selected tab is set
|
||||
/// using [`Tabs::select`] and styled using [`Tabs::highlight_style`]. The divider can be customized
|
||||
/// with [`Tabs::divider`].
|
||||
/// with [`Tabs::divider`]. Padding can be set with [`Tabs::padding`] or [`Tabs::padding_left`] and
|
||||
/// [`Tabs::padding_right`].
|
||||
///
|
||||
/// The divider defaults to |, and padding defaults to a singular space on each side.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
@@ -24,7 +25,8 @@ use crate::{
|
||||
/// .style(Style::default().white())
|
||||
/// .highlight_style(Style::default().yellow())
|
||||
/// .select(2)
|
||||
/// .divider(symbols::DOT);
|
||||
/// .divider(symbols::DOT)
|
||||
/// .padding("->", "<-");
|
||||
/// ```
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Tabs<'a> {
|
||||
@@ -40,6 +42,10 @@ pub struct Tabs<'a> {
|
||||
highlight_style: Style,
|
||||
/// Tab divider
|
||||
divider: Span<'a>,
|
||||
/// Tab Left Padding
|
||||
padding_left: Line<'a>,
|
||||
/// Tab Right Padding
|
||||
padding_right: Line<'a>,
|
||||
}
|
||||
|
||||
impl<'a> Tabs<'a> {
|
||||
@@ -48,6 +54,18 @@ impl<'a> Tabs<'a> {
|
||||
/// `titles` can be a [`Vec`] of [`&str`], [`String`] or anything that can be converted into
|
||||
/// [`Line`]. As such, titles can be styled independently.
|
||||
///
|
||||
/// The selected tab can be set with [`Tabs::select`]. The first tab has index 0 (this is also
|
||||
/// the default index).
|
||||
///
|
||||
/// The selected tab can have a different style with [`Tabs::highlight_style`]. This defaults to
|
||||
/// a style with the [`Modifier::REVERSED`] modifier added.
|
||||
///
|
||||
/// The default divider is a pipe (`|`), but it can be customized with [`Tabs::divider`].
|
||||
///
|
||||
/// The entire widget can be styled with [`Tabs::style`].
|
||||
///
|
||||
/// The widget can be wrapped in a [`Block`] using [`Tabs::block`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Basic titles.
|
||||
@@ -70,12 +88,15 @@ impl<'a> Tabs<'a> {
|
||||
titles: titles.into_iter().map(Into::into).collect(),
|
||||
selected: 0,
|
||||
style: Style::default(),
|
||||
highlight_style: Style::default(),
|
||||
highlight_style: DEFAULT_HIGHLIGHT_STYLE,
|
||||
divider: Span::raw(symbols::line::VERTICAL),
|
||||
padding_left: Line::from(" "),
|
||||
padding_right: Line::from(" "),
|
||||
}
|
||||
}
|
||||
|
||||
/// Surrounds the `Tabs` with a [`Block`].
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn block(mut self, block: Block<'a>) -> Tabs<'a> {
|
||||
self.block = Some(block);
|
||||
self
|
||||
@@ -83,8 +104,9 @@ impl<'a> Tabs<'a> {
|
||||
|
||||
/// Sets the selected tab.
|
||||
///
|
||||
/// The first tab has index 0 (this is also the default index).
|
||||
/// The first tab has index 0 (this is also the default index).
|
||||
/// The selected tab can have a different style with [`Tabs::highlight_style`].
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn select(mut self, selected: usize) -> Tabs<'a> {
|
||||
self.selected = selected;
|
||||
self
|
||||
@@ -92,19 +114,27 @@ impl<'a> Tabs<'a> {
|
||||
|
||||
/// Sets the style of the tabs.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// This will set the given style on the entire render area.
|
||||
/// More precise style can be applied to the titles by styling the ones given to [`Tabs::new`].
|
||||
/// The selected tab can be styled differently using [`Tabs::highlight_style`].
|
||||
pub fn style(mut self, style: Style) -> Tabs<'a> {
|
||||
self.style = style;
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style for the highlighted tab.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// Highlighted tab can be selected with [`Tabs::select`].
|
||||
pub fn highlight_style(mut self, style: Style) -> Tabs<'a> {
|
||||
self.highlight_style = style;
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn highlight_style<S: Into<Style>>(mut self, style: S) -> Tabs<'a> {
|
||||
self.highlight_style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
@@ -121,7 +151,7 @@ impl<'a> Tabs<'a> {
|
||||
/// ```
|
||||
/// Use dash (`-`) as separator.
|
||||
/// ```
|
||||
/// # use ratatui::{prelude::*, widgets::Tabs, symbols};
|
||||
/// # use ratatui::{prelude::*, widgets::Tabs};
|
||||
/// let tabs = Tabs::new(vec!["Tab 1", "Tab 2"]).divider("-");
|
||||
/// ```
|
||||
pub fn divider<T>(mut self, divider: T) -> Tabs<'a>
|
||||
@@ -131,6 +161,70 @@ impl<'a> Tabs<'a> {
|
||||
self.divider = divider.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the padding between tabs.
|
||||
///
|
||||
/// Both default to space.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// A space on either side of the tabs.
|
||||
/// ```
|
||||
/// # use ratatui::{prelude::*, widgets::Tabs};
|
||||
/// let tabs = Tabs::new(vec!["Tab 1", "Tab 2"]).padding(" ", " ");
|
||||
/// ```
|
||||
/// Nothing on either side of the tabs.
|
||||
/// ```
|
||||
/// # use ratatui::{prelude::*, widgets::Tabs};
|
||||
/// let tabs = Tabs::new(vec!["Tab 1", "Tab 2"]).padding("", "");
|
||||
/// ```
|
||||
pub fn padding<T, U>(mut self, left: T, right: U) -> Tabs<'a>
|
||||
where
|
||||
T: Into<Line<'a>>,
|
||||
U: Into<Line<'a>>,
|
||||
{
|
||||
self.padding_left = left.into();
|
||||
self.padding_right = right.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the left side padding between tabs.
|
||||
///
|
||||
/// Defaults to a space.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// An arrow on the left of tabs.
|
||||
/// ```
|
||||
/// # use ratatui::{prelude::*, widgets::Tabs};
|
||||
/// let tabs = Tabs::new(vec!["Tab 1", "Tab 2"]).padding_left("->");
|
||||
/// ```
|
||||
pub fn padding_left<T>(mut self, padding: T) -> Tabs<'a>
|
||||
where
|
||||
T: Into<Line<'a>>,
|
||||
{
|
||||
self.padding_left = padding.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the right side padding between tabs.
|
||||
///
|
||||
/// Defaults to a space.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// An arrow on the right of tabs.
|
||||
/// ```
|
||||
/// # use ratatui::{prelude::*, widgets::Tabs};
|
||||
/// let tabs = Tabs::new(vec!["Tab 1", "Tab 2"]).padding_right("<-");
|
||||
/// ```
|
||||
pub fn padding_right<T>(mut self, padding: T) -> Tabs<'a>
|
||||
where
|
||||
T: Into<Line<'a>>,
|
||||
{
|
||||
self.padding_left = padding.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Styled for Tabs<'a> {
|
||||
@@ -140,7 +234,7 @@ impl<'a> Styled for Tabs<'a> {
|
||||
self.style
|
||||
}
|
||||
|
||||
fn set_style(self, style: Style) -> Self::Item {
|
||||
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
|
||||
self.style(style)
|
||||
}
|
||||
}
|
||||
@@ -165,11 +259,21 @@ impl<'a> Widget for Tabs<'a> {
|
||||
let titles_length = self.titles.len();
|
||||
for (i, title) in self.titles.into_iter().enumerate() {
|
||||
let last_title = titles_length - 1 == i;
|
||||
x = x.saturating_add(1);
|
||||
let remaining_width = tabs_area.right().saturating_sub(x);
|
||||
|
||||
if remaining_width == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
// Left Padding
|
||||
let pos = buf.set_line(x, tabs_area.top(), &self.padding_left, remaining_width);
|
||||
x = pos.0;
|
||||
let remaining_width = tabs_area.right().saturating_sub(x);
|
||||
if remaining_width == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
// Title
|
||||
let pos = buf.set_line(x, tabs_area.top(), &title, remaining_width);
|
||||
if i == self.selected {
|
||||
buf.set_style(
|
||||
@@ -182,11 +286,20 @@ impl<'a> Widget for Tabs<'a> {
|
||||
self.highlight_style,
|
||||
);
|
||||
}
|
||||
x = pos.0.saturating_add(1);
|
||||
x = pos.0;
|
||||
let remaining_width = tabs_area.right().saturating_sub(x);
|
||||
if remaining_width == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
// Right Padding
|
||||
let pos = buf.set_line(x, tabs_area.top(), &self.padding_right, remaining_width);
|
||||
x = pos.0;
|
||||
let remaining_width = tabs_area.right().saturating_sub(x);
|
||||
if remaining_width == 0 || last_title {
|
||||
break;
|
||||
}
|
||||
|
||||
let pos = buf.set_span(x, tabs_area.top(), &self.divider, remaining_width);
|
||||
x = pos.0;
|
||||
}
|
||||
@@ -196,7 +309,7 @@ impl<'a> Widget for Tabs<'a> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{assert_buffer_eq, prelude::*, widgets::Borders};
|
||||
use crate::{assert_buffer_eq, widgets::Borders};
|
||||
|
||||
#[test]
|
||||
fn new() {
|
||||
@@ -214,8 +327,10 @@ mod tests {
|
||||
],
|
||||
selected: 0,
|
||||
style: Style::default(),
|
||||
highlight_style: Style::default(),
|
||||
highlight_style: DEFAULT_HIGHLIGHT_STYLE,
|
||||
divider: Span::raw(symbols::line::VERTICAL),
|
||||
padding_right: Line::from(" "),
|
||||
padding_left: Line::from(" "),
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -229,40 +344,56 @@ mod tests {
|
||||
#[test]
|
||||
fn render_default() {
|
||||
let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"]);
|
||||
assert_buffer_eq!(
|
||||
render(tabs, Rect::new(0, 0, 30, 1)),
|
||||
Buffer::with_lines(vec![" Tab1 │ Tab2 │ Tab3 │ Tab4 "])
|
||||
);
|
||||
let mut expected = Buffer::with_lines(vec![" Tab1 │ Tab2 │ Tab3 │ Tab4 "]);
|
||||
// first tab selected
|
||||
expected.set_style(Rect::new(1, 0, 4, 1), DEFAULT_HIGHLIGHT_STYLE);
|
||||
assert_buffer_eq!(render(tabs, Rect::new(0, 0, 30, 1)), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_no_padding() {
|
||||
let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"]).padding("", "");
|
||||
let mut expected = Buffer::with_lines(vec!["Tab1│Tab2│Tab3│Tab4 "]);
|
||||
// first tab selected
|
||||
expected.set_style(Rect::new(0, 0, 4, 1), DEFAULT_HIGHLIGHT_STYLE);
|
||||
assert_buffer_eq!(render(tabs, Rect::new(0, 0, 30, 1)), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_more_padding() {
|
||||
let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"]).padding("---", "++");
|
||||
let mut expected = Buffer::with_lines(vec!["---Tab1++│---Tab2++│---Tab3++│"]);
|
||||
// first tab selected
|
||||
expected.set_style(Rect::new(3, 0, 4, 1), DEFAULT_HIGHLIGHT_STYLE);
|
||||
assert_buffer_eq!(render(tabs, Rect::new(0, 0, 30, 1)), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_with_block() {
|
||||
let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"])
|
||||
.block(Block::default().title("Tabs").borders(Borders::ALL));
|
||||
assert_buffer_eq!(
|
||||
render(tabs, Rect::new(0, 0, 30, 3)),
|
||||
Buffer::with_lines(vec![
|
||||
"┌Tabs────────────────────────┐",
|
||||
"│ Tab1 │ Tab2 │ Tab3 │ Tab4 │",
|
||||
"└────────────────────────────┘",
|
||||
])
|
||||
);
|
||||
let mut expected = Buffer::with_lines(vec![
|
||||
"┌Tabs────────────────────────┐",
|
||||
"│ Tab1 │ Tab2 │ Tab3 │ Tab4 │",
|
||||
"└────────────────────────────┘",
|
||||
]);
|
||||
// first tab selected
|
||||
expected.set_style(Rect::new(2, 1, 4, 1), DEFAULT_HIGHLIGHT_STYLE);
|
||||
assert_buffer_eq!(render(tabs, Rect::new(0, 0, 30, 3)), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_style() {
|
||||
let tabs =
|
||||
Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"]).style(Style::default().fg(Color::Red));
|
||||
assert_buffer_eq!(
|
||||
render(tabs, Rect::new(0, 0, 30, 1)),
|
||||
Buffer::with_lines(vec![" Tab1 │ Tab2 │ Tab3 │ Tab4 ".red()])
|
||||
);
|
||||
let mut expected = Buffer::with_lines(vec![" Tab1 │ Tab2 │ Tab3 │ Tab4 ".red()]);
|
||||
expected.set_style(Rect::new(1, 0, 4, 1), DEFAULT_HIGHLIGHT_STYLE.red());
|
||||
assert_buffer_eq!(render(tabs, Rect::new(0, 0, 30, 1)), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_select() {
|
||||
let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"])
|
||||
.highlight_style(Style::new().reversed());
|
||||
let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"]);
|
||||
|
||||
// first tab selected
|
||||
assert_buffer_eq!(
|
||||
@@ -305,13 +436,13 @@ mod tests {
|
||||
fn render_style_and_selected() {
|
||||
let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"])
|
||||
.style(Style::new().red())
|
||||
.highlight_style(Style::new().reversed())
|
||||
.highlight_style(Style::new().underlined())
|
||||
.select(0);
|
||||
assert_buffer_eq!(
|
||||
render(tabs, Rect::new(0, 0, 30, 1)),
|
||||
Buffer::with_lines(vec![Line::from(vec![
|
||||
" ".red(),
|
||||
"Tab1".red().reversed(),
|
||||
"Tab1".red().underlined(),
|
||||
" │ Tab2 │ Tab3 │ Tab4 ".red(),
|
||||
])])
|
||||
);
|
||||
@@ -320,10 +451,10 @@ mod tests {
|
||||
#[test]
|
||||
fn render_divider() {
|
||||
let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"]).divider("--");
|
||||
assert_buffer_eq!(
|
||||
render(tabs, Rect::new(0, 0, 30, 1)),
|
||||
Buffer::with_lines(vec![" Tab1 -- Tab2 -- Tab3 -- Tab4 ",])
|
||||
);
|
||||
let mut expected = Buffer::with_lines(vec![" Tab1 -- Tab2 -- Tab3 -- Tab4 "]);
|
||||
// first tab selected
|
||||
expected.set_style(Rect::new(1, 0, 4, 1), DEFAULT_HIGHLIGHT_STYLE);
|
||||
assert_buffer_eq!(render(tabs, Rect::new(0, 0, 30, 1)), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
use std::error::Error;
|
||||
|
||||
use ratatui::{
|
||||
assert_buffer_eq,
|
||||
backend::{Backend, TestBackend},
|
||||
layout::Rect,
|
||||
widgets::Paragraph,
|
||||
Terminal,
|
||||
prelude::Buffer,
|
||||
widgets::{Paragraph, Widget},
|
||||
Terminal, TerminalOptions, Viewport,
|
||||
};
|
||||
|
||||
#[test]
|
||||
@@ -23,9 +25,9 @@ fn swap_buffer_clears_prev_buffer() {
|
||||
terminal
|
||||
.current_buffer_mut()
|
||||
.set_string(0, 0, "Hello", ratatui::style::Style::reset());
|
||||
assert_eq!(terminal.current_buffer_mut().content()[0].symbol, "H");
|
||||
assert_eq!(terminal.current_buffer_mut().content()[0].symbol(), "H");
|
||||
terminal.swap_buffers();
|
||||
assert_eq!(terminal.current_buffer_mut().content()[0].symbol, " ");
|
||||
assert_eq!(terminal.current_buffer_mut().content()[0].symbol(), " ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -36,14 +38,158 @@ fn terminal_draw_returns_the_completed_frame() -> Result<(), Box<dyn Error>> {
|
||||
let paragraph = Paragraph::new("Test");
|
||||
f.render_widget(paragraph, f.size());
|
||||
})?;
|
||||
assert_eq!(frame.buffer.get(0, 0).symbol, "T");
|
||||
assert_eq!(frame.buffer.get(0, 0).symbol(), "T");
|
||||
assert_eq!(frame.area, Rect::new(0, 0, 10, 10));
|
||||
terminal.backend_mut().resize(8, 8);
|
||||
let frame = terminal.draw(|f| {
|
||||
let paragraph = Paragraph::new("test");
|
||||
f.render_widget(paragraph, f.size());
|
||||
})?;
|
||||
assert_eq!(frame.buffer.get(0, 0).symbol, "t");
|
||||
assert_eq!(frame.buffer.get(0, 0).symbol(), "t");
|
||||
assert_eq!(frame.area, Rect::new(0, 0, 8, 8));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal_insert_before_moves_viewport() -> Result<(), Box<dyn Error>> {
|
||||
// When we have a terminal with 5 lines, and a single line viewport, if we insert a
|
||||
// number of lines less than the `terminal height - viewport height` it should move
|
||||
// viewport down to accommodate the new lines.
|
||||
|
||||
let backend = TestBackend::new(20, 5);
|
||||
let mut terminal = Terminal::with_options(
|
||||
backend,
|
||||
TerminalOptions {
|
||||
viewport: Viewport::Inline(1),
|
||||
},
|
||||
)?;
|
||||
|
||||
// insert_before cannot guarantee the contents of the viewport remain unharmed
|
||||
// by potential scrolling as such it is necessary to call draw afterwards to
|
||||
// redraw the contents of the viewport over the newly designated area.
|
||||
terminal.insert_before(2, |buf| {
|
||||
Paragraph::new(vec![
|
||||
"------ Line 1 ------".into(),
|
||||
"------ Line 2 ------".into(),
|
||||
])
|
||||
.render(buf.area, buf);
|
||||
})?;
|
||||
|
||||
terminal.draw(|f| {
|
||||
let paragraph = Paragraph::new("[---- Viewport ----]");
|
||||
f.render_widget(paragraph, f.size());
|
||||
})?;
|
||||
|
||||
assert_buffer_eq!(
|
||||
terminal.backend().buffer().clone(),
|
||||
Buffer::with_lines(vec![
|
||||
"------ Line 1 ------",
|
||||
"------ Line 2 ------",
|
||||
"[---- Viewport ----]",
|
||||
" ",
|
||||
" ",
|
||||
])
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal_insert_before_scrolls_on_large_input() -> Result<(), Box<dyn Error>> {
|
||||
// When we have a terminal with 5 lines, and a single line viewport, if we insert many
|
||||
// lines before the viewport (greater than `terminal height - viewport height`) it should
|
||||
// move the viewport down to the bottom of the terminal and scroll all lines above the viewport
|
||||
// until all have been added to the buffer.
|
||||
|
||||
let backend = TestBackend::new(20, 5);
|
||||
let mut terminal = Terminal::with_options(
|
||||
backend,
|
||||
TerminalOptions {
|
||||
viewport: Viewport::Inline(1),
|
||||
},
|
||||
)?;
|
||||
|
||||
terminal.insert_before(5, |buf| {
|
||||
Paragraph::new(vec![
|
||||
"------ Line 1 ------".into(),
|
||||
"------ Line 2 ------".into(),
|
||||
"------ Line 3 ------".into(),
|
||||
"------ Line 4 ------".into(),
|
||||
"------ Line 5 ------".into(),
|
||||
])
|
||||
.render(buf.area, buf);
|
||||
})?;
|
||||
|
||||
terminal.draw(|f| {
|
||||
let paragraph = Paragraph::new("[---- Viewport ----]");
|
||||
f.render_widget(paragraph, f.size());
|
||||
})?;
|
||||
|
||||
assert_buffer_eq!(
|
||||
terminal.backend().buffer().clone(),
|
||||
Buffer::with_lines(vec![
|
||||
"------ Line 2 ------",
|
||||
"------ Line 3 ------",
|
||||
"------ Line 4 ------",
|
||||
"------ Line 5 ------",
|
||||
"[---- Viewport ----]",
|
||||
])
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal_insert_before_scrolls_on_many_inserts() -> Result<(), Box<dyn Error>> {
|
||||
// This test ensures similar behaviour to `terminal_insert_before_scrolls_on_large_input`
|
||||
// but covers a bug previously present whereby multiple small insertions
|
||||
// (less than `terminal height - viewport height`) would have disparate behaviour to one large
|
||||
// insertion. This was caused by an undocumented cap on the height to be inserted, which has now
|
||||
// been removed.
|
||||
|
||||
let backend = TestBackend::new(20, 5);
|
||||
let mut terminal = Terminal::with_options(
|
||||
backend,
|
||||
TerminalOptions {
|
||||
viewport: Viewport::Inline(1),
|
||||
},
|
||||
)?;
|
||||
|
||||
terminal.insert_before(1, |buf| {
|
||||
Paragraph::new(vec!["------ Line 1 ------".into()]).render(buf.area, buf);
|
||||
})?;
|
||||
|
||||
terminal.insert_before(1, |buf| {
|
||||
Paragraph::new(vec!["------ Line 2 ------".into()]).render(buf.area, buf);
|
||||
})?;
|
||||
|
||||
terminal.insert_before(1, |buf| {
|
||||
Paragraph::new(vec!["------ Line 3 ------".into()]).render(buf.area, buf);
|
||||
})?;
|
||||
|
||||
terminal.insert_before(1, |buf| {
|
||||
Paragraph::new(vec!["------ Line 4 ------".into()]).render(buf.area, buf);
|
||||
})?;
|
||||
|
||||
terminal.insert_before(1, |buf| {
|
||||
Paragraph::new(vec!["------ Line 5 ------".into()]).render(buf.area, buf);
|
||||
})?;
|
||||
|
||||
terminal.draw(|f| {
|
||||
let paragraph = Paragraph::new("[---- Viewport ----]");
|
||||
f.render_widget(paragraph, f.size());
|
||||
})?;
|
||||
|
||||
assert_buffer_eq!(
|
||||
terminal.backend().buffer().clone(),
|
||||
Buffer::with_lines(vec![
|
||||
"------ Line 2 ------",
|
||||
"------ Line 3 ------",
|
||||
"------ Line 4 ------",
|
||||
"------ Line 5 ------",
|
||||
"[---- Viewport ----]",
|
||||
])
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ fn list_should_shows_the_length() {
|
||||
assert_eq!(list.len(), 3);
|
||||
assert!(!list.is_empty());
|
||||
|
||||
let empty_list = List::new(vec![]);
|
||||
let empty_list = List::default();
|
||||
assert_eq!(empty_list.len(), 0);
|
||||
assert!(empty_list.is_empty());
|
||||
}
|
||||
|
||||
@@ -19,19 +19,21 @@ fn widgets_table_column_spacing_can_be_changed() {
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let size = f.size();
|
||||
let table = Table::new(vec![
|
||||
Row::new(vec!["Row11", "Row12", "Row13"]),
|
||||
Row::new(vec!["Row21", "Row22", "Row23"]),
|
||||
Row::new(vec!["Row31", "Row32", "Row33"]),
|
||||
Row::new(vec!["Row41", "Row42", "Row43"]),
|
||||
])
|
||||
let table = Table::new(
|
||||
vec![
|
||||
Row::new(vec!["Row11", "Row12", "Row13"]),
|
||||
Row::new(vec!["Row21", "Row22", "Row23"]),
|
||||
Row::new(vec!["Row31", "Row32", "Row33"]),
|
||||
Row::new(vec!["Row41", "Row42", "Row43"]),
|
||||
],
|
||||
[
|
||||
Constraint::Length(5),
|
||||
Constraint::Length(5),
|
||||
Constraint::Length(5),
|
||||
],
|
||||
)
|
||||
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.widths(&[
|
||||
Constraint::Length(5),
|
||||
Constraint::Length(5),
|
||||
Constraint::Length(5),
|
||||
])
|
||||
.column_spacing(column_spacing);
|
||||
f.render_widget(table, size);
|
||||
})
|
||||
@@ -117,15 +119,17 @@ fn widgets_table_columns_widths_can_use_fixed_length_constraints() {
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let size = f.size();
|
||||
let table = Table::new(vec![
|
||||
Row::new(vec!["Row11", "Row12", "Row13"]),
|
||||
Row::new(vec!["Row21", "Row22", "Row23"]),
|
||||
Row::new(vec!["Row31", "Row32", "Row33"]),
|
||||
Row::new(vec!["Row41", "Row42", "Row43"]),
|
||||
])
|
||||
let table = Table::new(
|
||||
vec![
|
||||
Row::new(vec!["Row11", "Row12", "Row13"]),
|
||||
Row::new(vec!["Row21", "Row22", "Row23"]),
|
||||
Row::new(vec!["Row31", "Row32", "Row33"]),
|
||||
Row::new(vec!["Row41", "Row42", "Row43"]),
|
||||
],
|
||||
widths,
|
||||
)
|
||||
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.widths(widths);
|
||||
.block(Block::default().borders(Borders::ALL));
|
||||
f.render_widget(table, size);
|
||||
})
|
||||
.unwrap();
|
||||
@@ -198,28 +202,31 @@ fn widgets_table_columns_widths_can_use_fixed_length_constraints() {
|
||||
|
||||
#[test]
|
||||
fn widgets_table_columns_widths_can_use_percentage_constraints() {
|
||||
let test_case = |widths, expected| {
|
||||
#[track_caller]
|
||||
fn test_case(widths: &[Constraint], expected: Buffer) {
|
||||
let backend = TestBackend::new(30, 10);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let size = f.size();
|
||||
let table = Table::new(vec![
|
||||
Row::new(vec!["Row11", "Row12", "Row13"]),
|
||||
Row::new(vec!["Row21", "Row22", "Row23"]),
|
||||
Row::new(vec!["Row31", "Row32", "Row33"]),
|
||||
Row::new(vec!["Row41", "Row42", "Row43"]),
|
||||
])
|
||||
let table = Table::new(
|
||||
vec![
|
||||
Row::new(vec!["Row11", "Row12", "Row13"]),
|
||||
Row::new(vec!["Row21", "Row22", "Row23"]),
|
||||
Row::new(vec!["Row31", "Row32", "Row33"]),
|
||||
Row::new(vec!["Row41", "Row42", "Row43"]),
|
||||
],
|
||||
widths,
|
||||
)
|
||||
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.widths(widths)
|
||||
.column_spacing(0);
|
||||
f.render_widget(table, size);
|
||||
})
|
||||
.unwrap();
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
};
|
||||
}
|
||||
|
||||
// columns of zero width show nothing
|
||||
test_case(
|
||||
@@ -312,15 +319,17 @@ fn widgets_table_columns_widths_can_use_mixed_constraints() {
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let size = f.size();
|
||||
let table = Table::new(vec![
|
||||
Row::new(vec!["Row11", "Row12", "Row13"]),
|
||||
Row::new(vec!["Row21", "Row22", "Row23"]),
|
||||
Row::new(vec!["Row31", "Row32", "Row33"]),
|
||||
Row::new(vec!["Row41", "Row42", "Row43"]),
|
||||
])
|
||||
let table = Table::new(
|
||||
vec![
|
||||
Row::new(vec!["Row11", "Row12", "Row13"]),
|
||||
Row::new(vec!["Row21", "Row22", "Row23"]),
|
||||
Row::new(vec!["Row31", "Row32", "Row33"]),
|
||||
Row::new(vec!["Row41", "Row42", "Row43"]),
|
||||
],
|
||||
widths,
|
||||
)
|
||||
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.widths(widths);
|
||||
.block(Block::default().borders(Borders::ALL));
|
||||
f.render_widget(table, size);
|
||||
})
|
||||
.unwrap();
|
||||
@@ -422,15 +431,17 @@ fn widgets_table_columns_widths_can_use_ratio_constraints() {
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let size = f.size();
|
||||
let table = Table::new(vec![
|
||||
Row::new(vec!["Row11", "Row12", "Row13"]),
|
||||
Row::new(vec!["Row21", "Row22", "Row23"]),
|
||||
Row::new(vec!["Row31", "Row32", "Row33"]),
|
||||
Row::new(vec!["Row41", "Row42", "Row43"]),
|
||||
])
|
||||
let table = Table::new(
|
||||
vec![
|
||||
Row::new(vec!["Row11", "Row12", "Row13"]),
|
||||
Row::new(vec!["Row21", "Row22", "Row23"]),
|
||||
Row::new(vec!["Row31", "Row32", "Row33"]),
|
||||
Row::new(vec!["Row41", "Row42", "Row43"]),
|
||||
],
|
||||
widths,
|
||||
)
|
||||
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.widths(widths)
|
||||
.column_spacing(0);
|
||||
f.render_widget(table, size);
|
||||
})
|
||||
@@ -527,20 +538,22 @@ fn widgets_table_can_have_rows_with_multi_lines() {
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let size = f.size();
|
||||
let table = Table::new(vec![
|
||||
Row::new(vec!["Row11", "Row12", "Row13"]),
|
||||
Row::new(vec!["Row21", "Row22", "Row23"]).height(2),
|
||||
Row::new(vec!["Row31", "Row32", "Row33"]),
|
||||
Row::new(vec!["Row41", "Row42", "Row43"]).height(2),
|
||||
])
|
||||
let table = Table::new(
|
||||
vec![
|
||||
Row::new(vec!["Row11", "Row12", "Row13"]),
|
||||
Row::new(vec!["Row21", "Row22", "Row23"]).height(2),
|
||||
Row::new(vec!["Row31", "Row32", "Row33"]),
|
||||
Row::new(vec!["Row41", "Row42", "Row43"]).height(2),
|
||||
],
|
||||
[
|
||||
Constraint::Length(5),
|
||||
Constraint::Length(5),
|
||||
Constraint::Length(5),
|
||||
],
|
||||
)
|
||||
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.highlight_symbol(">> ")
|
||||
.widths(&[
|
||||
Constraint::Length(5),
|
||||
Constraint::Length(5),
|
||||
Constraint::Length(5),
|
||||
])
|
||||
.column_spacing(1);
|
||||
f.render_stateful_widget(table, size, state);
|
||||
})
|
||||
@@ -621,21 +634,23 @@ fn widgets_table_enable_always_highlight_spacing() {
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let size = f.size();
|
||||
let table = Table::new(vec![
|
||||
Row::new(vec!["Row11", "Row12", "Row13"]),
|
||||
Row::new(vec!["Row21", "Row22", "Row23"]).height(2),
|
||||
Row::new(vec!["Row31", "Row32", "Row33"]),
|
||||
Row::new(vec!["Row41", "Row42", "Row43"]).height(2),
|
||||
])
|
||||
let table = Table::new(
|
||||
vec![
|
||||
Row::new(vec!["Row11", "Row12", "Row13"]),
|
||||
Row::new(vec!["Row21", "Row22", "Row23"]).height(2),
|
||||
Row::new(vec!["Row31", "Row32", "Row33"]),
|
||||
Row::new(vec!["Row41", "Row42", "Row43"]).height(2),
|
||||
],
|
||||
[
|
||||
Constraint::Length(5),
|
||||
Constraint::Length(5),
|
||||
Constraint::Length(5),
|
||||
],
|
||||
)
|
||||
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.highlight_symbol(">> ")
|
||||
.highlight_spacing(space)
|
||||
.widths(&[
|
||||
Constraint::Length(5),
|
||||
Constraint::Length(5),
|
||||
Constraint::Length(5),
|
||||
])
|
||||
.column_spacing(1);
|
||||
f.render_stateful_widget(table, size, state);
|
||||
})
|
||||
@@ -755,28 +770,31 @@ fn widgets_table_can_have_elements_styled_individually() {
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let size = f.size();
|
||||
let table = Table::new(vec![
|
||||
Row::new(vec!["Row11", "Row12", "Row13"]).style(Style::default().fg(Color::Green)),
|
||||
Row::new(vec![
|
||||
Cell::from("Row21"),
|
||||
Cell::from("Row22").style(Style::default().fg(Color::Yellow)),
|
||||
Cell::from(Line::from(vec![
|
||||
Span::raw("Row"),
|
||||
Span::styled("23", Style::default().fg(Color::Blue)),
|
||||
]))
|
||||
.style(Style::default().fg(Color::Red)),
|
||||
])
|
||||
.style(Style::default().fg(Color::LightGreen)),
|
||||
])
|
||||
let table = Table::new(
|
||||
vec![
|
||||
Row::new(vec!["Row11", "Row12", "Row13"])
|
||||
.style(Style::default().fg(Color::Green)),
|
||||
Row::new(vec![
|
||||
Cell::from("Row21"),
|
||||
Cell::from("Row22").style(Style::default().fg(Color::Yellow)),
|
||||
Cell::from(Line::from(vec![
|
||||
Span::raw("Row"),
|
||||
Span::styled("23", Style::default().fg(Color::Blue)),
|
||||
]))
|
||||
.style(Style::default().fg(Color::Red)),
|
||||
])
|
||||
.style(Style::default().fg(Color::LightGreen)),
|
||||
],
|
||||
[
|
||||
Constraint::Length(6),
|
||||
Constraint::Length(6),
|
||||
Constraint::Length(6),
|
||||
],
|
||||
)
|
||||
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
|
||||
.block(Block::default().borders(Borders::LEFT | Borders::RIGHT))
|
||||
.highlight_symbol(">> ")
|
||||
.highlight_style(Style::default().add_modifier(Modifier::BOLD))
|
||||
.widths(&[
|
||||
Constraint::Length(6),
|
||||
Constraint::Length(6),
|
||||
Constraint::Length(6),
|
||||
])
|
||||
.column_spacing(1);
|
||||
f.render_stateful_widget(table, size, &mut state);
|
||||
})
|
||||
@@ -830,15 +848,17 @@ fn widgets_table_should_render_even_if_empty() {
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let size = f.size();
|
||||
let table = Table::new(vec![])
|
||||
.header(Row::new(vec!["Head1", "Head2", "Head3"]))
|
||||
.block(Block::default().borders(Borders::LEFT | Borders::RIGHT))
|
||||
.widths(&[
|
||||
let table = Table::new(
|
||||
vec![],
|
||||
[
|
||||
Constraint::Length(6),
|
||||
Constraint::Length(6),
|
||||
Constraint::Length(6),
|
||||
])
|
||||
.column_spacing(1);
|
||||
],
|
||||
)
|
||||
.header(Row::new(vec!["Head1", "Head2", "Head3"]))
|
||||
.block(Block::default().borders(Borders::LEFT | Borders::RIGHT))
|
||||
.column_spacing(1);
|
||||
f.render_widget(table, size);
|
||||
})
|
||||
.unwrap();
|
||||
@@ -868,17 +888,19 @@ fn widgets_table_columns_dont_panic() {
|
||||
|
||||
// based on https://github.com/fdehau/tui-rs/issues/470#issuecomment-852562848
|
||||
let table1_width = 98;
|
||||
let table1 = Table::new(vec![Row::new(vec!["r1", "r2", "r3", "r4"])])
|
||||
.header(Row::new(vec!["h1", "h2", "h3", "h4"]))
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.highlight_symbol(">> ")
|
||||
.column_spacing(1)
|
||||
.widths(&[
|
||||
let table1 = Table::new(
|
||||
vec![Row::new(vec!["r1", "r2", "r3", "r4"])],
|
||||
[
|
||||
Constraint::Percentage(15),
|
||||
Constraint::Percentage(15),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(45),
|
||||
]);
|
||||
],
|
||||
)
|
||||
.header(Row::new(vec!["h1", "h2", "h3", "h4"]))
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.highlight_symbol(">> ")
|
||||
.column_spacing(1);
|
||||
|
||||
let mut state = TableState::default();
|
||||
|
||||
@@ -898,21 +920,23 @@ fn widgets_table_should_clamp_offset_if_rows_are_removed() {
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let size = f.size();
|
||||
let table = Table::new(vec![
|
||||
Row::new(vec!["Row01", "Row02", "Row03"]),
|
||||
Row::new(vec!["Row11", "Row12", "Row13"]),
|
||||
Row::new(vec!["Row21", "Row22", "Row23"]),
|
||||
Row::new(vec!["Row31", "Row32", "Row33"]),
|
||||
Row::new(vec!["Row41", "Row42", "Row43"]),
|
||||
Row::new(vec!["Row51", "Row52", "Row53"]),
|
||||
])
|
||||
let table = Table::new(
|
||||
vec![
|
||||
Row::new(vec!["Row01", "Row02", "Row03"]),
|
||||
Row::new(vec!["Row11", "Row12", "Row13"]),
|
||||
Row::new(vec!["Row21", "Row22", "Row23"]),
|
||||
Row::new(vec!["Row31", "Row32", "Row33"]),
|
||||
Row::new(vec!["Row41", "Row42", "Row43"]),
|
||||
Row::new(vec!["Row51", "Row52", "Row53"]),
|
||||
],
|
||||
[
|
||||
Constraint::Length(5),
|
||||
Constraint::Length(5),
|
||||
Constraint::Length(5),
|
||||
],
|
||||
)
|
||||
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.widths(&[
|
||||
Constraint::Length(5),
|
||||
Constraint::Length(5),
|
||||
Constraint::Length(5),
|
||||
])
|
||||
.column_spacing(1);
|
||||
f.render_stateful_widget(table, size, &mut state);
|
||||
})
|
||||
@@ -934,15 +958,17 @@ fn widgets_table_should_clamp_offset_if_rows_are_removed() {
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let size = f.size();
|
||||
let table = Table::new(vec![Row::new(vec!["Row31", "Row32", "Row33"])])
|
||||
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.widths(&[
|
||||
let table = Table::new(
|
||||
vec![Row::new(vec!["Row31", "Row32", "Row33"])],
|
||||
[
|
||||
Constraint::Length(5),
|
||||
Constraint::Length(5),
|
||||
Constraint::Length(5),
|
||||
])
|
||||
.column_spacing(1);
|
||||
],
|
||||
)
|
||||
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.column_spacing(1);
|
||||
f.render_stateful_widget(table, size, &mut state);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
use ratatui::{
|
||||
backend::TestBackend, buffer::Buffer, layout::Rect, symbols, text::Line, widgets::Tabs,
|
||||
backend::TestBackend,
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::{Style, Stylize},
|
||||
symbols,
|
||||
text::Line,
|
||||
widgets::Tabs,
|
||||
Terminal,
|
||||
};
|
||||
|
||||
@@ -43,6 +49,7 @@ fn widgets_tabs_should_truncate_the_last_item() {
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
let expected = Buffer::with_lines(vec![format!(" Tab1 {} T ", symbols::line::VERTICAL)]);
|
||||
let mut expected = Buffer::with_lines(vec![format!(" Tab1 {} T ", symbols::line::VERTICAL)]);
|
||||
expected.set_style(Rect::new(1, 0, 4, 1), Style::new().reversed());
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user