Compare commits
77 Commits
rect-offse
...
jm/widget-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f99c224fc1 | ||
|
|
8061813f32 | ||
|
|
74a32afbae | ||
|
|
4ce67fc84e | ||
|
|
df4b706674 | ||
|
|
8b447ec4d6 | ||
|
|
7a48c5b11b | ||
|
|
8cfc316bcc | ||
|
|
2f8a9363fc | ||
|
|
d92997105b | ||
|
|
42cda6d287 | ||
|
|
4f7791079e | ||
|
|
cf67ed9b88 | ||
|
|
8a60a561c9 | ||
|
|
35941809e1 | ||
|
|
73fd367a74 | ||
|
|
1de9a82b7a | ||
|
|
d6587bc6b0 | ||
|
|
f429f688da | ||
|
|
4770e71581 | ||
|
|
eef1afe915 | ||
|
|
257db6257f | ||
|
|
70df102de0 | ||
|
|
bf2036987f | ||
|
|
0b5fd6bf8e | ||
|
|
fadc73d62e | ||
|
|
fcb5d589bb | ||
|
|
4955380932 | ||
|
|
828d17a3f5 | ||
|
|
9bd89c218a | ||
|
|
2cfe82a47e | ||
|
|
1a4bb1cbb8 | ||
|
|
839cca20bf | ||
|
|
f945a0bcff | ||
|
|
eb281df974 | ||
|
|
28e81c0714 | ||
|
|
76e5fe5a9a | ||
|
|
366cbae09f | ||
|
|
ec763af851 | ||
|
|
699c2d7c8d | ||
|
|
3cc29bdada | ||
|
|
8de3d52469 | ||
|
|
b30411d1c7 | ||
|
|
aa4260f92c | ||
|
|
5f1e119563 | ||
|
|
4d1784f2de | ||
|
|
baedc39494 | ||
|
|
366c2a0e6d | ||
|
|
bef2bc1e7c | ||
|
|
64eb3913a4 | ||
|
|
e95230beda | ||
|
|
f4637d40c3 | ||
|
|
da1ade7b2e | ||
|
|
1706b0a3e4 | ||
|
|
4392759501 | ||
|
|
20fc0ddfca | ||
|
|
3687f78f6a | ||
|
|
5fbb77ad20 | ||
|
|
97ee102f17 | ||
|
|
c442dfd1ad | ||
|
|
0a164965ea | ||
|
|
bf0923473c | ||
|
|
81b96338ea | ||
|
|
2e71c1874e | ||
|
|
c75aa1990f | ||
|
|
326a461f9a | ||
|
|
f3172c59d4 | ||
|
|
bef5bcf750 | ||
|
|
11264787d0 | ||
|
|
9b3c260b76 | ||
|
|
363c4c54e8 | ||
|
|
b7778e5cd1 | ||
|
|
b5061c5250 | ||
|
|
359204c929 | ||
|
|
14461c3a35 | ||
|
|
3b002fdcab | ||
|
|
0207160784 |
@@ -1,16 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
if !(command cargo-make >/dev/null 2>&1); then # Check if cargo-make is installed
|
||||
echo Attempting to run cargo-make as part of the pre-push hook but it\'s not installed.
|
||||
echo Please install it by running the following command:
|
||||
echo
|
||||
echo " cargo install --force cargo-make"
|
||||
echo
|
||||
echo If you don\'t want to run cargo-make as part of the pre-push hook, you can run
|
||||
echo the following command instead of git push:
|
||||
echo
|
||||
echo " git push --no-verify"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cargo make ci
|
||||
5
.github/CODEOWNERS
vendored
5
.github/CODEOWNERS
vendored
@@ -5,4 +5,7 @@
|
||||
# https://git-scm.com/docs/gitignore#_pattern_format
|
||||
|
||||
# Maintainers
|
||||
* @orhun @mindoodoo @sayanarijit @joshka @kdheepak @Valentin271 @EdJoPaTo
|
||||
* @orhun @joshka @kdheepak @Valentin271 @EdJoPaTo
|
||||
|
||||
# Past maintainers
|
||||
# @mindoodoo @sayanarijit
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
This document contains a list of breaking changes in each version and some notes to help migrate
|
||||
between versions. It is compiled manually from the commit history and changelog. We also tag PRs on
|
||||
github with a [breaking change] label.
|
||||
GitHub with a [breaking change] label.
|
||||
|
||||
[breaking change]: (https://github.com/ratatui-org/ratatui/issues?q=label%3A%22breaking+change%22)
|
||||
|
||||
@@ -10,6 +10,11 @@ github with a [breaking change] label.
|
||||
|
||||
This is a quick summary of the sections below:
|
||||
|
||||
- [Unreleased](#unreleased)
|
||||
- `Rect::inner` takes `Margin` directly instead of reference
|
||||
- `Buffer::filled` takes `Cell` directly instead of reference
|
||||
- `Stylize::bg()` now accepts `Into<Color>`
|
||||
- Removed deprecated `List::start_corner`
|
||||
- [v0.26.0](#v0260)
|
||||
- `Flex::Start` is the new default flex mode for `Layout`
|
||||
- `patch_style` & `reset_style` now consume and return `Self`
|
||||
@@ -37,7 +42,7 @@ This is a quick summary of the sections below:
|
||||
- `Scrollbar`: symbols moved to `symbols` module
|
||||
- MSRV is now 1.67.0
|
||||
- [v0.22.0](#v0220)
|
||||
- serde representation of `Borders` and `Modifiers` has changed
|
||||
- `serde` representation of `Borders` and `Modifiers` has changed
|
||||
- [v0.21.0](#v0210)
|
||||
- MSRV is now 1.65.0
|
||||
- `terminal::ViewPort` is now an enum
|
||||
@@ -47,16 +52,73 @@ This is a quick summary of the sections below:
|
||||
- MSRV is now 1.63.0
|
||||
- `List` no longer ignores empty strings
|
||||
|
||||
## Unreleased
|
||||
|
||||
### `Rect::inner` takes `Margin` directly instead of reference ([#1008])
|
||||
|
||||
[#1008]: https://github.com/ratatui-org/ratatui/pull/1008
|
||||
|
||||
`Margin` needs to be passed without reference now.
|
||||
|
||||
```diff
|
||||
-let area = area.inner(&Margin {
|
||||
+let area = area.inner(Margin {
|
||||
vertical: 0,
|
||||
horizontal: 2,
|
||||
});
|
||||
```
|
||||
|
||||
### `Buffer::filled` takes `Cell` directly instead of reference ([#1148])
|
||||
|
||||
[#1148]: https://github.com/ratatui-org/ratatui/pull/1148
|
||||
|
||||
`Buffer::filled` moves the `Cell` instead of taking a reference.
|
||||
|
||||
```diff
|
||||
-Buffer::filled(area, &Cell::new("X"));
|
||||
+Buffer::filled(area, Cell::new("X"));
|
||||
```
|
||||
|
||||
### `Stylize::bg()` now accepts `Into<Color>` ([#1103])
|
||||
|
||||
[#1103]: https://github.com/ratatui-org/ratatui/pull/1103
|
||||
|
||||
Previously, `Stylize::bg()` accepted `Color` but now accepts `Into<Color>`. This allows more
|
||||
flexible types from calling scopes, though it can break some type inference in the calling scope.
|
||||
|
||||
### Remove deprecated `List::start_corner` and `layout::Corner` ([#757])
|
||||
|
||||
[#757]: https://github.com/ratatui-org/ratatui/pull/757
|
||||
|
||||
`List::start_corner` was deprecated in v0.25. Use `List::direction` and `ListDirection` instead.
|
||||
|
||||
```diff
|
||||
- list.start_corner(Corner::TopLeft);
|
||||
- list.start_corner(Corner::TopRight);
|
||||
// This is not an error, BottomRight rendered top to bottom previously
|
||||
- list.start_corner(Corner::BottomRight);
|
||||
// all becomes
|
||||
+ list.direction(ListDirection::TopToBottom);
|
||||
```
|
||||
|
||||
```diff
|
||||
- list.start_corner(Corner::BottomLeft);
|
||||
// becomes
|
||||
+ list.direction(ListDirection::BottomToTop);
|
||||
```
|
||||
|
||||
`layout::Corner` was removed entirely.
|
||||
|
||||
## [v0.26.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.26.0)
|
||||
|
||||
### `Flex::Start` is the new default flex mode for `Layout`
|
||||
### `Flex::Start` is the new default flex mode for `Layout` ([#881])
|
||||
|
||||
[#881]: https://github.com/ratatui-org/ratatui/pull/881
|
||||
|
||||
Previously, constraints would stretch to fill all available space, violating constraints if
|
||||
necessary.
|
||||
|
||||
With v0.26.0, `Flex` modes are introduced and the default is `Flex::Start`, which will align
|
||||
With v0.26.0, `Flex` modes are introduced, and the default is `Flex::Start`, which will align
|
||||
areas associated with constraints to be beginning of the area. With v0.26.0, additionally,
|
||||
`Min` constraints grow to fill excess space. These changes will allow users to build layouts
|
||||
more easily.
|
||||
@@ -74,7 +136,7 @@ existing layouts with `Flex::Start`. However, to get old behavior, use `Flex::Le
|
||||
|
||||
[#774]: https://github.com/ratatui-org/ratatui/pull/774
|
||||
|
||||
Previously, `Table::new()` accepted `IntoIterator<Item=Row<'a>>`. The argument change to
|
||||
Previously, `Table::new()` accepted `IntoIterator<Item=Row<'a>>`. The argument change to
|
||||
`IntoIterator<Item: Into<Row<'a>>>`, This allows more flexible types from calling scopes, though it
|
||||
can some break type inference in the calling scope for empty containers.
|
||||
|
||||
@@ -91,7 +153,7 @@ This can be resolved either by providing an explicit type (e.g. `Vec::<Row>::new
|
||||
|
||||
[#776]: https://github.com/ratatui-org/ratatui/pull/776
|
||||
|
||||
Previously, `Tabs::new()` accepted `Vec<T>` where `T: Into<Line<'a>>`. This allows more flexible
|
||||
Previously, `Tabs::new()` accepted `Vec<T>` where `T: Into<Line<'a>>`. This allows more flexible
|
||||
types from calling scopes, though it can break some type inference in the calling scope.
|
||||
|
||||
This typically occurs when collecting an iterator prior to calling `Tabs::new`, and can be resolved
|
||||
@@ -108,7 +170,7 @@ by removing the call to `.collect()`.
|
||||
[#751]: https://github.com/ratatui-org/ratatui/pull/751
|
||||
|
||||
The default() implementation of Table now sets the column_spacing field to 1 and the segment_size
|
||||
field to SegmentSize::None. This will affect the rendering of a small amount of apps.
|
||||
field to `SegmentSize::None`. This will affect the rendering of a small amount of apps.
|
||||
|
||||
To use the previous default values, call `table.segment_size(Default::default())` and
|
||||
`table.column_spacing(0)`.
|
||||
@@ -222,8 +284,8 @@ widget in the default configuration would not show any indication of the selecte
|
||||
|
||||
[#664]: https://github.com/ratatui-org/ratatui/pull/664
|
||||
|
||||
Previously `Table`s could be constructed without widths. In almost all cases this is an error.
|
||||
A new widths parameter is now mandatory on `Table::new()`. Existing code of the form:
|
||||
Previously `Table`s could be constructed without `widths`. In almost all cases this is an error.
|
||||
A new `widths` parameter is now mandatory on `Table::new()`. Existing code of the form:
|
||||
|
||||
```diff
|
||||
- Table::new(rows).widths(widths)
|
||||
@@ -281,7 +343,7 @@ let layout = layout::new(Direction::Vertical, [Constraint::Min(1), Constraint::M
|
||||
|
||||
## [v0.24.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.24.0)
|
||||
|
||||
### ScrollbarState field type changed from `u16` to `usize` ([#456])
|
||||
### `ScrollbarState` field type changed from `u16` to `usize` ([#456])
|
||||
|
||||
[#456]: https://github.com/ratatui-org/ratatui/pull/456
|
||||
|
||||
@@ -385,12 +447,12 @@ The MSRV of ratatui is now 1.67 due to an MSRV update in a dependency (`time`).
|
||||
|
||||
## [v0.22.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.22.0)
|
||||
|
||||
### bitflags updated to 2.3 ([#205])
|
||||
### `bitflags` updated to 2.3 ([#205])
|
||||
|
||||
[#205]: https://github.com/ratatui-org/ratatui/issues/205
|
||||
|
||||
The serde representation of bitflags has changed. Any existing serialized types that have Borders or
|
||||
Modifiers will need to be re-serialized. This is documented in the [bitflags
|
||||
The `serde` representation of `bitflags` has changed. Any existing serialized types that have Borders or
|
||||
Modifiers will need to be re-serialized. This is documented in the [`bitflags`
|
||||
changelog](https://github.com/bitflags/bitflags/blob/main/CHANGELOG.md#200-rc2)..
|
||||
|
||||
## [v0.21.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.21.0)
|
||||
@@ -422,9 +484,9 @@ let terminal = Terminal::with_options(backend, TerminalOptions {
|
||||
|
||||
[#168]: https://github.com/ratatui-org/ratatui/issues/168
|
||||
|
||||
A new type `Masked` was introduced that implements `From<Text<'a>>`. This causes any code that did
|
||||
A new type `Masked` was introduced that implements `From<Text<'a>>`. This causes any code that
|
||||
previously did not need to use type annotations to fail to compile. To fix this, annotate or call
|
||||
to_string() / to_owned() / as_str() on the value. E.g.:
|
||||
`to_string()` / `to_owned()` / `as_str()` on the value. E.g.:
|
||||
|
||||
```diff
|
||||
- let paragraph = Paragraph::new("".as_ref());
|
||||
|
||||
10107
CHANGELOG.md
10107
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -56,11 +56,9 @@ documented.
|
||||
|
||||
### Run CI tests before pushing a PR
|
||||
|
||||
We're using [cargo-husky](https://github.com/rhysd/cargo-husky) to automatically run git hooks,
|
||||
which will run `cargo make ci` before each push. To initialize the hook run `cargo test`. If
|
||||
`cargo-make` is not installed, it will provide instructions to install it for you. This will ensure
|
||||
that your code is formatted, compiles and passes all tests before you push. If you need to skip this
|
||||
check, you can use `git push --no-verify`.
|
||||
Running `cargo make ci` before pushing will perform the same checks that we do in the CI process.
|
||||
It's not mandatory to do this before pushing, however it may save you time to do so instead of
|
||||
waiting for GitHub to run the checks.
|
||||
|
||||
### Sign your commits
|
||||
|
||||
|
||||
120
Cargo.toml
120
Cargo.toml
@@ -1,11 +1,13 @@
|
||||
[package]
|
||||
name = "ratatui"
|
||||
version = "0.26.1" # crate version
|
||||
version = "0.26.3" # crate version
|
||||
authors = ["Florian Dehau <work@fdehau.com>", "The Ratatui Developers"]
|
||||
description = "A library that's all about cooking up terminal user interfaces"
|
||||
documentation = "https://docs.rs/ratatui/latest/ratatui/"
|
||||
keywords = ["tui", "terminal", "dashboard"]
|
||||
repository = "https://github.com/ratatui-org/ratatui"
|
||||
homepage = "https://ratatui.rs"
|
||||
keywords = ["tui", "terminal", "dashboard"]
|
||||
categories = ["command-line-interface"]
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
exclude = [
|
||||
@@ -23,47 +25,46 @@ rust-version = "1.74.0"
|
||||
[badges]
|
||||
|
||||
[dependencies]
|
||||
crossterm = { version = "0.27", optional = true }
|
||||
termion = { version = "3.0", optional = true }
|
||||
termwiz = { version = "0.22.0", optional = true }
|
||||
|
||||
serde = { version = "1", optional = true, features = ["derive"] }
|
||||
bitflags = "2.3"
|
||||
cassowary = "0.3"
|
||||
indoc = "2.0"
|
||||
compact_str = "0.7.1"
|
||||
crossterm = { version = "0.27", optional = true }
|
||||
document-features = { version = "0.2.7", optional = true }
|
||||
itertools = "0.12"
|
||||
lru = "0.12.0"
|
||||
paste = "1.0.2"
|
||||
serde = { version = "1", optional = true, features = ["derive"] }
|
||||
stability = "0.2.0"
|
||||
strum = { version = "0.26", features = ["derive"] }
|
||||
termion = { version = "3.0", optional = true }
|
||||
termwiz = { version = "0.22.0", optional = true }
|
||||
time = { version = "0.3.11", optional = true, features = ["local-offset"] }
|
||||
unicode-segmentation = "1.10"
|
||||
unicode-truncate = "1"
|
||||
unicode-width = "0.1"
|
||||
document-features = { version = "0.2.7", optional = true }
|
||||
lru = "0.12.0"
|
||||
stability = "0.1.1"
|
||||
compact_str = "0.7.1"
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = "1.0.71"
|
||||
argh = "0.1.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"] }
|
||||
derive_builder = "0.20.0"
|
||||
fakeit = "1.1"
|
||||
font8x8 = "0.3.1"
|
||||
indoc = "2"
|
||||
palette = "0.7.3"
|
||||
pretty_assertions = "1.4.0"
|
||||
rand = "0.8.5"
|
||||
rand_chacha = "0.3.1"
|
||||
rstest = "0.18.2"
|
||||
rstest = "0.19.0"
|
||||
serde_json = "1.0.109"
|
||||
|
||||
[lints.rust]
|
||||
unsafe_code = "forbid"
|
||||
|
||||
[lints.clippy]
|
||||
cargo = { level = "warn", priority = -1 }
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
cast_possible_truncation = "allow"
|
||||
cast_possible_wrap = "allow"
|
||||
@@ -73,7 +74,6 @@ missing_errors_doc = "allow"
|
||||
missing_panics_doc = "allow"
|
||||
module_name_repetitions = "allow"
|
||||
must_use_candidate = "allow"
|
||||
wildcard_imports = "allow"
|
||||
|
||||
# nursery or restricted
|
||||
as_underscore = "warn"
|
||||
@@ -87,11 +87,15 @@ map_err_ignore = "warn"
|
||||
missing_const_for_fn = "warn"
|
||||
mixed_read_write_in_expression = "warn"
|
||||
mod_module_files = "warn"
|
||||
needless_pass_by_ref_mut = "warn"
|
||||
needless_raw_strings = "warn"
|
||||
or_fun_call = "warn"
|
||||
redundant_type_annotations = "warn"
|
||||
rest_pat_in_fully_bound_structs = "warn"
|
||||
string_lit_chars_any = "warn"
|
||||
string_slice = "warn"
|
||||
string_to_string = "warn"
|
||||
unnecessary_self_imports = "warn"
|
||||
use_self = "warn"
|
||||
|
||||
[features]
|
||||
@@ -102,15 +106,15 @@ use_self = "warn"
|
||||
## which allows you to set the underline color of text.
|
||||
default = ["crossterm", "underline-color"]
|
||||
#! Generally an application will only use one backend, so you should only enable one of the following features:
|
||||
## enables the [`CrosstermBackend`] backend and adds a dependency on the [Crossterm crate].
|
||||
## enables the [`CrosstermBackend`](backend::CrosstermBackend) backend and adds a dependency on [`crossterm`].
|
||||
crossterm = ["dep:crossterm"]
|
||||
## enables the [`TermionBackend`] backend and adds a dependency on the [Termion crate].
|
||||
## enables the [`TermionBackend`](backend::TermionBackend) backend and adds a dependency on [`termion`].
|
||||
termion = ["dep:termion"]
|
||||
## enables the [`TermwizBackend`] backend and adds a dependency on the [Termwiz crate].
|
||||
## enables the [`TermwizBackend`](backend::TermwizBackend) backend and adds a dependency on [`termwiz`].
|
||||
termwiz = ["dep:termwiz"]
|
||||
|
||||
#! The following optional features are available for all backends:
|
||||
## enables serialization and deserialization of style and color types using the [Serde crate].
|
||||
## enables serialization and deserialization of style and color types using the [`serde`] crate.
|
||||
## This is useful if you want to save themes to a file.
|
||||
serde = ["dep:serde", "bitflags/serde", "compact_str/serde"]
|
||||
|
||||
@@ -122,12 +126,14 @@ all-widgets = ["widget-calendar"]
|
||||
|
||||
#! Widgets that add dependencies are gated behind feature flags to prevent unused transitive
|
||||
#! dependencies. The available features are:
|
||||
## enables the [`calendar`] widget module and adds a dependency on the [Time crate].
|
||||
## enables the [`calendar`](widgets::calendar) widget module and adds a dependency on [`time`].
|
||||
widget-calendar = ["dep:time"]
|
||||
|
||||
#! Underline color is only supported by the [`CrosstermBackend`] backend, and is not supported
|
||||
#! on Windows 7.
|
||||
#! The following optional features are only available for some backends:
|
||||
|
||||
## enables the backend code that sets the underline color.
|
||||
## Underline color is only supported by the [`CrosstermBackend`](backend::CrosstermBackend) backend,
|
||||
## and is not supported on Windows 7.
|
||||
underline-color = ["dep:crossterm"]
|
||||
|
||||
#! The following features are unstable and may change in the future:
|
||||
@@ -135,13 +141,13 @@ underline-color = ["dep:crossterm"]
|
||||
## Enable all unstable features.
|
||||
unstable = ["unstable-rendered-line-info", "unstable-widget-ref"]
|
||||
|
||||
## Enables the [`Paragraph::line_count`](crate::widgets::Paragraph::line_count)
|
||||
## [`Paragraph::line_width`](crate::widgets::Paragraph::line_width) methods
|
||||
## Enables the [`Paragraph::line_count`](widgets::Paragraph::line_count)
|
||||
## [`Paragraph::line_width`](widgets::Paragraph::line_width) methods
|
||||
## which are experimental and may change in the future.
|
||||
## See [Issue 293](https://github.com/ratatui-org/ratatui/issues/293) for more details.
|
||||
unstable-rendered-line-info = []
|
||||
|
||||
## Enables the `WidgetRef` and `StatefulWidgetRef` traits which are experimental and may change in
|
||||
## Enables the [`WidgetRef`](widgets::WidgetRef) and [`StatefulWidgetRef`](widgets::StatefulWidgetRef) traits which are experimental and may change in
|
||||
## the future.
|
||||
unstable-widget-ref = []
|
||||
|
||||
@@ -151,6 +157,11 @@ all-features = true
|
||||
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
# Improve benchmark consistency
|
||||
[profile.bench]
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[[bench]]
|
||||
name = "barchart"
|
||||
harness = false
|
||||
@@ -159,6 +170,10 @@ harness = false
|
||||
name = "block"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "line"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "list"
|
||||
harness = false
|
||||
@@ -186,13 +201,13 @@ required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "canvas"
|
||||
required-features = ["crossterm"]
|
||||
name = "calendar"
|
||||
required-features = ["crossterm", "widget-calendar"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "calendar"
|
||||
required-features = ["crossterm", "widget-calendar"]
|
||||
name = "canvas"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
@@ -211,6 +226,16 @@ name = "colors_rgb"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "constraint-explorer"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "constraints"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = false
|
||||
|
||||
[[example]]
|
||||
name = "custom_widget"
|
||||
required-features = ["crossterm"]
|
||||
@@ -231,6 +256,11 @@ name = "docsrs"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = false
|
||||
|
||||
[[example]]
|
||||
name = "flex"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "gauge"
|
||||
required-features = ["crossterm"]
|
||||
@@ -241,23 +271,18 @@ name = "hello_world"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "inline"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "layout"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "constraints"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = false
|
||||
|
||||
[[example]]
|
||||
name = "flex"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "constraint-explorer"
|
||||
name = "line_gauge"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
@@ -266,6 +291,12 @@ name = "list"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "minimal"
|
||||
required-features = ["crossterm"]
|
||||
# prefer to show the more featureful examples in the docs
|
||||
doc-scrape-examples = false
|
||||
|
||||
[[example]]
|
||||
name = "modifiers"
|
||||
required-features = ["crossterm"]
|
||||
@@ -317,11 +348,6 @@ name = "user_input"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "inline"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[test]]
|
||||
name = "state_serde"
|
||||
required-features = ["serde"]
|
||||
|
||||
@@ -7,11 +7,12 @@ skip_core_tasks = true
|
||||
# all features except the backend ones
|
||||
ALL_FEATURES = "all-widgets,macros,serde"
|
||||
|
||||
[env.ALL_FEATURES_FLAG]
|
||||
# Windows does not support building termion, so this avoids the build failure by providing two
|
||||
# sets of flags, one for Windows and one for other platforms.
|
||||
# Windows: --features=all-widgets,macros,serde,crossterm,termwiz,underline-color
|
||||
# Other: --features=all-widgets,macros,serde,crossterm,termion,termwiz,underline-color
|
||||
ALL_FEATURES_FLAG = { source = "${CARGO_MAKE_RUST_TARGET_OS}", default_value = "--features=all-widgets,macros,serde,crossterm,termion,termwiz,unstable", mapping = { "windows" = "--features=all-widgets,macros,serde,crossterm,termwiz,unstable" } }
|
||||
source = "${CARGO_MAKE_RUST_TARGET_OS}"
|
||||
default_value = "--features=all-widgets,macros,serde,crossterm,termion,termwiz,underline-color,unstable"
|
||||
mapping = { "windows" = "--features=all-widgets,macros,serde,crossterm,termwiz,underline-color,unstable" }
|
||||
|
||||
[tasks.default]
|
||||
alias = "ci"
|
||||
|
||||
55
README.md
55
README.md
@@ -43,10 +43,10 @@ Ratatui was forked from the [tui-rs] crate in 2023 in order to continue its deve
|
||||
|
||||
## Installation
|
||||
|
||||
Add `ratatui` and `crossterm` as dependencies to your cargo.toml:
|
||||
Add `ratatui` as a dependency to your cargo.toml:
|
||||
|
||||
```shell
|
||||
cargo add ratatui crossterm
|
||||
cargo add ratatui
|
||||
```
|
||||
|
||||
Ratatui uses [Crossterm] by default as it works on most platforms. See the [Installation]
|
||||
@@ -110,7 +110,8 @@ module] and the [Backends] section of the [Ratatui Website] for more info.
|
||||
|
||||
The drawing logic is delegated to a closure that takes a [`Frame`] instance as argument. The
|
||||
[`Frame`] provides the size of the area to draw to and allows the app to render any [`Widget`]
|
||||
using the provided [`render_widget`] method. See the [Widgets] section of the [Ratatui Website]
|
||||
using the provided [`render_widget`] method. After this closure returns, a diff is performed and
|
||||
only the changes are drawn to the terminal. See the [Widgets] section of the [Ratatui Website]
|
||||
for more info.
|
||||
|
||||
### Handling events
|
||||
@@ -125,12 +126,17 @@ Website] for more info. For example, if you are using [Crossterm], you can use t
|
||||
```rust
|
||||
use std::io::{self, stdout};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
use ratatui::{
|
||||
crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
terminal::{
|
||||
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
|
||||
},
|
||||
ExecutableCommand,
|
||||
},
|
||||
prelude::*,
|
||||
widgets::*,
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
fn main() -> io::Result<()> {
|
||||
enable_raw_mode()?;
|
||||
@@ -161,8 +167,7 @@ fn handle_events() -> io::Result<bool> {
|
||||
|
||||
fn ui(frame: &mut Frame) {
|
||||
frame.render_widget(
|
||||
Paragraph::new("Hello World!")
|
||||
.block(Block::default().title("Greeting").borders(Borders::ALL)),
|
||||
Paragraph::new("Hello World!").block(Block::bordered().title("Greeting")),
|
||||
frame.size(),
|
||||
);
|
||||
}
|
||||
@@ -206,14 +211,8 @@ fn ui(frame: &mut Frame) {
|
||||
[Constraint::Percentage(50), Constraint::Percentage(50)],
|
||||
)
|
||||
.split(main_layout[1]);
|
||||
frame.render_widget(
|
||||
Block::default().borders(Borders::ALL).title("Left"),
|
||||
inner_layout[0],
|
||||
);
|
||||
frame.render_widget(
|
||||
Block::default().borders(Borders::ALL).title("Right"),
|
||||
inner_layout[1],
|
||||
);
|
||||
frame.render_widget(Block::bordered().title("Left"), inner_layout[0]);
|
||||
frame.render_widget(Block::bordered().title("Right"), inner_layout[1]);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -327,24 +326,20 @@ Running this example produces the following output:
|
||||
[Termwiz]: https://crates.io/crates/termwiz
|
||||
[tui-rs]: https://crates.io/crates/tui
|
||||
[GitHub Sponsors]: https://github.com/sponsors/ratatui-org
|
||||
[Crate Badge]: https://img.shields.io/crates/v/ratatui?logo=rust&style=flat-square
|
||||
[License Badge]: https://img.shields.io/crates/l/ratatui?style=flat-square
|
||||
[CI Badge]:
|
||||
https://img.shields.io/github/actions/workflow/status/ratatui-org/ratatui/ci.yml?style=flat-square&logo=github
|
||||
[Crate Badge]: https://img.shields.io/crates/v/ratatui?logo=rust&style=flat-square&logoColor=E05D44&color=E05D44
|
||||
[License Badge]: https://img.shields.io/crates/l/ratatui?style=flat-square&color=1370D3
|
||||
[CI Badge]: https://img.shields.io/github/actions/workflow/status/ratatui-org/ratatui/ci.yml?style=flat-square&logo=github
|
||||
[CI Workflow]: https://github.com/ratatui-org/ratatui/actions/workflows/ci.yml
|
||||
[Codecov Badge]:
|
||||
https://img.shields.io/codecov/c/github/ratatui-org/ratatui?logo=codecov&style=flat-square&token=BAQ8SOKEST
|
||||
[Codecov Badge]: https://img.shields.io/codecov/c/github/ratatui-org/ratatui?logo=codecov&style=flat-square&token=BAQ8SOKEST&color=C43AC3&logoColor=C43AC3
|
||||
[Codecov]: https://app.codecov.io/gh/ratatui-org/ratatui
|
||||
[Deps.rs Badge]: https://deps.rs/repo/github/ratatui-org/ratatui/status.svg?style=flat-square
|
||||
[Deps.rs]: https://deps.rs/repo/github/ratatui-org/ratatui
|
||||
[Discord Badge]:
|
||||
https://img.shields.io/discord/1070692720437383208?label=discord&logo=discord&style=flat-square
|
||||
[Discord Badge]: https://img.shields.io/discord/1070692720437383208?label=discord&logo=discord&style=flat-square&color=1370D3&logoColor=1370D3
|
||||
[Discord Server]: https://discord.gg/pMCEU9hNEj
|
||||
[Docs Badge]: https://img.shields.io/docsrs/ratatui?logo=rust&style=flat-square
|
||||
[Matrix Badge]:
|
||||
https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix
|
||||
[Docs Badge]: https://img.shields.io/docsrs/ratatui?logo=rust&style=flat-square&logoColor=E05D44
|
||||
[Matrix Badge]: https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix&color=C43AC3
|
||||
[Matrix]: https://matrix.to/#/#ratatui:matrix.org
|
||||
[Sponsors Badge]: https://img.shields.io/github/sponsors/ratatui-org?logo=github&style=flat-square
|
||||
[Sponsors Badge]: https://img.shields.io/github/sponsors/ratatui-org?logo=github&style=flat-square&color=1370D3
|
||||
|
||||
<!-- cargo-rdme end -->
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
use criterion::{criterion_group, criterion_main, BatchSize, Bencher, BenchmarkId, Criterion};
|
||||
use criterion::{criterion_group, criterion_main, BatchSize, Bencher, Criterion};
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
prelude::Alignment,
|
||||
layout::{Alignment, Rect},
|
||||
widgets::{
|
||||
block::{Position, Title},
|
||||
Block, Borders, Padding, Widget,
|
||||
Block, Padding, Widget,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -13,32 +12,31 @@ use ratatui::{
|
||||
fn block(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("block");
|
||||
|
||||
for buffer_size in [
|
||||
Rect::new(0, 0, 100, 50), // vertically split screen
|
||||
Rect::new(0, 0, 200, 50), // 1080p fullscreen with medium font
|
||||
Rect::new(0, 0, 256, 256), // Max sized area
|
||||
for (width, height) in [
|
||||
(100, 50), // vertically split screen
|
||||
(200, 50), // 1080p fullscreen with medium font
|
||||
(256, 256), // Max sized area
|
||||
] {
|
||||
let buffer_area = buffer_size.area();
|
||||
let buffer_size = Rect::new(0, 0, width, height);
|
||||
|
||||
// Render an empty block
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("render_empty", buffer_area),
|
||||
format!("render_empty/{width}x{height}"),
|
||||
&Block::new(),
|
||||
|b, block| render(b, block, buffer_size),
|
||||
);
|
||||
|
||||
// Render with all features
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("render_all_feature", buffer_area),
|
||||
&Block::new()
|
||||
.borders(Borders::ALL)
|
||||
format!("render_all_feature/{width}x{height}"),
|
||||
&Block::bordered()
|
||||
.padding(Padding::new(5, 5, 2, 2))
|
||||
.title("test title")
|
||||
.title(
|
||||
Title::from("bottom left title")
|
||||
.alignment(Alignment::Right)
|
||||
.position(Position::Bottom),
|
||||
)
|
||||
.padding(Padding::new(5, 5, 2, 2)),
|
||||
),
|
||||
|b, block| render(b, block, buffer_size),
|
||||
);
|
||||
}
|
||||
|
||||
39
benches/line.rs
Normal file
39
benches/line.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use std::hint::black_box;
|
||||
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Rect},
|
||||
style::Stylize,
|
||||
text::Line,
|
||||
widgets::Widget,
|
||||
};
|
||||
|
||||
fn line_render(criterion: &mut Criterion) {
|
||||
for alignment in [Alignment::Left, Alignment::Center, Alignment::Right] {
|
||||
let mut group = criterion.benchmark_group(format!("line_render/{alignment}"));
|
||||
group.sample_size(1000);
|
||||
|
||||
let line = &Line::from(vec![
|
||||
"This".red(),
|
||||
" ".green(),
|
||||
"is".italic(),
|
||||
" ".blue(),
|
||||
"SPARTA!!".bold(),
|
||||
])
|
||||
.alignment(alignment);
|
||||
|
||||
for width in [0, 3, 4, 6, 7, 10, 42] {
|
||||
let area = Rect::new(0, 0, width, 1);
|
||||
|
||||
group.bench_function(width.to_string(), |bencher| {
|
||||
let mut buffer = Buffer::empty(area);
|
||||
bencher.iter(|| black_box(line).render(area, &mut buffer));
|
||||
});
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
}
|
||||
|
||||
criterion_group!(benches, line_render);
|
||||
criterion_main!(benches);
|
||||
37
cliff.toml
37
cliff.toml
@@ -1,4 +1,9 @@
|
||||
# configuration for https://github.com/orhun/git-cliff
|
||||
# git-cliff ~ configuration file
|
||||
# https://git-cliff.org/docs/configuration
|
||||
|
||||
[remote.github]
|
||||
owner = "ratatui-org"
|
||||
repo = "ratatui"
|
||||
|
||||
[changelog]
|
||||
# changelog header
|
||||
@@ -21,8 +26,10 @@ body = """
|
||||
{% endif -%}
|
||||
|
||||
{% macro commit(commit) -%}
|
||||
- [{{ commit.id | truncate(length=7, end="") }}]({{ "https://github.com/ratatui-org/ratatui/commit/" ~ commit.id }})
|
||||
*({{commit.scope | default(value = "uncategorized") | lower }})* {{ commit.message | upper_first }}
|
||||
- [{{ commit.id | truncate(length=7, end="") }}]({{ "https://github.com/ratatui-org/ratatui/commit/" ~ commit.id }}) \
|
||||
*({{commit.scope | default(value = "uncategorized") | lower }})* {{ commit.message | upper_first | trim }}\
|
||||
{% if commit.github.username %} by @{{ commit.github.username }}{%- endif -%}\
|
||||
{% if commit.github.pr_number %} in [#{{ commit.github.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.github.pr_number }}){%- endif %}\
|
||||
{%- if commit.breaking %} [**breaking**]{% endif %}
|
||||
{%- if commit.body %}
|
||||
|
||||
@@ -49,6 +56,28 @@ body = """
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{%- endfor %}
|
||||
|
||||
{% if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}
|
||||
### New Contributors
|
||||
{%- endif %}\
|
||||
{% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}
|
||||
* @{{ contributor.username }} made their first contribution
|
||||
{%- if contributor.pr_number %} in \
|
||||
[#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \
|
||||
{%- endif %}
|
||||
{%- endfor -%}
|
||||
|
||||
{% if version %}
|
||||
{% if previous.version %}
|
||||
**Full Changelog**: {{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }}
|
||||
{% endif %}
|
||||
{% else -%}
|
||||
{% raw %}\n{% endraw %}
|
||||
{% endif %}
|
||||
|
||||
{%- macro remote_url() -%}
|
||||
https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}\
|
||||
{% endmacro %}
|
||||
"""
|
||||
|
||||
|
||||
@@ -68,7 +97,7 @@ filter_unconventional = true
|
||||
split_commits = false
|
||||
# regex for preprocessing the commit messages
|
||||
commit_preprocessors = [
|
||||
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/ratatui-org/ratatui/issues/${2}))" },
|
||||
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "" },
|
||||
{ pattern = '(better safe shared layout cache)', replace = "perf(layout): ${1}" },
|
||||
{ pattern = '(Clarify README.md)', replace = "docs(readme): ${1}" },
|
||||
{ pattern = '(Update README.md)', replace = "docs(readme): ${1}" },
|
||||
|
||||
17
clippy.toml
Normal file
17
clippy.toml
Normal file
@@ -0,0 +1,17 @@
|
||||
avoid-breaking-exported-api = false
|
||||
|
||||
# https://rust-lang.github.io/rust-clippy/master/index.html#/multiple_crate_versions
|
||||
# ratatui -> bitflags v2.3
|
||||
# termwiz -> wezterm-blob-leases -> mac_address -> nix -> bitflags v1.3.2
|
||||
# crossterm -> all the windows- deps https://github.com/ratatui-org/ratatui/pull/1064#issuecomment-2078848980
|
||||
allowed-duplicate-crates = [
|
||||
"bitflags",
|
||||
"windows-targets",
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
@@ -1,20 +1,20 @@
|
||||
# Examples
|
||||
|
||||
This folder contains unreleased code. View the [examples for the latest release
|
||||
(0.25.0)](https://github.com/ratatui-org/ratatui/tree/v0.25.0/examples) instead.
|
||||
This folder might use unreleased code. View the examples for the latest release instead.
|
||||
|
||||
> [!WARNING]
|
||||
>
|
||||
> There are backwards incompatible changes in these examples, as they are designed to compile
|
||||
> There may be backwards incompatible changes in these examples, as they are designed to compile
|
||||
> against the `main` branch.
|
||||
>
|
||||
> There are a few workaround for this problem:
|
||||
>
|
||||
> - View the examples as they were when the latest version was release by selecting the tag that
|
||||
> matches that version. E.g. <https://github.com/ratatui-org/ratatui/tree/v0.25.0/examples>. There
|
||||
> is a combo box at the top of this page which allows you to select any previous tagged version.
|
||||
> - To view the code locally, checkout the tag using `git switch --detach v0.25.0`.
|
||||
> - Use the latest [alpha version of Ratatui]. These are released weekly on Saturdays.
|
||||
> matches that version. E.g. <https://github.com/ratatui-org/ratatui/tree/v0.26.1/examples>.
|
||||
> - If you're viewing this file on GitHub, there is a combo box at the top of this page which
|
||||
> allows you to select any previous tagged version.
|
||||
> - To view the code locally, checkout the tag. E.g. `git switch --detach v0.26.1`.
|
||||
> - Use the latest [alpha version of Ratatui] in your app. These are released weekly on Saturdays.
|
||||
> - Compile your code against the main branch either locally by adding e.g. `path = "../ratatui"` to
|
||||
> the dependency, or remotely by adding `git = "https://github.com/ratatui-org/ratatui"`
|
||||
>
|
||||
@@ -164,6 +164,18 @@ cargo run --example=gauge --features=crossterm
|
||||
|
||||
![Gauge][gauge.gif]
|
||||
|
||||
## Line Gauge
|
||||
|
||||
Demonstrates the [`Line
|
||||
Gauge`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.LineGauge.html) widget. Source:
|
||||
[line_gauge.rs](./line_gauge.rs).
|
||||
|
||||
```shell
|
||||
cargo run --example=line_gauge --features=crossterm
|
||||
```
|
||||
|
||||
![LineGauge][line_gauge.gif]
|
||||
|
||||
## Inline
|
||||
|
||||
Demonstrates how to use the
|
||||
@@ -346,6 +358,7 @@ examples/vhs/generate.bash
|
||||
[inline.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/inline.gif?raw=true
|
||||
[layout.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/layout.gif?raw=true
|
||||
[list.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/list.gif?raw=true
|
||||
[line_gauge.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/line_gauge.gif?raw=true
|
||||
[modifiers.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/modifiers.gif?raw=true
|
||||
[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
|
||||
|
||||
@@ -19,14 +19,18 @@ use std::{
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{Bar, BarChart, BarGroup, Block, Borders, Paragraph},
|
||||
backend::{Backend, CrosstermBackend},
|
||||
crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
},
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
terminal::{Frame, Terminal},
|
||||
text::{Line, Span},
|
||||
widgets::{Bar, BarChart, BarGroup, Block, Paragraph},
|
||||
};
|
||||
|
||||
struct Company<'a> {
|
||||
@@ -159,7 +163,7 @@ fn ui(frame: &mut Frame, app: &App) {
|
||||
let [left, right] = horizontal.areas(bottom);
|
||||
|
||||
let barchart = BarChart::default()
|
||||
.block(Block::default().title("Data1").borders(Borders::ALL))
|
||||
.block(Block::bordered().title("Data1"))
|
||||
.data(&app.data)
|
||||
.bar_width(9)
|
||||
.bar_style(Style::default().fg(Color::Yellow))
|
||||
@@ -217,7 +221,7 @@ fn draw_bar_with_group_labels(f: &mut Frame, app: &App, area: Rect) {
|
||||
let groups = create_groups(app, false);
|
||||
|
||||
let mut barchart = BarChart::default()
|
||||
.block(Block::default().title("Data1").borders(Borders::ALL))
|
||||
.block(Block::bordered().title("Data1"))
|
||||
.bar_width(7)
|
||||
.group_gap(3);
|
||||
|
||||
@@ -246,7 +250,7 @@ fn draw_horizontal_bars(f: &mut Frame, app: &App, area: Rect) {
|
||||
let groups = create_groups(app, true);
|
||||
|
||||
let mut barchart = BarChart::default()
|
||||
.block(Block::default().title("Data1").borders(Borders::ALL))
|
||||
.block(Block::bordered().title("Data1"))
|
||||
.bar_width(1)
|
||||
.group_gap(1)
|
||||
.bar_gap(0)
|
||||
@@ -286,15 +290,13 @@ fn draw_legend(f: &mut Frame, area: Rect) {
|
||||
"- Company B",
|
||||
Style::default().fg(Color::Yellow),
|
||||
)),
|
||||
Line::from(vec![Span::styled(
|
||||
Line::from(Span::styled(
|
||||
"- Company C",
|
||||
Style::default().fg(Color::White),
|
||||
)]),
|
||||
)),
|
||||
];
|
||||
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.style(Style::default().fg(Color::White));
|
||||
let block = Block::bordered().style(Style::default().fg(Color::White));
|
||||
let paragraph = Paragraph::new(text).block(block);
|
||||
f.render_widget(paragraph, area);
|
||||
}
|
||||
|
||||
@@ -20,14 +20,18 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
backend::CrosstermBackend,
|
||||
crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
},
|
||||
layout::{Alignment, Constraint, Layout, Rect},
|
||||
style::{Style, Stylize},
|
||||
terminal::Frame,
|
||||
text::Line,
|
||||
widgets::{
|
||||
block::{Position, Title},
|
||||
Block, BorderType, Borders, Padding, Paragraph, Wrap,
|
||||
@@ -160,23 +164,20 @@ fn render_border_type(
|
||||
frame: &mut Frame,
|
||||
area: Rect,
|
||||
) {
|
||||
let block = Block::new()
|
||||
.borders(Borders::ALL)
|
||||
let block = Block::bordered()
|
||||
.border_type(border_type)
|
||||
.title(format!("BorderType::{border_type:#?}"));
|
||||
frame.render_widget(paragraph.clone().block(block), area);
|
||||
}
|
||||
fn render_styled_borders(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
|
||||
let block = Block::new()
|
||||
.borders(Borders::ALL)
|
||||
let block = Block::bordered()
|
||||
.border_style(Style::new().blue().on_white().bold().italic())
|
||||
.title("Styled borders");
|
||||
frame.render_widget(paragraph.clone().block(block), area);
|
||||
}
|
||||
|
||||
fn render_styled_block(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
|
||||
let block = Block::new()
|
||||
.borders(Borders::ALL)
|
||||
let block = Block::bordered()
|
||||
.style(Style::new().blue().on_white().bold().italic())
|
||||
.title("Styled block");
|
||||
frame.render_widget(paragraph.clone().block(block), area);
|
||||
@@ -184,8 +185,7 @@ fn render_styled_block(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
|
||||
|
||||
// Note: this currently renders incorrectly, see https://github.com/ratatui-org/ratatui/issues/349
|
||||
fn render_styled_title(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
|
||||
let block = Block::new()
|
||||
.borders(Borders::ALL)
|
||||
let block = Block::bordered()
|
||||
.title("Styled title")
|
||||
.title_style(Style::new().blue().on_white().bold().italic());
|
||||
frame.render_widget(paragraph.clone().block(block), area);
|
||||
@@ -196,21 +196,19 @@ fn render_styled_title_content(paragraph: &Paragraph, frame: &mut Frame, area: R
|
||||
"Styled ".blue().on_white().bold().italic(),
|
||||
"title content".red().on_white().bold().italic(),
|
||||
]);
|
||||
let block = Block::new().borders(Borders::ALL).title(title);
|
||||
let block = Block::bordered().title(title);
|
||||
frame.render_widget(paragraph.clone().block(block), area);
|
||||
}
|
||||
|
||||
fn render_multiple_titles(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
|
||||
let block = Block::new()
|
||||
.borders(Borders::ALL)
|
||||
let block = Block::bordered()
|
||||
.title("Multiple".blue().on_white().bold().italic())
|
||||
.title("Titles".red().on_white().bold().italic());
|
||||
frame.render_widget(paragraph.clone().block(block), area);
|
||||
}
|
||||
|
||||
fn render_multiple_title_positions(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
|
||||
let block = Block::new()
|
||||
.borders(Borders::ALL)
|
||||
let block = Block::bordered()
|
||||
.title(
|
||||
Title::from("top left")
|
||||
.position(Position::Top)
|
||||
@@ -245,16 +243,15 @@ fn render_multiple_title_positions(paragraph: &Paragraph, frame: &mut Frame, are
|
||||
}
|
||||
|
||||
fn render_padding(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
|
||||
let block = Block::new()
|
||||
.borders(Borders::ALL)
|
||||
.title("Padding")
|
||||
.padding(Padding::new(5, 10, 1, 2));
|
||||
let block = Block::bordered()
|
||||
.padding(Padding::new(5, 10, 1, 2))
|
||||
.title("Padding");
|
||||
frame.render_widget(paragraph.clone().block(block), area);
|
||||
}
|
||||
|
||||
fn render_nested_blocks(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
|
||||
let outer_block = Block::new().borders(Borders::ALL).title("Outer block");
|
||||
let inner_block = Block::new().borders(Borders::ALL).title("Inner block");
|
||||
let outer_block = Block::bordered().title("Outer block");
|
||||
let inner_block = Block::bordered().title("Inner block");
|
||||
let inner = outer_block.inner(area);
|
||||
frame.render_widget(outer_block, area);
|
||||
frame.render_widget(paragraph.clone().block(inner_block), inner);
|
||||
|
||||
@@ -13,16 +13,20 @@
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
#![allow(clippy::wildcard_imports)]
|
||||
|
||||
use std::{error::Error, io};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
use ratatui::{
|
||||
backend::CrosstermBackend,
|
||||
crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
},
|
||||
layout::{Constraint, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
widgets::calendar::{CalendarEventStore, DateStyler, Monthly},
|
||||
Frame, Terminal,
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::calendar::*};
|
||||
use time::{Date, Month, OffsetDateTime};
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
@@ -52,8 +56,8 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw(f: &mut Frame) {
|
||||
let app_area = f.size();
|
||||
fn draw(frame: &mut Frame) {
|
||||
let app_area = frame.size();
|
||||
|
||||
let calarea = Rect {
|
||||
x: app_area.x + 1,
|
||||
@@ -80,7 +84,7 @@ fn draw(f: &mut Frame) {
|
||||
});
|
||||
for col in cols {
|
||||
let cal = cals::get_cal(start.month(), start.year(), &list);
|
||||
f.render_widget(cal, col);
|
||||
frame.render_widget(cal, col);
|
||||
start = start.replace_month(start.month().next()).unwrap();
|
||||
}
|
||||
}
|
||||
@@ -166,6 +170,7 @@ fn make_dates(current_year: i32) -> CalendarEventStore {
|
||||
}
|
||||
|
||||
mod cals {
|
||||
#[allow(clippy::wildcard_imports)]
|
||||
use super::*;
|
||||
|
||||
pub fn get_cal<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
|
||||
|
||||
@@ -13,21 +13,26 @@
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
#![allow(clippy::wildcard_imports)]
|
||||
|
||||
use std::{
|
||||
io::{self, stdout, Stdout},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
};
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{canvas::*, *},
|
||||
backend::CrosstermBackend,
|
||||
crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
},
|
||||
layout::{Constraint, Layout, Rect},
|
||||
style::{Color, Stylize},
|
||||
symbols::Marker,
|
||||
terminal::{Frame, Terminal},
|
||||
widgets::{
|
||||
canvas::{Canvas, Circle, Map, MapResolution, Rectangle},
|
||||
Block, Widget,
|
||||
},
|
||||
};
|
||||
|
||||
fn main() -> io::Result<()> {
|
||||
@@ -137,7 +142,7 @@ impl App {
|
||||
|
||||
fn map_canvas(&self) -> impl Widget + '_ {
|
||||
Canvas::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("World"))
|
||||
.block(Block::bordered().title("World"))
|
||||
.marker(self.marker)
|
||||
.paint(|ctx| {
|
||||
ctx.draw(&Map {
|
||||
@@ -152,7 +157,7 @@ impl App {
|
||||
|
||||
fn pong_canvas(&self) -> impl Widget + '_ {
|
||||
Canvas::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("Pong"))
|
||||
.block(Block::bordered().title("Pong"))
|
||||
.marker(self.marker)
|
||||
.paint(|ctx| {
|
||||
ctx.draw(&self.ball);
|
||||
@@ -167,7 +172,7 @@ impl App {
|
||||
let bottom = 0.0;
|
||||
let top = f64::from(area.height).mul_add(2.0, -4.0);
|
||||
Canvas::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("Rects"))
|
||||
.block(Block::bordered().title("Rects"))
|
||||
.marker(self.marker)
|
||||
.x_bounds([left, right])
|
||||
.y_bounds([bottom, top])
|
||||
|
||||
@@ -19,14 +19,19 @@ use std::{
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{block::Title, Axis, Block, Borders, Chart, Dataset, GraphType, LegendPosition},
|
||||
backend::{Backend, CrosstermBackend},
|
||||
crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
},
|
||||
layout::{Alignment, Constraint, Layout, Rect},
|
||||
style::{Color, Modifier, Style, Stylize},
|
||||
symbols::{self, Marker},
|
||||
terminal::{Frame, Terminal},
|
||||
text::Span,
|
||||
widgets::{block::Title, Axis, Block, Chart, Dataset, GraphType, LegendPosition},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -184,11 +189,7 @@ fn render_chart1(f: &mut Frame, area: Rect, app: &App) {
|
||||
];
|
||||
|
||||
let chart = Chart::new(datasets)
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Chart 1".cyan().bold())
|
||||
.borders(Borders::ALL),
|
||||
)
|
||||
.block(Block::bordered().title("Chart 1".cyan().bold()))
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
.title("X Axis")
|
||||
@@ -217,13 +218,11 @@ fn render_line_chart(f: &mut Frame, area: Rect) {
|
||||
|
||||
let chart = Chart::new(datasets)
|
||||
.block(
|
||||
Block::default()
|
||||
.title(
|
||||
Title::default()
|
||||
.content("Line chart".cyan().bold())
|
||||
.alignment(Alignment::Center),
|
||||
)
|
||||
.borders(Borders::ALL),
|
||||
Block::bordered().title(
|
||||
Title::default()
|
||||
.content("Line chart".cyan().bold())
|
||||
.alignment(Alignment::Center),
|
||||
),
|
||||
)
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
@@ -269,7 +268,7 @@ fn render_scatter(f: &mut Frame, area: Rect) {
|
||||
|
||||
let chart = Chart::new(datasets)
|
||||
.block(
|
||||
Block::new().borders(Borders::all()).title(
|
||||
Block::bordered().title(
|
||||
Title::default()
|
||||
.content("Scatter chart".cyan().bold())
|
||||
.alignment(Alignment::Center),
|
||||
|
||||
@@ -23,14 +23,18 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
backend::{Backend, CrosstermBackend},
|
||||
crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
},
|
||||
layout::{Alignment, Constraint, Layout, Rect},
|
||||
style::{Color, Style, Stylize},
|
||||
terminal::{Frame, Terminal},
|
||||
text::Line,
|
||||
widgets::{Block, Borders, Paragraph},
|
||||
};
|
||||
|
||||
@@ -230,12 +234,12 @@ fn render_indexed_colors(frame: &mut Frame, area: Rect) {
|
||||
}
|
||||
|
||||
fn title_block(title: String) -> Block<'static> {
|
||||
Block::default()
|
||||
Block::new()
|
||||
.borders(Borders::TOP)
|
||||
.border_style(Style::new().dark_gray())
|
||||
.title(title)
|
||||
.title_alignment(Alignment::Center)
|
||||
.border_style(Style::new().dark_gray())
|
||||
.title_style(Style::new().reset())
|
||||
.title(title)
|
||||
}
|
||||
|
||||
fn render_indexed_grayscale(frame: &mut Frame, area: Rect) {
|
||||
|
||||
@@ -33,13 +33,21 @@ use std::{
|
||||
};
|
||||
|
||||
use color_eyre::{config::HookBuilder, eyre, Result};
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode, KeyEventKind},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
};
|
||||
use palette::{convert::FromColorUnclamped, Okhsv, Srgb};
|
||||
use ratatui::prelude::*;
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
buffer::Buffer,
|
||||
crossterm::{
|
||||
event::{self, Event, KeyCode, KeyEventKind},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
},
|
||||
layout::{Constraint, Layout, Rect},
|
||||
style::Color,
|
||||
terminal::Terminal,
|
||||
text::Text,
|
||||
widgets::Widget,
|
||||
};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct App {
|
||||
@@ -141,8 +149,7 @@ impl App {
|
||||
/// to update the colors to render.
|
||||
impl Widget for &mut App {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
#[allow(clippy::enum_glob_use)]
|
||||
use Constraint::*;
|
||||
use Constraint::{Length, Min};
|
||||
let [top, colors] = Layout::vertical([Length(1), Min(0)]).areas(area);
|
||||
let [title, fps] = Layout::horizontal([Min(0), Length(8)]).areas(top);
|
||||
Text::from("colors_rgb example. Press q to quit")
|
||||
|
||||
@@ -13,23 +13,30 @@
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
#![allow(clippy::enum_glob_use, clippy::wildcard_imports)]
|
||||
|
||||
use std::io::{self, stdout};
|
||||
|
||||
use color_eyre::{config::HookBuilder, Result};
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode, KeyEventKind},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use ratatui::{
|
||||
layout::{Constraint::*, Flex},
|
||||
prelude::*,
|
||||
style::palette::tailwind::*,
|
||||
symbols::line,
|
||||
widgets::{Block, Paragraph, Wrap},
|
||||
backend::{Backend, CrosstermBackend},
|
||||
buffer::Buffer,
|
||||
crossterm::{
|
||||
event::{self, Event, KeyCode, KeyEventKind},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
},
|
||||
layout::{
|
||||
Constraint::{self, Fill, Length, Max, Min, Percentage, Ratio},
|
||||
Flex, Layout, Rect,
|
||||
},
|
||||
style::{
|
||||
palette::tailwind::{BLUE, SKY, SLATE, STONE},
|
||||
Color, Style, Stylize,
|
||||
},
|
||||
symbols::{self, line},
|
||||
terminal::Terminal,
|
||||
text::{Line, Span, Text},
|
||||
widgets::{Block, Paragraph, Widget, Wrap},
|
||||
};
|
||||
use strum::{Display, EnumIter, FromRepr};
|
||||
|
||||
@@ -123,24 +130,23 @@ impl App {
|
||||
}
|
||||
|
||||
fn handle_events(&mut self) -> Result<()> {
|
||||
use KeyCode::*;
|
||||
match event::read()? {
|
||||
Event::Key(key) if key.kind == KeyEventKind::Press => match key.code {
|
||||
Char('q') | Esc => self.exit(),
|
||||
Char('1') => self.swap_constraint(ConstraintName::Min),
|
||||
Char('2') => self.swap_constraint(ConstraintName::Max),
|
||||
Char('3') => self.swap_constraint(ConstraintName::Length),
|
||||
Char('4') => self.swap_constraint(ConstraintName::Percentage),
|
||||
Char('5') => self.swap_constraint(ConstraintName::Ratio),
|
||||
Char('6') => self.swap_constraint(ConstraintName::Fill),
|
||||
Char('+') => self.increment_spacing(),
|
||||
Char('-') => self.decrement_spacing(),
|
||||
Char('x') => self.delete_block(),
|
||||
Char('a') => self.insert_block(),
|
||||
Char('k') | Up => self.increment_value(),
|
||||
Char('j') | Down => self.decrement_value(),
|
||||
Char('h') | Left => self.prev_block(),
|
||||
Char('l') | Right => self.next_block(),
|
||||
KeyCode::Char('q') | KeyCode::Esc => self.exit(),
|
||||
KeyCode::Char('1') => self.swap_constraint(ConstraintName::Min),
|
||||
KeyCode::Char('2') => self.swap_constraint(ConstraintName::Max),
|
||||
KeyCode::Char('3') => self.swap_constraint(ConstraintName::Length),
|
||||
KeyCode::Char('4') => self.swap_constraint(ConstraintName::Percentage),
|
||||
KeyCode::Char('5') => self.swap_constraint(ConstraintName::Ratio),
|
||||
KeyCode::Char('6') => self.swap_constraint(ConstraintName::Fill),
|
||||
KeyCode::Char('+') => self.increment_spacing(),
|
||||
KeyCode::Char('-') => self.decrement_spacing(),
|
||||
KeyCode::Char('x') => self.delete_block(),
|
||||
KeyCode::Char('a') => self.insert_block(),
|
||||
KeyCode::Char('k') | KeyCode::Up => self.increment_value(),
|
||||
KeyCode::Char('j') | KeyCode::Down => self.decrement_value(),
|
||||
KeyCode::Char('h') | KeyCode::Left => self.prev_block(),
|
||||
KeyCode::Char('l') | KeyCode::Right => self.next_block(),
|
||||
_ => {}
|
||||
},
|
||||
_ => {}
|
||||
@@ -436,7 +442,7 @@ impl ConstraintBlock {
|
||||
} else {
|
||||
main_color
|
||||
};
|
||||
Block::default()
|
||||
Block::new()
|
||||
.fg(Self::TEXT_COLOR)
|
||||
.bg(selected_color)
|
||||
.render(area, buf);
|
||||
|
||||
@@ -13,17 +13,30 @@
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
#![allow(clippy::enum_glob_use, clippy::wildcard_imports)]
|
||||
|
||||
use std::io::{self, stdout};
|
||||
|
||||
use color_eyre::{config::HookBuilder, Result};
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
buffer::Buffer,
|
||||
crossterm::{
|
||||
event::{self, Event, KeyCode, KeyEventKind},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
},
|
||||
layout::{
|
||||
Constraint::{self, Fill, Length, Max, Min, Percentage, Ratio},
|
||||
Layout, Rect,
|
||||
},
|
||||
style::{palette::tailwind, Color, Modifier, Style, Stylize},
|
||||
symbols,
|
||||
terminal::Terminal,
|
||||
text::Line,
|
||||
widgets::{
|
||||
Block, Padding, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, StatefulWidget,
|
||||
Tabs, Widget,
|
||||
},
|
||||
};
|
||||
use ratatui::{layout::Constraint::*, prelude::*, style::palette::tailwind, widgets::*};
|
||||
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
|
||||
|
||||
const SPACER_HEIGHT: u16 = 0;
|
||||
@@ -108,15 +121,17 @@ impl App {
|
||||
|
||||
fn handle_events(&mut self) -> Result<()> {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
use KeyCode::*;
|
||||
if key.kind != KeyEventKind::Press {
|
||||
return Ok(());
|
||||
}
|
||||
match key.code {
|
||||
Char('q') | Esc => self.quit(),
|
||||
Char('l') | Right => self.next(),
|
||||
Char('h') | Left => self.previous(),
|
||||
Char('j') | Down => self.down(),
|
||||
Char('k') | Up => self.up(),
|
||||
Char('g') | Home => self.top(),
|
||||
Char('G') | End => self.bottom(),
|
||||
KeyCode::Char('q') | KeyCode::Esc => self.quit(),
|
||||
KeyCode::Char('l') | KeyCode::Right => self.next(),
|
||||
KeyCode::Char('h') | KeyCode::Left => self.previous(),
|
||||
KeyCode::Char('j') | KeyCode::Down => self.down(),
|
||||
KeyCode::Char('k') | KeyCode::Up => self.up(),
|
||||
KeyCode::Char('g') | KeyCode::Home => self.top(),
|
||||
KeyCode::Char('G') | KeyCode::End => self.bottom(),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
@@ -194,7 +209,7 @@ impl App {
|
||||
);
|
||||
Paragraph::new(width_bar.dark_gray())
|
||||
.centered()
|
||||
.block(Block::default().padding(Padding {
|
||||
.block(Block::new().padding(Padding {
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 1,
|
||||
|
||||
@@ -15,15 +15,23 @@
|
||||
|
||||
use std::{error::Error, io, ops::ControlFlow, time::Duration};
|
||||
|
||||
use crossterm::{
|
||||
event::{
|
||||
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, MouseButton, MouseEvent,
|
||||
MouseEventKind,
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
buffer::Buffer,
|
||||
crossterm::{
|
||||
event::{
|
||||
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, MouseButton, MouseEvent,
|
||||
MouseEventKind,
|
||||
},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
layout::{Constraint, Layout, Rect},
|
||||
style::{Color, Style},
|
||||
terminal::{Frame, Terminal},
|
||||
text::Line,
|
||||
widgets::{Paragraph, Widget},
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::Paragraph};
|
||||
|
||||
/// A custom widget that renders a button with a label, theme and state.
|
||||
#[derive(Debug, Clone)]
|
||||
|
||||
@@ -4,12 +4,15 @@ use std::{
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
},
|
||||
terminal::Terminal,
|
||||
};
|
||||
use ratatui::prelude::*;
|
||||
|
||||
use crate::{app::App, ui};
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
use std::{error::Error, io, sync::mpsc, thread, time::Duration};
|
||||
|
||||
use ratatui::prelude::*;
|
||||
use termion::{
|
||||
event::Key,
|
||||
input::{MouseTerminal, TermRead},
|
||||
raw::IntoRawMode,
|
||||
screen::IntoAlternateScreen,
|
||||
use ratatui::{
|
||||
backend::{Backend, TermionBackend},
|
||||
terminal::Terminal,
|
||||
termion::{
|
||||
event::Key,
|
||||
input::{MouseTerminal, TermRead},
|
||||
raw::IntoRawMode,
|
||||
screen::IntoAlternateScreen,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{app::App, ui};
|
||||
|
||||
@@ -3,10 +3,13 @@ use std::{
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use ratatui::prelude::*;
|
||||
use termwiz::{
|
||||
input::{InputEvent, KeyCode},
|
||||
terminal::Terminal as TermwizTerminal,
|
||||
use ratatui::{
|
||||
backend::TermwizBackend,
|
||||
terminal::Terminal,
|
||||
termwiz::{
|
||||
input::{InputEvent, KeyCode},
|
||||
terminal::Terminal as TermwizTerminal,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{app::App, ui};
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
#[allow(clippy::wildcard_imports)]
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{canvas::*, *},
|
||||
layout::{Constraint, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
symbols,
|
||||
terminal::Frame,
|
||||
text::{self, Span},
|
||||
widgets::{
|
||||
canvas::{self, Canvas, Circle, Map, MapResolution, Rectangle},
|
||||
Axis, BarChart, Block, Cell, Chart, Dataset, Gauge, LineGauge, List, ListItem, Paragraph,
|
||||
Row, Sparkline, Table, Tabs, Wrap,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::app::App;
|
||||
@@ -14,7 +21,7 @@ pub fn draw(f: &mut Frame, app: &mut App) {
|
||||
.iter()
|
||||
.map(|t| text::Line::from(Span::styled(*t, Style::default().fg(Color::Green))))
|
||||
.collect::<Tabs>()
|
||||
.block(Block::default().borders(Borders::ALL).title(app.title))
|
||||
.block(Block::bordered().title(app.title))
|
||||
.highlight_style(Style::default().fg(Color::Yellow))
|
||||
.select(app.tabs.index);
|
||||
f.render_widget(tabs, chunks[0]);
|
||||
@@ -46,12 +53,12 @@ fn draw_gauges(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
])
|
||||
.margin(1)
|
||||
.split(area);
|
||||
let block = Block::default().borders(Borders::ALL).title("Graphs");
|
||||
let block = Block::bordered().title("Graphs");
|
||||
f.render_widget(block, area);
|
||||
|
||||
let label = format!("{:.2}%", app.progress * 100.0);
|
||||
let gauge = Gauge::default()
|
||||
.block(Block::default().title("Gauge:"))
|
||||
.block(Block::new().title("Gauge:"))
|
||||
.gauge_style(
|
||||
Style::default()
|
||||
.fg(Color::Magenta)
|
||||
@@ -64,7 +71,7 @@ fn draw_gauges(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
f.render_widget(gauge, chunks[0]);
|
||||
|
||||
let sparkline = Sparkline::default()
|
||||
.block(Block::default().title("Sparkline:"))
|
||||
.block(Block::new().title("Sparkline:"))
|
||||
.style(Style::default().fg(Color::Green))
|
||||
.data(&app.sparkline.points)
|
||||
.bar_set(if app.enhanced_graphics {
|
||||
@@ -75,8 +82,8 @@ fn draw_gauges(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
f.render_widget(sparkline, chunks[1]);
|
||||
|
||||
let line_gauge = LineGauge::default()
|
||||
.block(Block::default().title("LineGauge:"))
|
||||
.gauge_style(Style::default().fg(Color::Magenta))
|
||||
.block(Block::new().title("LineGauge:"))
|
||||
.filled_style(Style::default().fg(Color::Magenta))
|
||||
.line_set(if app.enhanced_graphics {
|
||||
symbols::line::THICK
|
||||
} else {
|
||||
@@ -110,7 +117,7 @@ fn draw_charts(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
.map(|i| ListItem::new(vec![text::Line::from(Span::raw(*i))]))
|
||||
.collect();
|
||||
let tasks = List::new(tasks)
|
||||
.block(Block::default().borders(Borders::ALL).title("List"))
|
||||
.block(Block::bordered().title("List"))
|
||||
.highlight_style(Style::default().add_modifier(Modifier::BOLD))
|
||||
.highlight_symbol("> ");
|
||||
f.render_stateful_widget(tasks, chunks[0], &mut app.tasks.state);
|
||||
@@ -138,12 +145,12 @@ fn draw_charts(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
ListItem::new(content)
|
||||
})
|
||||
.collect();
|
||||
let logs = List::new(logs).block(Block::default().borders(Borders::ALL).title("List"));
|
||||
let logs = List::new(logs).block(Block::bordered().title("List"));
|
||||
f.render_stateful_widget(logs, chunks[1], &mut app.logs.state);
|
||||
}
|
||||
|
||||
let barchart = BarChart::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("Bar chart"))
|
||||
.block(Block::bordered().title("Bar chart"))
|
||||
.data(&app.barchart)
|
||||
.bar_width(3)
|
||||
.bar_gap(2)
|
||||
@@ -195,14 +202,12 @@ fn draw_charts(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
];
|
||||
let chart = Chart::new(datasets)
|
||||
.block(
|
||||
Block::default()
|
||||
.title(Span::styled(
|
||||
"Chart",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
))
|
||||
.borders(Borders::ALL),
|
||||
Block::bordered().title(Span::styled(
|
||||
"Chart",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)),
|
||||
)
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
@@ -254,7 +259,7 @@ fn draw_text(f: &mut Frame, area: Rect) {
|
||||
"One more thing is that it should display unicode characters: 10€"
|
||||
),
|
||||
];
|
||||
let block = Block::default().borders(Borders::ALL).title(Span::styled(
|
||||
let block = Block::bordered().title(Span::styled(
|
||||
"Footer",
|
||||
Style::default()
|
||||
.fg(Color::Magenta)
|
||||
@@ -292,11 +297,11 @@ fn draw_second_tab(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.bottom_margin(1),
|
||||
)
|
||||
.block(Block::default().title("Servers").borders(Borders::ALL));
|
||||
.block(Block::bordered().title("Servers"));
|
||||
f.render_widget(table, chunks[0]);
|
||||
|
||||
let map = Canvas::default()
|
||||
.block(Block::default().title("World").borders(Borders::ALL))
|
||||
.block(Block::bordered().title("World"))
|
||||
.paint(|ctx| {
|
||||
ctx.draw(&Map {
|
||||
color: Color::White,
|
||||
@@ -390,6 +395,6 @@ fn draw_third_tab(f: &mut Frame, _app: &mut App, area: Rect) {
|
||||
Constraint::Ratio(1, 3),
|
||||
],
|
||||
)
|
||||
.block(Block::default().title("Colors").borders(Borders::ALL));
|
||||
.block(Block::bordered().title("Colors"));
|
||||
f.render_widget(table, chunks[0]);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,24 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use color_eyre::{eyre::Context, Result};
|
||||
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind};
|
||||
use itertools::Itertools;
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
use ratatui::{
|
||||
backend::Backend,
|
||||
buffer::Buffer,
|
||||
crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind},
|
||||
layout::{Constraint, Layout, Rect},
|
||||
style::Color,
|
||||
terminal::Terminal,
|
||||
text::{Line, Span},
|
||||
widgets::{Block, Tabs, Widget},
|
||||
};
|
||||
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
|
||||
|
||||
use crate::{destroy, tabs::*, term, THEME};
|
||||
use crate::{
|
||||
destroy,
|
||||
tabs::{AboutTab, EmailTab, RecipeTab, TracerouteTab, WeatherTab},
|
||||
term, THEME,
|
||||
};
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct App {
|
||||
@@ -38,14 +50,10 @@ enum Tab {
|
||||
}
|
||||
|
||||
pub fn run(terminal: &mut Terminal<impl Backend>) -> Result<()> {
|
||||
App::new().run(terminal)
|
||||
App::default().run(terminal)
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Run the app until the user quits.
|
||||
pub fn run(&mut self, terminal: &mut Terminal<impl Backend>) -> Result<()> {
|
||||
while self.is_running() {
|
||||
@@ -86,14 +94,13 @@ impl App {
|
||||
}
|
||||
|
||||
fn handle_key_press(&mut self, key: KeyEvent) {
|
||||
use KeyCode::*;
|
||||
match key.code {
|
||||
Char('q') | Esc => self.mode = Mode::Quit,
|
||||
Char('h') | Left => self.prev_tab(),
|
||||
Char('l') | Right => self.next_tab(),
|
||||
Char('k') | Up => self.prev(),
|
||||
Char('j') | Down => self.next(),
|
||||
Char('d') | Delete => self.destroy(),
|
||||
KeyCode::Char('q') | KeyCode::Esc => self.mode = Mode::Quit,
|
||||
KeyCode::Char('h') | KeyCode::Left => self.prev_tab(),
|
||||
KeyCode::Char('l') | KeyCode::Right => self.next_tab(),
|
||||
KeyCode::Char('k') | KeyCode::Up => self.prev(),
|
||||
KeyCode::Char('j') | KeyCode::Down => self.next(),
|
||||
KeyCode::Char('d') | KeyCode::Delete => self.destroy(),
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -20,7 +20,16 @@
|
||||
//!
|
||||
//! ```rust
|
||||
//! use anyhow::Result;
|
||||
//! use ratatui::prelude::*;
|
||||
//! use ratatui::{
|
||||
//! backend::{self, Backend, CrosstermBackend},
|
||||
//! buffer::{self, Buffer},
|
||||
//! layout::{self, Alignment, Constraint, Direction, Layout, Margin, Rect},
|
||||
//! style::{self, Color, Modifier, Style, Styled, Stylize},
|
||||
//! symbols::{self, Marker},
|
||||
//! terminal::{CompletedFrame, Frame, Terminal, TerminalOptions, Viewport},
|
||||
//! text::{self, Line, Masked, Span, Text},
|
||||
//! widgets::{block::BlockExt, StatefulWidget, Widget},
|
||||
//! };
|
||||
//! use tui_big_text::{BigTextBuilder, PixelSize};
|
||||
//!
|
||||
//! fn render(frame: &mut Frame) -> Result<()> {
|
||||
@@ -50,7 +59,13 @@ use std::cmp::min;
|
||||
|
||||
use derive_builder::Builder;
|
||||
use font8x8::UnicodeFonts;
|
||||
use ratatui::{prelude::*, text::StyledGrapheme};
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::Style,
|
||||
text::{Line, StyledGrapheme},
|
||||
widgets::Widget,
|
||||
};
|
||||
|
||||
#[allow(unused)]
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Default)]
|
||||
@@ -79,7 +94,16 @@ pub enum PixelSize {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::prelude::*;
|
||||
/// use ratatui::{
|
||||
/// backend::{self, Backend, CrosstermBackend},
|
||||
/// buffer::{self, Buffer},
|
||||
/// layout::{self, Alignment, Constraint, Direction, Layout, Margin, Rect},
|
||||
/// style::{self, Color, Modifier, Style, Styled, Stylize},
|
||||
/// symbols::{self, Marker},
|
||||
/// terminal::{CompletedFrame, Frame, Terminal, TerminalOptions, Viewport},
|
||||
/// text::{self, Line, Masked, Span, Text},
|
||||
/// widgets::{block::BlockExt, StatefulWidget, Widget},
|
||||
/// };
|
||||
/// use tui_big_text::{BigTextBuilder, PixelSize};
|
||||
///
|
||||
/// BigText::builder()
|
||||
@@ -276,7 +300,7 @@ fn render_glyph(glyph: [u8; 8], area: Rect, buf: &mut Buffer, pixel_size: PixelS
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ratatui::assert_buffer_eq;
|
||||
use ratatui::style::Stylize;
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -308,7 +332,7 @@ mod tests {
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 80, 8));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines(vec![
|
||||
let expected = Buffer::with_lines([
|
||||
" ████ ██ ███ ████ ██ ",
|
||||
"██ ██ ██ ██ ",
|
||||
"███ ███ █████ ███ ██ ██ ████ ██ ███ █████ ████ ",
|
||||
@@ -318,7 +342,7 @@ mod tests {
|
||||
" ████ ████ ██ ██ ██ ████ ████ ███████ ████ ██ ██ ████ ",
|
||||
" █████ ",
|
||||
]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -329,7 +353,7 @@ mod tests {
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 70, 6));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines(vec![
|
||||
let expected = Buffer::with_lines([
|
||||
"██████ █ ███",
|
||||
"█ ██ █ ██ ██",
|
||||
" ██ ██ ███ ██ ██ █████ ████ ████ █████ ████ ██",
|
||||
@@ -337,7 +361,7 @@ mod tests {
|
||||
" ██ ██ ██ ██ ██ ██ ██ ██ █████ ██ ██████ ██ ██",
|
||||
" ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ █ ██ ██ ██",
|
||||
]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -348,7 +372,7 @@ mod tests {
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 40, 16));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines(vec![
|
||||
let expected = Buffer::with_lines([
|
||||
"██ ██ ███ █ ██ ",
|
||||
"███ ███ ██ ██ ",
|
||||
"███████ ██ ██ ██ █████ ███ ",
|
||||
@@ -366,7 +390,7 @@ mod tests {
|
||||
"███████ ████ ██ ██ ████ █████ ",
|
||||
" ",
|
||||
]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -378,18 +402,17 @@ mod tests {
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 48, 8));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let mut expected = Buffer::with_lines(vec![
|
||||
" ████ █ ███ ███ ",
|
||||
"██ ██ ██ ██ ██ ",
|
||||
"███ █████ ██ ██ ██ ████ ██ ",
|
||||
" ███ ██ ██ ██ ██ ██ ██ █████ ",
|
||||
" ███ ██ ██ ██ ██ ██████ ██ ██ ",
|
||||
"██ ██ ██ █ █████ ██ ██ ██ ██ ",
|
||||
" ████ ██ ██ ████ ████ ███ ██ ",
|
||||
" █████ ",
|
||||
let expected = Buffer::with_lines([
|
||||
" ████ █ ███ ███ ".bold(),
|
||||
"██ ██ ██ ██ ██ ".bold(),
|
||||
"███ █████ ██ ██ ██ ████ ██ ".bold(),
|
||||
" ███ ██ ██ ██ ██ ██ ██ █████ ".bold(),
|
||||
" ███ ██ ██ ██ ██ ██████ ██ ██ ".bold(),
|
||||
"██ ██ ██ █ █████ ██ ██ ██ ██ ".bold(),
|
||||
" ████ ██ ██ ████ ████ ███ ██ ".bold(),
|
||||
" █████ ".bold(),
|
||||
]);
|
||||
expected.set_style(Rect::new(0, 0, 48, 8), Style::new().bold());
|
||||
assert_buffer_eq!(buf, expected);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -404,7 +427,7 @@ mod tests {
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 40, 24));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let mut expected = Buffer::with_lines(vec![
|
||||
let mut expected = Buffer::with_lines([
|
||||
"██████ ███ ",
|
||||
" ██ ██ ██ ",
|
||||
" ██ ██ ████ ██ ",
|
||||
@@ -433,7 +456,7 @@ mod tests {
|
||||
expected.set_style(Rect::new(0, 0, 24, 8), Style::new().red());
|
||||
expected.set_style(Rect::new(0, 8, 40, 8), Style::new().green());
|
||||
expected.set_style(Rect::new(0, 16, 32, 8), Style::new().blue());
|
||||
assert_buffer_eq!(buf, expected);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -445,13 +468,13 @@ mod tests {
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 80, 4));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines(vec![
|
||||
let expected = Buffer::with_lines([
|
||||
"▄█▀▀█▄ ▀▀ ▀██ ▀██▀ ▀▀ ",
|
||||
"▀██▄ ▀██ ██▀▀█▄ ▄█▀▀▄█▀ ██ ▄█▀▀█▄ ██ ▀██ ██▀▀█▄ ▄█▀▀█▄ ",
|
||||
"▄▄ ▀██ ██ ██ ██ ▀█▄▄██ ██ ██▀▀▀▀ ██ ▄█ ██ ██ ██ ██▀▀▀▀ ",
|
||||
" ▀▀▀▀ ▀▀▀▀ ▀▀ ▀▀ ▄▄▄▄█▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀▀▀▀ ▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀ ",
|
||||
]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -463,12 +486,12 @@ mod tests {
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 70, 3));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines(vec![
|
||||
let expected = Buffer::with_lines([
|
||||
"█▀██▀█ ▄█ ▀██",
|
||||
" ██ ▀█▄█▀█▄ ██ ██ ██▀▀█▄ ▄█▀▀█▄ ▀▀▀█▄ ▀██▀▀ ▄█▀▀█▄ ▄▄▄██",
|
||||
" ██ ██ ▀▀ ██ ██ ██ ██ ██ ▄▄ ▄█▀▀██ ██ ▄ ██▀▀▀▀ ██ ██",
|
||||
]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -480,7 +503,7 @@ mod tests {
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 40, 8));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines(vec![
|
||||
let expected = Buffer::with_lines([
|
||||
"██▄ ▄██ ▀██ ▄█ ▀▀ ",
|
||||
"███████ ██ ██ ██ ▀██▀▀ ▀██ ",
|
||||
"██ ▀ ██ ██ ██ ██ ██ ▄ ██ ",
|
||||
@@ -490,7 +513,7 @@ mod tests {
|
||||
" ██ ▄█ ██ ██ ██ ██▀▀▀▀ ▀▀▀█▄ ",
|
||||
"▀▀▀▀▀▀▀ ▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀ ▀▀▀▀▀ ",
|
||||
]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -503,14 +526,13 @@ mod tests {
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 48, 4));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let mut expected = Buffer::with_lines(vec![
|
||||
"▄█▀▀█▄ ▄█ ▀██ ▀██ ",
|
||||
"▀██▄ ▀██▀▀ ██ ██ ██ ▄█▀▀█▄ ▄▄▄██ ",
|
||||
"▄▄ ▀██ ██ ▄ ▀█▄▄██ ██ ██▀▀▀▀ ██ ██ ",
|
||||
" ▀▀▀▀ ▀▀ ▄▄▄▄█▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀ ▀▀ ",
|
||||
let expected = Buffer::with_lines([
|
||||
"▄█▀▀█▄ ▄█ ▀██ ▀██ ".bold(),
|
||||
"▀██▄ ▀██▀▀ ██ ██ ██ ▄█▀▀█▄ ▄▄▄██ ".bold(),
|
||||
"▄▄ ▀██ ██ ▄ ▀█▄▄██ ██ ██▀▀▀▀ ██ ██ ".bold(),
|
||||
" ▀▀▀▀ ▀▀ ▄▄▄▄█▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀ ▀▀ ".bold(),
|
||||
]);
|
||||
expected.set_style(Rect::new(0, 0, 48, 4), Style::new().bold());
|
||||
assert_buffer_eq!(buf, expected);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -526,7 +548,7 @@ mod tests {
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 40, 12));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let mut expected = Buffer::with_lines(vec![
|
||||
let mut expected = Buffer::with_lines([
|
||||
"▀██▀▀█▄ ▀██ ",
|
||||
" ██▄▄█▀ ▄█▀▀█▄ ▄▄▄██ ",
|
||||
" ██ ▀█▄ ██▀▀▀▀ ██ ██ ",
|
||||
@@ -543,7 +565,7 @@ mod tests {
|
||||
expected.set_style(Rect::new(0, 0, 24, 4), Style::new().red());
|
||||
expected.set_style(Rect::new(0, 4, 40, 4), Style::new().green());
|
||||
expected.set_style(Rect::new(0, 8, 32, 4), Style::new().blue());
|
||||
assert_buffer_eq!(buf, expected);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -555,7 +577,7 @@ mod tests {
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 40, 8));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines(vec![
|
||||
let expected = Buffer::with_lines([
|
||||
"▐█▌ █ ▐█ ██ █ ",
|
||||
"█ █ █ ▐▌ ",
|
||||
"█▌ ▐█ ██▌ ▐█▐▌ █ ▐█▌ ▐▌ ▐█ ██▌ ▐█▌ ",
|
||||
@@ -565,7 +587,7 @@ mod tests {
|
||||
"▐█▌ ▐█▌ █ █ █ ▐█▌ ▐█▌ ███▌▐█▌ █ █ ▐█▌ ",
|
||||
" ██▌ ",
|
||||
]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -577,7 +599,7 @@ mod tests {
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 35, 6));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines(vec![
|
||||
let expected = Buffer::with_lines([
|
||||
"███ ▐ ▐█",
|
||||
"▌█▐ █ █",
|
||||
" █ █▐█ █ █ ██▌ ▐█▌ ▐█▌ ▐██ ▐█▌ █",
|
||||
@@ -585,7 +607,7 @@ mod tests {
|
||||
" █ ▐▌▐▌█ █ █ █ █ ▐██ █ ███ █ █",
|
||||
" █ ▐▌ █ █ █ █ █ █ █ █ █▐ █ █ █",
|
||||
]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -597,7 +619,7 @@ mod tests {
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 16));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines(vec![
|
||||
let expected = Buffer::with_lines([
|
||||
"█ ▐▌ ▐█ ▐ █ ",
|
||||
"█▌█▌ █ █ ",
|
||||
"███▌█ █ █ ▐██ ▐█ ",
|
||||
@@ -615,7 +637,7 @@ mod tests {
|
||||
"███▌▐█▌ █ █ ▐█▌ ██▌ ",
|
||||
" ",
|
||||
]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -628,18 +650,17 @@ mod tests {
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 24, 8));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let mut expected = Buffer::with_lines(vec![
|
||||
"▐█▌ ▐ ▐█ ▐█ ",
|
||||
"█ █ █ █ █ ",
|
||||
"█▌ ▐██ █ █ █ ▐█▌ █ ",
|
||||
"▐█ █ █ █ █ █ █ ▐██ ",
|
||||
" ▐█ █ █ █ █ ███ █ █ ",
|
||||
"█ █ █▐ ▐██ █ █ █ █ ",
|
||||
"▐█▌ ▐▌ █ ▐█▌ ▐█▌ ▐█▐▌",
|
||||
" ██▌ ",
|
||||
let expected = Buffer::with_lines([
|
||||
"▐█▌ ▐ ▐█ ▐█ ".bold(),
|
||||
"█ █ █ █ █ ".bold(),
|
||||
"█▌ ▐██ █ █ █ ▐█▌ █ ".bold(),
|
||||
"▐█ █ █ █ █ █ █ ▐██ ".bold(),
|
||||
" ▐█ █ █ █ █ ███ █ █ ".bold(),
|
||||
"█ █ █▐ ▐██ █ █ █ █ ".bold(),
|
||||
"▐█▌ ▐▌ █ ▐█▌ ▐█▌ ▐█▐▌".bold(),
|
||||
" ██▌ ".bold(),
|
||||
]);
|
||||
expected.set_style(Rect::new(0, 0, 24, 8), Style::new().bold());
|
||||
assert_buffer_eq!(buf, expected);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -655,7 +676,7 @@ mod tests {
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 24));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let mut expected = Buffer::with_lines(vec![
|
||||
let mut expected = Buffer::with_lines([
|
||||
"███ ▐█ ",
|
||||
"▐▌▐▌ █ ",
|
||||
"▐▌▐▌▐█▌ █ ",
|
||||
@@ -684,7 +705,7 @@ mod tests {
|
||||
expected.set_style(Rect::new(0, 0, 12, 8), Style::new().red());
|
||||
expected.set_style(Rect::new(0, 8, 20, 8), Style::new().green());
|
||||
expected.set_style(Rect::new(0, 16, 16, 8), Style::new().blue());
|
||||
assert_buffer_eq!(buf, expected);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -717,13 +738,13 @@ mod tests {
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 40, 4));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines(vec![
|
||||
let expected = Buffer::with_lines([
|
||||
"▟▀▙ ▀ ▝█ ▜▛ ▀ ",
|
||||
"▜▙ ▝█ █▀▙ ▟▀▟▘ █ ▟▀▙ ▐▌ ▝█ █▀▙ ▟▀▙ ",
|
||||
"▄▝█ █ █ █ ▜▄█ █ █▀▀ ▐▌▗▌ █ █ █ █▀▀ ",
|
||||
"▝▀▘ ▝▀▘ ▀ ▀ ▄▄▛ ▝▀▘ ▝▀▘ ▀▀▀▘▝▀▘ ▀ ▀ ▝▀▘ ",
|
||||
]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -735,12 +756,12 @@ mod tests {
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 35, 3));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines(vec![
|
||||
let expected = Buffer::with_lines([
|
||||
"▛█▜ ▟ ▝█",
|
||||
" █ ▜▟▜▖█ █ █▀▙ ▟▀▙ ▝▀▙ ▝█▀ ▟▀▙ ▗▄█",
|
||||
" █ ▐▌▝▘█ █ █ █ █ ▄ ▟▀█ █▗ █▀▀ █ █",
|
||||
]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -752,7 +773,7 @@ mod tests {
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 8));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let expected = Buffer::with_lines(vec![
|
||||
let expected = Buffer::with_lines([
|
||||
"█▖▟▌ ▝█ ▟ ▀ ",
|
||||
"███▌█ █ █ ▝█▀ ▝█ ",
|
||||
"█▝▐▌█ █ █ █▗ █ ",
|
||||
@@ -762,7 +783,7 @@ mod tests {
|
||||
"▐▌▗▌ █ █ █ █▀▀ ▝▀▙ ",
|
||||
"▀▀▀▘▝▀▘ ▀ ▀ ▝▀▘ ▀▀▘ ",
|
||||
]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -775,14 +796,13 @@ mod tests {
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 24, 4));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let mut expected = Buffer::with_lines(vec![
|
||||
"▟▀▙ ▟ ▝█ ▝█ ",
|
||||
"▜▙ ▝█▀ █ █ █ ▟▀▙ ▗▄█ ",
|
||||
"▄▝█ █▗ ▜▄█ █ █▀▀ █ █ ",
|
||||
"▝▀▘ ▝▘ ▄▄▛ ▝▀▘ ▝▀▘ ▝▀▝▘",
|
||||
let expected = Buffer::with_lines([
|
||||
"▟▀▙ ▟ ▝█ ▝█ ".bold(),
|
||||
"▜▙ ▝█▀ █ █ █ ▟▀▙ ▗▄█ ".bold(),
|
||||
"▄▝█ █▗ ▜▄█ █ █▀▀ █ █ ".bold(),
|
||||
"▝▀▘ ▝▘ ▄▄▛ ▝▀▘ ▝▀▘ ▝▀▝▘".bold(),
|
||||
]);
|
||||
expected.set_style(Rect::new(0, 0, 24, 4), Style::new().bold());
|
||||
assert_buffer_eq!(buf, expected);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -798,7 +818,7 @@ mod tests {
|
||||
.build()?;
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 12));
|
||||
big_text.render(buf.area, &mut buf);
|
||||
let mut expected = Buffer::with_lines(vec![
|
||||
let mut expected = Buffer::with_lines([
|
||||
"▜▛▜▖ ▝█ ",
|
||||
"▐▙▟▘▟▀▙ ▗▄█ ",
|
||||
"▐▌▜▖█▀▀ █ █ ",
|
||||
@@ -815,7 +835,7 @@ mod tests {
|
||||
expected.set_style(Rect::new(0, 0, 12, 4), Style::new().red());
|
||||
expected.set_style(Rect::new(0, 4, 20, 4), Style::new().green());
|
||||
expected.set_style(Rect::new(0, 8, 16, 4), Style::new().blue());
|
||||
assert_buffer_eq!(buf, expected);
|
||||
assert_eq!(buf, expected);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use palette::{IntoColor, Okhsv, Srgb};
|
||||
use ratatui::prelude::*;
|
||||
use ratatui::{buffer::Buffer, layout::Rect, style::Color, widgets::Widget};
|
||||
|
||||
/// A widget that renders a color swatch of RGB colors.
|
||||
///
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
use rand::Rng;
|
||||
use rand_chacha::rand_core::SeedableRng;
|
||||
use ratatui::{buffer::Cell, layout::Flex, prelude::*};
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Flex, Layout, Rect},
|
||||
style::{Color, Style},
|
||||
terminal::Frame,
|
||||
widgets::Widget,
|
||||
};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::big_text::{BigTextBuilder, PixelSize};
|
||||
@@ -59,7 +65,7 @@ fn drip(frame_count: usize, area: Rect, buf: &mut Buffer) {
|
||||
if rng.gen_ratio(1, 10) {
|
||||
*dest = src;
|
||||
} else {
|
||||
*dest = Cell::default();
|
||||
dest.reset();
|
||||
}
|
||||
} else {
|
||||
// move the pixel down one row
|
||||
|
||||
@@ -14,11 +14,9 @@
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
#![allow(
|
||||
clippy::enum_glob_use,
|
||||
clippy::missing_errors_doc,
|
||||
clippy::module_name_repetitions,
|
||||
clippy::must_use_candidate,
|
||||
clippy::wildcard_imports
|
||||
clippy::must_use_candidate
|
||||
)]
|
||||
|
||||
mod app;
|
||||
@@ -30,16 +28,17 @@ mod tabs;
|
||||
mod term;
|
||||
mod theme;
|
||||
|
||||
pub use app::*;
|
||||
use color_eyre::Result;
|
||||
pub use colors::*;
|
||||
pub use term::*;
|
||||
pub use theme::*;
|
||||
|
||||
pub use self::{
|
||||
colors::{color_from_oklab, RgbSwatch},
|
||||
theme::THEME,
|
||||
};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
errors::init_hooks()?;
|
||||
let terminal = &mut term::init()?;
|
||||
App::new().run(terminal)?;
|
||||
app::run(terminal)?;
|
||||
term::restore()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
use itertools::Itertools;
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Constraint, Layout, Margin, Rect},
|
||||
widgets::{Block, Borders, Clear, Padding, Paragraph, Widget, Wrap},
|
||||
};
|
||||
|
||||
use crate::{RgbSwatch, THEME};
|
||||
|
||||
@@ -64,20 +68,16 @@ impl Widget for AboutTab {
|
||||
}
|
||||
|
||||
fn render_crate_description(area: Rect, buf: &mut Buffer) {
|
||||
let area = area.inner(
|
||||
&(Margin {
|
||||
vertical: 4,
|
||||
horizontal: 2,
|
||||
}),
|
||||
);
|
||||
let area = area.inner(Margin {
|
||||
vertical: 4,
|
||||
horizontal: 2,
|
||||
});
|
||||
Clear.render(area, buf); // clear out the color swatches
|
||||
Block::new().style(THEME.content).render(area, buf);
|
||||
let area = area.inner(
|
||||
&(Margin {
|
||||
vertical: 1,
|
||||
horizontal: 2,
|
||||
}),
|
||||
);
|
||||
let area = area.inner(Margin {
|
||||
vertical: 1,
|
||||
horizontal: 2,
|
||||
});
|
||||
let text = "- cooking up terminal user interfaces -
|
||||
|
||||
Ratatui is a Rust crate that provides widgets (e.g. Paragraph, Table) and draws them to the \
|
||||
@@ -108,7 +108,7 @@ pub fn render_logo(selected_row: usize, area: Rect, buf: &mut Buffer) {
|
||||
} else {
|
||||
THEME.logo.rat_eye_alt
|
||||
};
|
||||
let area = area.inner(&Margin {
|
||||
let area = area.inner(Margin {
|
||||
vertical: 0,
|
||||
horizontal: 2,
|
||||
});
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
use itertools::Itertools;
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Constraint, Layout, Margin, Rect},
|
||||
style::{Styled, Stylize},
|
||||
text::Line,
|
||||
widgets::{
|
||||
Block, BorderType, Borders, Clear, List, ListItem, ListState, Padding, Paragraph,
|
||||
Scrollbar, ScrollbarState, StatefulWidget, Tabs, Widget,
|
||||
},
|
||||
};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{RgbSwatch, THEME};
|
||||
@@ -59,7 +68,7 @@ impl EmailTab {
|
||||
impl Widget for EmailTab {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
RgbSwatch.render(area, buf);
|
||||
let area = area.inner(&Margin {
|
||||
let area = area.inner(Margin {
|
||||
vertical: 1,
|
||||
horizontal: 2,
|
||||
});
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
use itertools::Itertools;
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Constraint, Layout, Margin, Rect},
|
||||
style::{Style, Stylize},
|
||||
text::Line,
|
||||
widgets::{
|
||||
Block, Clear, Padding, Paragraph, Row, Scrollbar, ScrollbarOrientation, ScrollbarState,
|
||||
StatefulWidget, Table, TableState, Widget, Wrap,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{RgbSwatch, THEME};
|
||||
|
||||
@@ -105,7 +114,7 @@ impl RecipeTab {
|
||||
impl Widget for RecipeTab {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
RgbSwatch.render(area, buf);
|
||||
let area = area.inner(&Margin {
|
||||
let area = area.inner(Margin {
|
||||
vertical: 1,
|
||||
horizontal: 2,
|
||||
});
|
||||
@@ -124,7 +133,7 @@ impl Widget for RecipeTab {
|
||||
};
|
||||
render_scrollbar(self.row_index, scrollbar_area, buf);
|
||||
|
||||
let area = area.inner(&Margin {
|
||||
let area = area.inner(Margin {
|
||||
horizontal: 2,
|
||||
vertical: 1,
|
||||
});
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
use itertools::Itertools;
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{canvas::*, *},
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Constraint, Layout, Margin, Rect},
|
||||
style::{Styled, Stylize},
|
||||
symbols::Marker,
|
||||
widgets::{
|
||||
canvas::{self, Canvas, Map, MapResolution, Points},
|
||||
Block, BorderType, Clear, Padding, Row, Scrollbar, ScrollbarOrientation, ScrollbarState,
|
||||
Sparkline, StatefulWidget, Table, TableState, Widget,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{RgbSwatch, THEME};
|
||||
@@ -26,7 +33,7 @@ impl TracerouteTab {
|
||||
impl Widget for TracerouteTab {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
RgbSwatch.render(area, buf);
|
||||
let area = area.inner(&Margin {
|
||||
let area = area.inner(Margin {
|
||||
vertical: 1,
|
||||
horizontal: 2,
|
||||
});
|
||||
@@ -49,10 +56,10 @@ fn render_hops(selected_row: usize, area: Rect, buf: &mut Buffer) {
|
||||
.iter()
|
||||
.map(|hop| Row::new(vec![hop.host, hop.address]))
|
||||
.collect_vec();
|
||||
let block = Block::default()
|
||||
.title("Traceroute bad.horse".bold().white())
|
||||
let block = Block::new()
|
||||
.padding(Padding::new(1, 1, 1, 1))
|
||||
.title_alignment(Alignment::Center)
|
||||
.padding(Padding::new(1, 1, 1, 1));
|
||||
.title("Traceroute bad.horse".bold().white());
|
||||
StatefulWidget::render(
|
||||
Table::new(rows, [Constraint::Max(100), Constraint::Length(15)])
|
||||
.header(Row::new(vec!["Host", "Address"]).set_style(THEME.traceroute.header))
|
||||
@@ -104,7 +111,7 @@ fn render_map(selected_row: usize, area: Rect, buf: &mut Buffer) {
|
||||
let theme = THEME.traceroute.map;
|
||||
let path: Option<(&Hop, &Hop)> = HOPS.iter().tuple_windows().nth(selected_row);
|
||||
let map = Map {
|
||||
resolution: canvas::MapResolution::High,
|
||||
resolution: MapResolution::High,
|
||||
color: theme.color,
|
||||
};
|
||||
Canvas::default()
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
use itertools::Itertools;
|
||||
use palette::Okhsv;
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{calendar::CalendarEventStore, *},
|
||||
buffer::Buffer,
|
||||
layout::{Constraint, Direction, Layout, Margin, Rect},
|
||||
style::{Color, Style, Stylize},
|
||||
symbols,
|
||||
widgets::{
|
||||
calendar::{CalendarEventStore, Monthly},
|
||||
Bar, BarChart, BarGroup, Block, Clear, LineGauge, Padding, Widget,
|
||||
},
|
||||
};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
@@ -28,14 +34,14 @@ impl WeatherTab {
|
||||
impl Widget for WeatherTab {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
RgbSwatch.render(area, buf);
|
||||
let area = area.inner(&Margin {
|
||||
let area = area.inner(Margin {
|
||||
vertical: 1,
|
||||
horizontal: 2,
|
||||
});
|
||||
Clear.render(area, buf);
|
||||
Block::new().style(THEME.content).render(area, buf);
|
||||
|
||||
let area = area.inner(&Margin {
|
||||
let area = area.inner(Margin {
|
||||
horizontal: 2,
|
||||
vertical: 1,
|
||||
});
|
||||
@@ -59,7 +65,7 @@ impl Widget for WeatherTab {
|
||||
|
||||
fn render_calendar(area: Rect, buf: &mut Buffer) {
|
||||
let date = OffsetDateTime::now_utc().date();
|
||||
calendar::Monthly::new(date, CalendarEventStore::today(Style::new().red().bold()))
|
||||
Monthly::new(date, CalendarEventStore::today(Style::new().red().bold()))
|
||||
.block(Block::new().padding(Padding::new(0, 0, 2, 0)))
|
||||
.show_month_header(Style::new().bold())
|
||||
.show_weekdays_header(Style::new().italic())
|
||||
@@ -140,8 +146,8 @@ fn render_line_gauge(percent: f64, area: Rect, buf: &mut Buffer) {
|
||||
// cycle color hue based on the percent for a neat effect yellow -> red
|
||||
let hue = 90.0 - (percent as f32 * 0.6);
|
||||
let value = Okhsv::max_value();
|
||||
let fg = color_from_oklab(hue, Okhsv::max_saturation(), value);
|
||||
let bg = color_from_oklab(hue, Okhsv::max_saturation(), value * 0.5);
|
||||
let filled_color = color_from_oklab(hue, Okhsv::max_saturation(), value);
|
||||
let unfilled_color = color_from_oklab(hue, Okhsv::max_saturation(), value * 0.5);
|
||||
let label = if percent < 100.0 {
|
||||
format!("Downloading: {percent}%")
|
||||
} else {
|
||||
@@ -151,7 +157,8 @@ fn render_line_gauge(percent: f64, area: Rect, buf: &mut Buffer) {
|
||||
.ratio(percent / 100.0)
|
||||
.label(label)
|
||||
.style(Style::new().light_blue())
|
||||
.gauge_style(Style::new().fg(fg).bg(bg))
|
||||
.filled_style(Style::new().fg(filled_color))
|
||||
.unfilled_style(Style::new().fg(unfilled_color))
|
||||
.line_set(symbols::line::THICK)
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
@@ -4,12 +4,16 @@ use std::{
|
||||
};
|
||||
|
||||
use color_eyre::{eyre::WrapErr, Result};
|
||||
use crossterm::{
|
||||
event::{self, Event},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
crossterm::{
|
||||
event::{self, Event},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
},
|
||||
layout::Rect,
|
||||
terminal::{Terminal, TerminalOptions, Viewport},
|
||||
};
|
||||
use ratatui::prelude::*;
|
||||
|
||||
pub fn init() -> Result<Terminal<impl Backend>> {
|
||||
// this size is to match the size of the terminal when running the demo
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use ratatui::prelude::*;
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
|
||||
pub struct Theme {
|
||||
pub root: Style,
|
||||
|
||||
@@ -15,12 +15,17 @@
|
||||
|
||||
use std::io::{self, stdout};
|
||||
|
||||
use crossterm::{
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
};
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
backend::CrosstermBackend,
|
||||
crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
},
|
||||
layout::{Constraint, Layout},
|
||||
style::{Color, Modifier, Style, Stylize},
|
||||
terminal::{Frame, Terminal},
|
||||
text::{Line, Span, Text},
|
||||
widgets::{Block, Borders, Paragraph},
|
||||
};
|
||||
|
||||
@@ -51,13 +56,11 @@ fn main() -> io::Result<()> {
|
||||
|
||||
fn hello_world(frame: &mut Frame) {
|
||||
frame.render_widget(
|
||||
Paragraph::new("Hello World!")
|
||||
.block(Block::default().title("Greeting").borders(Borders::ALL)),
|
||||
Paragraph::new("Hello World!").block(Block::bordered().title("Greeting")),
|
||||
frame.size(),
|
||||
);
|
||||
}
|
||||
|
||||
use crossterm::event::{self, Event, KeyCode};
|
||||
fn handle_events() -> io::Result<bool> {
|
||||
if event::poll(std::time::Duration::from_millis(50))? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
@@ -87,8 +90,8 @@ fn layout(frame: &mut Frame) {
|
||||
Block::new().borders(Borders::TOP).title("Status Bar"),
|
||||
status_bar,
|
||||
);
|
||||
frame.render_widget(Block::default().borders(Borders::ALL).title("Left"), left);
|
||||
frame.render_widget(Block::default().borders(Borders::ALL).title("Right"), right);
|
||||
frame.render_widget(Block::bordered().title("Left"), left);
|
||||
frame.render_widget(Block::bordered().title("Right"), right);
|
||||
}
|
||||
|
||||
fn styling(frame: &mut Frame) {
|
||||
|
||||
@@ -13,22 +13,30 @@
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
#![allow(clippy::enum_glob_use, clippy::wildcard_imports)]
|
||||
|
||||
use std::io::{self, stdout};
|
||||
|
||||
use color_eyre::{config::HookBuilder, Result};
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode, KeyEventKind},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
};
|
||||
use ratatui::{
|
||||
layout::{Constraint::*, Flex},
|
||||
prelude::*,
|
||||
style::palette::tailwind,
|
||||
symbols::line,
|
||||
widgets::{block::Title, *},
|
||||
backend::{Backend, CrosstermBackend},
|
||||
buffer::Buffer,
|
||||
crossterm::{
|
||||
event::{self, Event, KeyCode, KeyEventKind},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
},
|
||||
layout::{
|
||||
Alignment,
|
||||
Constraint::{self, Fill, Length, Max, Min, Percentage, Ratio},
|
||||
Flex, Layout, Rect,
|
||||
},
|
||||
style::{palette::tailwind, Color, Modifier, Style, Stylize},
|
||||
symbols::{self, line},
|
||||
terminal::Terminal,
|
||||
text::{Line, Text},
|
||||
widgets::{
|
||||
block::Title, Block, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState,
|
||||
StatefulWidget, Tabs, Widget,
|
||||
},
|
||||
};
|
||||
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
|
||||
|
||||
@@ -177,18 +185,17 @@ impl App {
|
||||
}
|
||||
|
||||
fn handle_events(&mut self) -> Result<()> {
|
||||
use KeyCode::*;
|
||||
match event::read()? {
|
||||
Event::Key(key) if key.kind == KeyEventKind::Press => match key.code {
|
||||
Char('q') | Esc => self.quit(),
|
||||
Char('l') | Right => self.next(),
|
||||
Char('h') | Left => self.previous(),
|
||||
Char('j') | Down => self.down(),
|
||||
Char('k') | Up => self.up(),
|
||||
Char('g') | Home => self.top(),
|
||||
Char('G') | End => self.bottom(),
|
||||
Char('+') => self.increment_spacing(),
|
||||
Char('-') => self.decrement_spacing(),
|
||||
KeyCode::Char('q') | KeyCode::Esc => self.quit(),
|
||||
KeyCode::Char('l') | KeyCode::Right => self.next(),
|
||||
KeyCode::Char('h') | KeyCode::Left => self.previous(),
|
||||
KeyCode::Char('j') | KeyCode::Down => self.down(),
|
||||
KeyCode::Char('k') | KeyCode::Up => self.up(),
|
||||
KeyCode::Char('g') | KeyCode::Home => self.top(),
|
||||
KeyCode::Char('G') | KeyCode::End => self.bottom(),
|
||||
KeyCode::Char('+') => self.increment_spacing(),
|
||||
KeyCode::Char('-') => self.decrement_spacing(),
|
||||
_ => (),
|
||||
},
|
||||
_ => {}
|
||||
@@ -364,7 +371,7 @@ impl SelectedTab {
|
||||
|
||||
/// Convert a `SelectedTab` into a `Line` to display it by the `Tabs` widget.
|
||||
fn to_tab_title(value: Self) -> Line<'static> {
|
||||
use tailwind::*;
|
||||
use tailwind::{INDIGO, ORANGE, SKY};
|
||||
let text = value.to_string();
|
||||
let color = match value {
|
||||
Self::Legacy => ORANGE.c400,
|
||||
@@ -509,7 +516,7 @@ impl Example {
|
||||
}
|
||||
|
||||
const fn color_for_constraint(constraint: Constraint) -> Color {
|
||||
use tailwind::*;
|
||||
use tailwind::{BLUE, SLATE};
|
||||
match constraint {
|
||||
Constraint::Min(_) => BLUE.c900,
|
||||
Constraint::Max(_) => BLUE.c800,
|
||||
|
||||
@@ -13,20 +13,22 @@
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
#![allow(clippy::enum_glob_use)]
|
||||
|
||||
use std::{io::stdout, time::Duration};
|
||||
|
||||
use color_eyre::{config::HookBuilder, Result};
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode, KeyEventKind},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
};
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
style::palette::tailwind,
|
||||
widgets::{block::Title, Block, Borders, Gauge, Padding, Paragraph},
|
||||
backend::{Backend, CrosstermBackend},
|
||||
buffer::Buffer,
|
||||
crossterm::{
|
||||
event::{self, Event, KeyCode, KeyEventKind},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
},
|
||||
layout::{Alignment, Constraint, Layout, Rect},
|
||||
style::{palette::tailwind, Color, Style, Stylize},
|
||||
terminal::Terminal,
|
||||
text::Span,
|
||||
widgets::{block::Title, Block, Borders, Gauge, Padding, Paragraph, Widget},
|
||||
};
|
||||
|
||||
const GAUGE1_COLOR: Color = tailwind::RED.c800;
|
||||
@@ -99,10 +101,9 @@ impl App {
|
||||
if event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
use KeyCode::*;
|
||||
match key.code {
|
||||
Char(' ') | Enter => self.start(),
|
||||
Char('q') | Esc => self.quit(),
|
||||
KeyCode::Char(' ') | KeyCode::Enter => self.start(),
|
||||
KeyCode::Char('q') | KeyCode::Esc => self.quit(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -123,7 +124,7 @@ impl App {
|
||||
impl Widget for &App {
|
||||
#[allow(clippy::similar_names)]
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
use Constraint::*;
|
||||
use Constraint::{Length, Min, Ratio};
|
||||
let layout = Layout::vertical([Length(2), Min(0), Length(1)]);
|
||||
let [header_area, gauge_area, footer_area] = layout.areas(area);
|
||||
|
||||
@@ -206,11 +207,11 @@ impl App {
|
||||
|
||||
fn title_block(title: &str) -> Block {
|
||||
let title = Title::from(title).alignment(Alignment::Center);
|
||||
Block::default()
|
||||
.title(title)
|
||||
Block::new()
|
||||
.borders(Borders::NONE)
|
||||
.fg(CUSTOM_LABEL_COLOR)
|
||||
.padding(Padding::vertical(1))
|
||||
.title(title)
|
||||
.fg(CUSTOM_LABEL_COLOR)
|
||||
}
|
||||
|
||||
fn init_error_hooks() -> color_eyre::Result<()> {
|
||||
|
||||
@@ -19,12 +19,16 @@ use std::{
|
||||
};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
use ratatui::{
|
||||
backend::CrosstermBackend,
|
||||
crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
},
|
||||
terminal::{Frame, Terminal},
|
||||
widgets::Paragraph,
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::Paragraph};
|
||||
|
||||
/// This is a bare minimum example. There are many approaches to running an application loop, so
|
||||
/// this is not meant to be prescriptive. It is only meant to demonstrate the basic setup and
|
||||
|
||||
@@ -13,8 +13,6 @@
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
#![allow(clippy::wildcard_imports)]
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, VecDeque},
|
||||
error::Error,
|
||||
@@ -25,7 +23,16 @@ use std::{
|
||||
};
|
||||
|
||||
use rand::distributions::{Distribution, Uniform};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Alignment, Constraint, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
symbols,
|
||||
terminal::{Frame, Terminal, Viewport},
|
||||
text::{Line, Span},
|
||||
widgets::{block, Block, Gauge, LineGauge, List, ListItem, Paragraph, Widget},
|
||||
TerminalOptions,
|
||||
};
|
||||
|
||||
const NUM_DOWNLOADS: usize = 10;
|
||||
|
||||
@@ -236,7 +243,7 @@ fn run_app<B: Backend>(
|
||||
fn ui(f: &mut Frame, downloads: &Downloads) {
|
||||
let area = f.size();
|
||||
|
||||
let block = Block::default().title(block::Title::from("Progress").alignment(Alignment::Center));
|
||||
let block = Block::new().title(block::Title::from("Progress").alignment(Alignment::Center));
|
||||
f.render_widget(block, area);
|
||||
|
||||
let vertical = Layout::vertical([Constraint::Length(2), Constraint::Length(4)]).margin(1);
|
||||
@@ -248,7 +255,7 @@ fn ui(f: &mut Frame, downloads: &Downloads) {
|
||||
let done = NUM_DOWNLOADS - downloads.pending.len() - downloads.in_progress.len();
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
let progress = LineGauge::default()
|
||||
.gauge_style(Style::default().fg(Color::Blue))
|
||||
.filled_style(Style::default().fg(Color::Blue))
|
||||
.label(format!("{done}/{NUM_DOWNLOADS}"))
|
||||
.ratio(done as f64 / NUM_DOWNLOADS as f64);
|
||||
f.render_widget(progress, progress_area);
|
||||
|
||||
@@ -13,20 +13,25 @@
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
#![allow(clippy::enum_glob_use)]
|
||||
|
||||
use std::{error::Error, io};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use ratatui::{
|
||||
layout::Constraint::*,
|
||||
prelude::*,
|
||||
widgets::{Block, Borders, Paragraph},
|
||||
backend::{Backend, CrosstermBackend},
|
||||
crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
},
|
||||
layout::{
|
||||
Constraint,
|
||||
Constraint::{Length, Max, Min, Percentage, Ratio},
|
||||
Layout, Rect,
|
||||
},
|
||||
style::{Color, Style, Stylize},
|
||||
terminal::{Frame, Terminal},
|
||||
text::Line,
|
||||
widgets::{Block, Paragraph},
|
||||
};
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
@@ -190,10 +195,9 @@ fn render_example_combination(
|
||||
title: &str,
|
||||
constraints: Vec<(Constraint, Constraint)>,
|
||||
) {
|
||||
let block = Block::default()
|
||||
let block = Block::bordered()
|
||||
.title(title.gray())
|
||||
.style(Style::reset())
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::DarkGray));
|
||||
let inner = block.inner(area);
|
||||
frame.render_widget(block, area);
|
||||
|
||||
218
examples/line_gauge.rs
Normal file
218
examples/line_gauge.rs
Normal file
@@ -0,0 +1,218 @@
|
||||
//! # [Ratatui] Line Gauge example
|
||||
//!
|
||||
//! The latest version of this example is available in the [examples] folder in the repository.
|
||||
//!
|
||||
//! Please note that the examples are designed to be run against the `main` branch of the Github
|
||||
//! repository. This means that you may not be able to compile with the latest release version on
|
||||
//! crates.io, or the one that you have installed locally.
|
||||
//!
|
||||
//! See the [examples readme] for more information on finding examples that match the version of the
|
||||
//! library you are using.
|
||||
//!
|
||||
//! [Ratatui]: https://github.com/ratatui-org/ratatui
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
use std::{io::stdout, time::Duration};
|
||||
|
||||
use color_eyre::{config::HookBuilder, Result};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
buffer::Buffer,
|
||||
crossterm::{
|
||||
event::{self, Event, KeyCode, KeyEventKind},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
},
|
||||
layout::{Alignment, Constraint, Layout, Rect},
|
||||
style::{palette::tailwind, Color, Style, Stylize},
|
||||
terminal::Terminal,
|
||||
widgets::{block::Title, Block, Borders, LineGauge, Padding, Paragraph, Widget},
|
||||
};
|
||||
|
||||
const CUSTOM_LABEL_COLOR: Color = tailwind::SLATE.c200;
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
struct App {
|
||||
state: AppState,
|
||||
progress_columns: u16,
|
||||
progress: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
enum AppState {
|
||||
#[default]
|
||||
Running,
|
||||
Started,
|
||||
Quitting,
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
init_error_hooks()?;
|
||||
let terminal = init_terminal()?;
|
||||
App::default().run(terminal)?;
|
||||
restore_terminal()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn run(&mut self, mut terminal: Terminal<impl Backend>) -> Result<()> {
|
||||
while self.state != AppState::Quitting {
|
||||
self.draw(&mut terminal)?;
|
||||
self.handle_events()?;
|
||||
self.update(terminal.size()?.width);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw(&self, terminal: &mut Terminal<impl Backend>) -> Result<()> {
|
||||
terminal.draw(|f| f.render_widget(self, f.size()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update(&mut self, terminal_width: u16) {
|
||||
if self.state != AppState::Started {
|
||||
return;
|
||||
}
|
||||
|
||||
self.progress_columns = (self.progress_columns + 1).clamp(0, terminal_width);
|
||||
self.progress = f64::from(self.progress_columns) / f64::from(terminal_width);
|
||||
}
|
||||
|
||||
fn handle_events(&mut self) -> Result<()> {
|
||||
let timeout = Duration::from_secs_f32(1.0 / 20.0);
|
||||
if event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
KeyCode::Char(' ') | KeyCode::Enter => self.start(),
|
||||
KeyCode::Char('q') | KeyCode::Esc => self.quit(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn start(&mut self) {
|
||||
self.state = AppState::Started;
|
||||
}
|
||||
|
||||
fn quit(&mut self) {
|
||||
self.state = AppState::Quitting;
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for &App {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
use Constraint::{Length, Min, Ratio};
|
||||
let layout = Layout::vertical([Length(2), Min(0), Length(1)]);
|
||||
let [header_area, main_area, footer_area] = layout.areas(area);
|
||||
|
||||
let layout = Layout::vertical([Ratio(1, 3); 3]);
|
||||
let [gauge1_area, gauge2_area, gauge3_area] = layout.areas(main_area);
|
||||
|
||||
header().render(header_area, buf);
|
||||
footer().render(footer_area, buf);
|
||||
|
||||
self.render_gauge1(gauge1_area, buf);
|
||||
self.render_gauge2(gauge2_area, buf);
|
||||
self.render_gauge3(gauge3_area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
fn header() -> impl Widget {
|
||||
Paragraph::new("Ratatui Line Gauge Example")
|
||||
.bold()
|
||||
.alignment(Alignment::Center)
|
||||
.fg(CUSTOM_LABEL_COLOR)
|
||||
}
|
||||
|
||||
fn footer() -> impl Widget {
|
||||
Paragraph::new("Press ENTER / SPACE to start")
|
||||
.alignment(Alignment::Center)
|
||||
.fg(CUSTOM_LABEL_COLOR)
|
||||
.bold()
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn render_gauge1(&self, area: Rect, buf: &mut Buffer) {
|
||||
let title = title_block("Blue / red only foreground");
|
||||
LineGauge::default()
|
||||
.block(title)
|
||||
.filled_style(Style::default().fg(Color::Blue))
|
||||
.unfilled_style(Style::default().fg(Color::Red))
|
||||
.label("Foreground:")
|
||||
.ratio(self.progress)
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
fn render_gauge2(&self, area: Rect, buf: &mut Buffer) {
|
||||
let title = title_block("Blue / red only background");
|
||||
LineGauge::default()
|
||||
.block(title)
|
||||
.filled_style(Style::default().fg(Color::Blue).bg(Color::Blue))
|
||||
.unfilled_style(Style::default().fg(Color::Red).bg(Color::Red))
|
||||
.label("Background:")
|
||||
.ratio(self.progress)
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
fn render_gauge3(&self, area: Rect, buf: &mut Buffer) {
|
||||
let title = title_block("Fully styled with background");
|
||||
LineGauge::default()
|
||||
.block(title)
|
||||
.filled_style(
|
||||
Style::default()
|
||||
.fg(tailwind::BLUE.c400)
|
||||
.bg(tailwind::BLUE.c600),
|
||||
)
|
||||
.unfilled_style(
|
||||
Style::default()
|
||||
.fg(tailwind::RED.c400)
|
||||
.bg(tailwind::RED.c800),
|
||||
)
|
||||
.label("Both:")
|
||||
.ratio(self.progress)
|
||||
.render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
fn title_block(title: &str) -> Block {
|
||||
let title = Title::from(title).alignment(Alignment::Center);
|
||||
Block::default()
|
||||
.title(title)
|
||||
.borders(Borders::NONE)
|
||||
.fg(CUSTOM_LABEL_COLOR)
|
||||
.padding(Padding::vertical(1))
|
||||
}
|
||||
|
||||
fn init_error_hooks() -> color_eyre::Result<()> {
|
||||
let (panic, error) = HookBuilder::default().into_hooks();
|
||||
let panic = panic.into_panic_hook();
|
||||
let error = error.into_eyre_hook();
|
||||
color_eyre::eyre::set_hook(Box::new(move |e| {
|
||||
let _ = restore_terminal();
|
||||
error(e)
|
||||
}))?;
|
||||
std::panic::set_hook(Box::new(move |info| {
|
||||
let _ = restore_terminal();
|
||||
panic(info);
|
||||
}));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn init_terminal() -> color_eyre::Result<Terminal<impl Backend>> {
|
||||
enable_raw_mode()?;
|
||||
stdout().execute(EnterAlternateScreen)?;
|
||||
let backend = CrosstermBackend::new(stdout());
|
||||
let terminal = Terminal::new(backend)?;
|
||||
Ok(terminal)
|
||||
}
|
||||
|
||||
fn restore_terminal() -> color_eyre::Result<()> {
|
||||
disable_raw_mode()?;
|
||||
stdout().execute(LeaveAlternateScreen)?;
|
||||
Ok(())
|
||||
}
|
||||
125
examples/list.rs
125
examples/list.rs
@@ -13,17 +13,26 @@
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
#![allow(clippy::enum_glob_use, clippy::wildcard_imports)]
|
||||
|
||||
use std::{error::Error, io, io::stdout};
|
||||
|
||||
use color_eyre::config::HookBuilder;
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode, KeyEventKind},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
buffer::Buffer,
|
||||
crossterm::{
|
||||
event::{self, Event, KeyCode, KeyEventKind},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
},
|
||||
layout::{Alignment, Constraint, Layout, Rect},
|
||||
style::{palette::tailwind, Color, Modifier, Style, Stylize},
|
||||
terminal::Terminal,
|
||||
text::Line,
|
||||
widgets::{
|
||||
Block, Borders, HighlightSpacing, List, ListItem, ListState, Padding, Paragraph,
|
||||
StatefulWidget, Widget, Wrap,
|
||||
},
|
||||
};
|
||||
use ratatui::{prelude::*, style::palette::tailwind, widgets::*};
|
||||
|
||||
const TODO_HEADER_BG: Color = tailwind::BLUE.c950;
|
||||
const NORMAL_ROW_COLOR: Color = tailwind::SLATE.c950;
|
||||
@@ -38,15 +47,25 @@ enum Status {
|
||||
Completed,
|
||||
}
|
||||
|
||||
struct TodoItem<'a> {
|
||||
todo: &'a str,
|
||||
info: &'a str,
|
||||
struct TodoItem {
|
||||
todo: String,
|
||||
info: String,
|
||||
status: Status,
|
||||
}
|
||||
|
||||
struct StatefulList<'a> {
|
||||
impl TodoItem {
|
||||
fn new(todo: &str, info: &str, status: Status) -> Self {
|
||||
Self {
|
||||
todo: todo.to_string(),
|
||||
info: info.to_string(),
|
||||
status,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct TodoList {
|
||||
state: ListState,
|
||||
items: Vec<TodoItem<'a>>,
|
||||
items: Vec<TodoItem>,
|
||||
last_selected: Option<usize>,
|
||||
}
|
||||
|
||||
@@ -56,8 +75,8 @@ struct StatefulList<'a> {
|
||||
///
|
||||
/// Check the event handling at the bottom to see how to change the state on incoming events.
|
||||
/// Check the drawing logic for items on how to specify the highlighting style for selected items.
|
||||
struct App<'a> {
|
||||
items: StatefulList<'a>,
|
||||
struct App {
|
||||
items: TodoList,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
@@ -102,10 +121,10 @@ fn restore_terminal() -> color_eyre::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl<'a> App<'a> {
|
||||
impl App {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
items: StatefulList::with_items([
|
||||
items: TodoList::with_items(&[
|
||||
("Rewrite everything with Rust!", "I can't hold my inner voice. He tells me to rewrite the complete universe with Rust", Status::Todo),
|
||||
("Rewrite all of your tui apps with Ratatui", "Yes, you heard that right. Go and replace your tui with Ratatui.", Status::Completed),
|
||||
("Pet your cat", "Minnak loves to be pet by you! Don't forget to pet and give some treats!", Status::Todo),
|
||||
@@ -135,22 +154,23 @@ impl<'a> App<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl App<'_> {
|
||||
impl App {
|
||||
fn run(&mut self, mut terminal: Terminal<impl Backend>) -> io::Result<()> {
|
||||
loop {
|
||||
self.draw(&mut terminal)?;
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
use KeyCode::*;
|
||||
match key.code {
|
||||
Char('q') | Esc => return Ok(()),
|
||||
Char('h') | Left => self.items.unselect(),
|
||||
Char('j') | Down => self.items.next(),
|
||||
Char('k') | Up => self.items.previous(),
|
||||
Char('l') | Right | Enter => self.change_status(),
|
||||
Char('g') => self.go_top(),
|
||||
Char('G') => self.go_bottom(),
|
||||
KeyCode::Char('q') | KeyCode::Esc => return Ok(()),
|
||||
KeyCode::Char('h') | KeyCode::Left => self.items.unselect(),
|
||||
KeyCode::Char('j') | KeyCode::Down => self.items.next(),
|
||||
KeyCode::Char('k') | KeyCode::Up => self.items.previous(),
|
||||
KeyCode::Char('l') | KeyCode::Right | KeyCode::Enter => {
|
||||
self.change_status();
|
||||
}
|
||||
KeyCode::Char('g') => self.go_top(),
|
||||
KeyCode::Char('G') => self.go_bottom(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -164,7 +184,7 @@ impl App<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for &mut App<'_> {
|
||||
impl Widget for &mut App {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
// Create a space for header, todo list and the footer.
|
||||
let vertical = Layout::vertical([
|
||||
@@ -186,16 +206,16 @@ impl Widget for &mut App<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
impl App<'_> {
|
||||
impl App {
|
||||
fn render_todo(&mut self, area: Rect, buf: &mut Buffer) {
|
||||
// We create two blocks, one is for the header (outer) and the other is for list (inner).
|
||||
let outer_block = Block::default()
|
||||
let outer_block = Block::new()
|
||||
.borders(Borders::NONE)
|
||||
.fg(TEXT_COLOR)
|
||||
.bg(TODO_HEADER_BG)
|
||||
.title_alignment(Alignment::Center)
|
||||
.title("TODO List")
|
||||
.title_alignment(Alignment::Center);
|
||||
let inner_block = Block::default()
|
||||
.fg(TEXT_COLOR)
|
||||
.bg(TODO_HEADER_BG);
|
||||
let inner_block = Block::new()
|
||||
.borders(Borders::NONE)
|
||||
.fg(TEXT_COLOR)
|
||||
.bg(NORMAL_ROW_COLOR);
|
||||
@@ -238,24 +258,24 @@ impl App<'_> {
|
||||
// We get the info depending on the item's state.
|
||||
let info = if let Some(i) = self.items.state.selected() {
|
||||
match self.items.items[i].status {
|
||||
Status::Completed => "✓ DONE: ".to_string() + self.items.items[i].info,
|
||||
Status::Todo => "TODO: ".to_string() + self.items.items[i].info,
|
||||
Status::Completed => format!("✓ DONE: {}", self.items.items[i].info),
|
||||
Status::Todo => format!("TODO: {}", self.items.items[i].info),
|
||||
}
|
||||
} else {
|
||||
"Nothing to see here...".to_string()
|
||||
};
|
||||
|
||||
// We show the list item's info under the list in this paragraph
|
||||
let outer_info_block = Block::default()
|
||||
let outer_info_block = Block::new()
|
||||
.borders(Borders::NONE)
|
||||
.fg(TEXT_COLOR)
|
||||
.bg(TODO_HEADER_BG)
|
||||
.title_alignment(Alignment::Center)
|
||||
.title("TODO Info")
|
||||
.title_alignment(Alignment::Center);
|
||||
let inner_info_block = Block::default()
|
||||
.fg(TEXT_COLOR)
|
||||
.bg(TODO_HEADER_BG);
|
||||
let inner_info_block = Block::new()
|
||||
.borders(Borders::NONE)
|
||||
.bg(NORMAL_ROW_COLOR)
|
||||
.padding(Padding::horizontal(1));
|
||||
.padding(Padding::horizontal(1))
|
||||
.bg(NORMAL_ROW_COLOR);
|
||||
|
||||
// This is a similar process to what we did for list. outer_info_area will be used for
|
||||
// header inner_info_area will be used for the list info.
|
||||
@@ -288,11 +308,14 @@ fn render_footer(area: Rect, buf: &mut Buffer) {
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
impl StatefulList<'_> {
|
||||
fn with_items<'a>(items: [(&'a str, &'a str, Status); 6]) -> StatefulList<'a> {
|
||||
StatefulList {
|
||||
impl TodoList {
|
||||
fn with_items(items: &[(&str, &str, Status)]) -> Self {
|
||||
Self {
|
||||
state: ListState::default(),
|
||||
items: items.iter().map(TodoItem::from).collect(),
|
||||
items: items
|
||||
.iter()
|
||||
.map(|(todo, info, status)| TodoItem::new(todo, info, *status))
|
||||
.collect(),
|
||||
last_selected: None,
|
||||
}
|
||||
}
|
||||
@@ -333,7 +356,7 @@ impl StatefulList<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
impl TodoItem<'_> {
|
||||
impl TodoItem {
|
||||
fn to_list_item(&self, index: usize) -> ListItem {
|
||||
let bg_color = match index % 2 {
|
||||
0 => NORMAL_ROW_COLOR,
|
||||
@@ -350,13 +373,3 @@ impl TodoItem<'_> {
|
||||
ListItem::new(line).bg(bg_color)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&(&'a str, &'a str, Status)> for TodoItem<'a> {
|
||||
fn from((todo, info, status): &(&'a str, &'a str, Status)) -> Self {
|
||||
Self {
|
||||
todo,
|
||||
info,
|
||||
status: *status,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
48
examples/minimal.rs
Normal file
48
examples/minimal.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
//! # [Ratatui] Minimal example
|
||||
//!
|
||||
//! The latest version of this example is available in the [examples] folder in the repository.
|
||||
//!
|
||||
//! Please note that the examples are designed to be run against the `main` branch of the Github
|
||||
//! repository. This means that you may not be able to compile with the latest release version on
|
||||
//! crates.io, or the one that you have installed locally.
|
||||
//!
|
||||
//! See the [examples readme] for more information on finding examples that match the version of the
|
||||
//! library you are using.
|
||||
//!
|
||||
//! [Ratatui]: https://github.com/ratatui-org/ratatui
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
use ratatui::{
|
||||
backend::CrosstermBackend,
|
||||
crossterm::{
|
||||
event::{self, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
},
|
||||
text::Text,
|
||||
Terminal,
|
||||
};
|
||||
|
||||
/// This is a bare minimum example. There are many approaches to running an application loop, so
|
||||
/// this is not meant to be prescriptive. See the [examples] folder for more complete examples.
|
||||
/// In particular, the [hello-world] example is a good starting point.
|
||||
///
|
||||
/// [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
/// [hello-world]: https://github.com/ratatui-org/ratatui/blob/main/examples/hello_world.rs
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut terminal = Terminal::new(CrosstermBackend::new(std::io::stdout()))?;
|
||||
enable_raw_mode()?;
|
||||
execute!(terminal.backend_mut(), EnterAlternateScreen)?;
|
||||
loop {
|
||||
terminal.draw(|frame| frame.render_widget(Text::raw("Hello World!"), frame.size()))?;
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
disable_raw_mode()?;
|
||||
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -25,13 +25,20 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use ratatui::{prelude::*, widgets::Paragraph};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
},
|
||||
layout::{Constraint, Layout},
|
||||
style::{Color, Modifier, Style, Stylize},
|
||||
terminal::{Frame, Terminal},
|
||||
text::Line,
|
||||
widgets::Paragraph,
|
||||
};
|
||||
|
||||
type Result<T> = result::Result<T, Box<dyn Error>>;
|
||||
|
||||
|
||||
@@ -31,13 +31,15 @@
|
||||
|
||||
use std::{error::Error, io};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{Block, Borders, Paragraph},
|
||||
backend::{Backend, CrosstermBackend},
|
||||
crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
},
|
||||
terminal::{Frame, Terminal},
|
||||
text::Line,
|
||||
widgets::{Block, Paragraph},
|
||||
};
|
||||
|
||||
type Result<T> = std::result::Result<T, Box<dyn Error>>;
|
||||
@@ -142,11 +144,9 @@ fn ui(f: &mut Frame, app: &App) {
|
||||
Line::from("try first without the panic handler to see the difference"),
|
||||
];
|
||||
|
||||
let b = Block::default()
|
||||
.title("Panic Handler Demo")
|
||||
.borders(Borders::ALL);
|
||||
let paragraph = Paragraph::new(text)
|
||||
.block(Block::bordered().title("Panic Handler Demo"))
|
||||
.centered();
|
||||
|
||||
let p = Paragraph::new(text).block(b).centered();
|
||||
|
||||
f.render_widget(p, f.size());
|
||||
f.render_widget(paragraph, f.size());
|
||||
}
|
||||
|
||||
@@ -19,14 +19,18 @@ use std::{
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{Block, Borders, Paragraph, Wrap},
|
||||
backend::{Backend, CrosstermBackend},
|
||||
crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
},
|
||||
layout::{Constraint, Layout},
|
||||
style::{Color, Modifier, Style, Stylize},
|
||||
terminal::{Frame, Terminal},
|
||||
text::{Line, Masked, Span},
|
||||
widgets::{Block, Paragraph, Wrap},
|
||||
};
|
||||
|
||||
struct App {
|
||||
@@ -105,7 +109,7 @@ fn ui(f: &mut Frame, app: &App) {
|
||||
let mut long_line = s.repeat(usize::from(size.width) / s.len() + 4);
|
||||
long_line.push('\n');
|
||||
|
||||
let block = Block::default().black();
|
||||
let block = Block::new().black();
|
||||
f.render_widget(block, size);
|
||||
|
||||
let layout = Layout::vertical([Constraint::Ratio(1, 4); 4]).split(size);
|
||||
@@ -127,8 +131,7 @@ fn ui(f: &mut Frame, app: &App) {
|
||||
];
|
||||
|
||||
let create_block = |title| {
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
Block::bordered()
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.title(Span::styled(
|
||||
title,
|
||||
|
||||
@@ -18,14 +18,17 @@
|
||||
|
||||
use std::{error::Error, io};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{Block, Borders, Clear, Paragraph, Wrap},
|
||||
backend::{Backend, CrosstermBackend},
|
||||
crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
},
|
||||
layout::{Constraint, Layout, Rect},
|
||||
style::Stylize,
|
||||
terminal::{Frame, Terminal},
|
||||
widgets::{Block, Clear, Paragraph, Wrap},
|
||||
};
|
||||
|
||||
struct App {
|
||||
@@ -98,14 +101,11 @@ fn ui(f: &mut Frame, app: &App) {
|
||||
.wrap(Wrap { trim: true });
|
||||
f.render_widget(paragraph, instructions);
|
||||
|
||||
let block = Block::default()
|
||||
.title("Content")
|
||||
.borders(Borders::ALL)
|
||||
.on_blue();
|
||||
let block = Block::bordered().title("Content").on_blue();
|
||||
f.render_widget(block, content);
|
||||
|
||||
if app.show_popup {
|
||||
let block = Block::default().title("Popup").borders(Borders::ALL);
|
||||
let block = Block::bordered().title("Popup");
|
||||
let area = centered_rect(60, 20, area);
|
||||
f.render_widget(Clear, area); //this clears out the background
|
||||
f.render_widget(block, area);
|
||||
|
||||
@@ -19,10 +19,15 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
||||
use indoc::indoc;
|
||||
use itertools::izip;
|
||||
use ratatui::{prelude::*, widgets::Paragraph};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
crossterm::terminal::{disable_raw_mode, enable_raw_mode},
|
||||
terminal::{Terminal, Viewport},
|
||||
widgets::Paragraph,
|
||||
TerminalOptions,
|
||||
};
|
||||
|
||||
/// A fun example of using half block characters to draw a logo
|
||||
#[allow(clippy::many_single_char_names)]
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
#![warn(clippy::pedantic)]
|
||||
#![allow(clippy::wildcard_imports)]
|
||||
|
||||
use std::{
|
||||
error::Error,
|
||||
@@ -22,12 +21,20 @@ use std::{
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
},
|
||||
layout::{Alignment, Constraint, Layout, Margin},
|
||||
style::{Color, Style, Stylize},
|
||||
symbols::scrollbar,
|
||||
terminal::{Frame, Terminal},
|
||||
text::{Line, Masked, Span},
|
||||
widgets::{Block, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState},
|
||||
};
|
||||
use ratatui::{prelude::*, symbols::scrollbar, widgets::*};
|
||||
|
||||
#[derive(Default)]
|
||||
struct App {
|
||||
@@ -186,7 +193,7 @@ fn ui(f: &mut Frame, app: &mut App) {
|
||||
.begin_symbol(None)
|
||||
.track_symbol(None)
|
||||
.end_symbol(None),
|
||||
chunks[2].inner(&Margin {
|
||||
chunks[2].inner(Margin {
|
||||
vertical: 1,
|
||||
horizontal: 0,
|
||||
}),
|
||||
@@ -204,7 +211,7 @@ fn ui(f: &mut Frame, app: &mut App) {
|
||||
Scrollbar::new(ScrollbarOrientation::HorizontalBottom)
|
||||
.thumb_symbol("🬋")
|
||||
.end_symbol(None),
|
||||
chunks[3].inner(&Margin {
|
||||
chunks[3].inner(Margin {
|
||||
vertical: 0,
|
||||
horizontal: 1,
|
||||
}),
|
||||
@@ -222,7 +229,7 @@ fn ui(f: &mut Frame, app: &mut App) {
|
||||
Scrollbar::new(ScrollbarOrientation::HorizontalBottom)
|
||||
.thumb_symbol("░")
|
||||
.track_symbol(Some("─")),
|
||||
chunks[4].inner(&Margin {
|
||||
chunks[4].inner(Margin {
|
||||
vertical: 0,
|
||||
horizontal: 1,
|
||||
}),
|
||||
|
||||
@@ -19,17 +19,20 @@ use std::{
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use rand::{
|
||||
distributions::{Distribution, Uniform},
|
||||
rngs::ThreadRng,
|
||||
};
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
backend::{Backend, CrosstermBackend},
|
||||
crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
},
|
||||
layout::{Constraint, Layout},
|
||||
style::{Color, Style},
|
||||
terminal::{Frame, Terminal},
|
||||
widgets::{Block, Borders, Sparkline},
|
||||
};
|
||||
|
||||
@@ -151,18 +154,18 @@ fn ui(f: &mut Frame, app: &App) {
|
||||
.split(f.size());
|
||||
let sparkline = Sparkline::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Data1")
|
||||
.borders(Borders::LEFT | Borders::RIGHT),
|
||||
Block::new()
|
||||
.borders(Borders::LEFT | Borders::RIGHT)
|
||||
.title("Data1"),
|
||||
)
|
||||
.data(&app.data1)
|
||||
.style(Style::default().fg(Color::Yellow));
|
||||
f.render_widget(sparkline, chunks[0]);
|
||||
let sparkline = Sparkline::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Data2")
|
||||
.borders(Borders::LEFT | Borders::RIGHT),
|
||||
Block::new()
|
||||
.borders(Borders::LEFT | Borders::RIGHT)
|
||||
.title("Data2"),
|
||||
)
|
||||
.data(&app.data2)
|
||||
.style(Style::default().bg(Color::Green));
|
||||
@@ -170,9 +173,9 @@ fn ui(f: &mut Frame, app: &App) {
|
||||
// Multiline
|
||||
let sparkline = Sparkline::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Data3")
|
||||
.borders(Borders::LEFT | Borders::RIGHT),
|
||||
Block::new()
|
||||
.borders(Borders::LEFT | Borders::RIGHT)
|
||||
.title("Data3"),
|
||||
)
|
||||
.data(&app.data3)
|
||||
.style(Style::default().fg(Color::Red));
|
||||
|
||||
@@ -13,17 +13,25 @@
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
#![allow(clippy::enum_glob_use, clippy::wildcard_imports)]
|
||||
|
||||
use std::{error::Error, io};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
},
|
||||
layout::{Constraint, Layout, Margin, Rect},
|
||||
style::{self, Color, Modifier, Style, Stylize},
|
||||
terminal::{Frame, Terminal},
|
||||
text::{Line, Text},
|
||||
widgets::{
|
||||
Block, BorderType, Cell, HighlightSpacing, Paragraph, Row, Scrollbar, ScrollbarOrientation,
|
||||
ScrollbarState, Table, TableState,
|
||||
},
|
||||
};
|
||||
use style::palette::tailwind;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
@@ -212,13 +220,12 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
use KeyCode::*;
|
||||
match key.code {
|
||||
Char('q') | Esc => return Ok(()),
|
||||
Char('j') | Down => app.next(),
|
||||
Char('k') | Up => app.previous(),
|
||||
Char('l') | Right => app.next_color(),
|
||||
Char('h') | Left => app.previous_color(),
|
||||
KeyCode::Char('q') | KeyCode::Esc => return Ok(()),
|
||||
KeyCode::Char('j') | KeyCode::Down => app.next(),
|
||||
KeyCode::Char('k') | KeyCode::Up => app.previous(),
|
||||
KeyCode::Char('l') | KeyCode::Right => app.next_color(),
|
||||
KeyCode::Char('h') | KeyCode::Left => app.previous_color(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -318,7 +325,7 @@ fn render_scrollbar(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
.orientation(ScrollbarOrientation::VerticalRight)
|
||||
.begin_symbol(None)
|
||||
.end_symbol(None),
|
||||
area.inner(&Margin {
|
||||
area.inner(Margin {
|
||||
vertical: 1,
|
||||
horizontal: 1,
|
||||
}),
|
||||
@@ -331,10 +338,9 @@ fn render_footer(f: &mut Frame, app: &App, area: Rect) {
|
||||
.style(Style::new().fg(app.colors.row_fg).bg(app.colors.buffer_bg))
|
||||
.centered()
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::new().fg(app.colors.footer_border_color))
|
||||
.border_type(BorderType::Double),
|
||||
Block::bordered()
|
||||
.border_type(BorderType::Double)
|
||||
.border_style(Style::new().fg(app.colors.footer_border_color)),
|
||||
);
|
||||
f.render_widget(info_footer, area);
|
||||
}
|
||||
|
||||
@@ -13,17 +13,24 @@
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
#![allow(clippy::wildcard_imports, clippy::enum_glob_use)]
|
||||
|
||||
use std::io::stdout;
|
||||
|
||||
use color_eyre::{config::HookBuilder, Result};
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode, KeyEventKind},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
buffer::Buffer,
|
||||
crossterm::{
|
||||
event::{self, Event, KeyCode, KeyEventKind},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
},
|
||||
layout::{Constraint, Layout, Rect},
|
||||
style::{palette::tailwind, Color, Stylize},
|
||||
symbols,
|
||||
terminal::Terminal,
|
||||
text::Line,
|
||||
widgets::{Block, Padding, Paragraph, Tabs, Widget},
|
||||
};
|
||||
use ratatui::{prelude::*, style::palette::tailwind, widgets::*};
|
||||
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -77,11 +84,10 @@ impl App {
|
||||
fn handle_events(&mut self) -> std::io::Result<()> {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
use KeyCode::*;
|
||||
match key.code {
|
||||
Char('l') | Right => self.next_tab(),
|
||||
Char('h') | Left => self.previous_tab(),
|
||||
Char('q') | Esc => self.quit(),
|
||||
KeyCode::Char('l') | KeyCode::Right => self.next_tab(),
|
||||
KeyCode::Char('h') | KeyCode::Left => self.previous_tab(),
|
||||
KeyCode::Char('q') | KeyCode::Esc => self.quit(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -120,7 +126,7 @@ impl SelectedTab {
|
||||
|
||||
impl Widget for &App {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
use Constraint::*;
|
||||
use Constraint::{Length, Min};
|
||||
let vertical = Layout::vertical([Length(1), Min(0), Length(1)]);
|
||||
let [header_area, inner_area, footer_area] = vertical.areas(area);
|
||||
|
||||
@@ -205,8 +211,7 @@ impl SelectedTab {
|
||||
|
||||
/// A block surrounding the tab's content
|
||||
fn block(self) -> Block<'static> {
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
Block::bordered()
|
||||
.border_set(symbols::border::PROPORTIONAL_TALL)
|
||||
.padding(Padding::horizontal(1))
|
||||
.border_style(self.palette().c700)
|
||||
|
||||
@@ -29,14 +29,18 @@
|
||||
|
||||
use std::{error::Error, io};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{Block, Borders, List, ListItem, Paragraph},
|
||||
backend::{Backend, CrosstermBackend},
|
||||
crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
},
|
||||
layout::{Constraint, Layout},
|
||||
style::{Color, Modifier, Style, Stylize},
|
||||
terminal::{Frame, Terminal},
|
||||
text::{Line, Span, Text},
|
||||
widgets::{Block, List, ListItem, Paragraph},
|
||||
};
|
||||
|
||||
enum InputMode {
|
||||
@@ -49,7 +53,7 @@ struct App {
|
||||
/// Current value of the input box
|
||||
input: String,
|
||||
/// Position of cursor in the editor area.
|
||||
cursor_position: usize,
|
||||
character_index: usize,
|
||||
/// Current input mode
|
||||
input_mode: InputMode,
|
||||
/// History of recorded messages
|
||||
@@ -62,34 +66,46 @@ impl App {
|
||||
input: String::new(),
|
||||
input_mode: InputMode::Normal,
|
||||
messages: Vec::new(),
|
||||
cursor_position: 0,
|
||||
character_index: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn move_cursor_left(&mut self) {
|
||||
let cursor_moved_left = self.cursor_position.saturating_sub(1);
|
||||
self.cursor_position = self.clamp_cursor(cursor_moved_left);
|
||||
let cursor_moved_left = self.character_index.saturating_sub(1);
|
||||
self.character_index = self.clamp_cursor(cursor_moved_left);
|
||||
}
|
||||
|
||||
fn move_cursor_right(&mut self) {
|
||||
let cursor_moved_right = self.cursor_position.saturating_add(1);
|
||||
self.cursor_position = self.clamp_cursor(cursor_moved_right);
|
||||
let cursor_moved_right = self.character_index.saturating_add(1);
|
||||
self.character_index = self.clamp_cursor(cursor_moved_right);
|
||||
}
|
||||
|
||||
fn enter_char(&mut self, new_char: char) {
|
||||
self.input.insert(self.cursor_position, new_char);
|
||||
|
||||
let index = self.byte_index();
|
||||
self.input.insert(index, new_char);
|
||||
self.move_cursor_right();
|
||||
}
|
||||
|
||||
/// Returns the byte index based on the character position.
|
||||
///
|
||||
/// Since each character in a string can be contain multiple bytes, it's necessary to calculate
|
||||
/// the byte index based on the index of the character.
|
||||
fn byte_index(&self) -> usize {
|
||||
self.input
|
||||
.char_indices()
|
||||
.map(|(i, _)| i)
|
||||
.nth(self.character_index)
|
||||
.unwrap_or(self.input.len())
|
||||
}
|
||||
|
||||
fn delete_char(&mut self) {
|
||||
let is_not_cursor_leftmost = self.cursor_position != 0;
|
||||
let is_not_cursor_leftmost = self.character_index != 0;
|
||||
if is_not_cursor_leftmost {
|
||||
// Method "remove" is not used on the saved text for deleting the selected char.
|
||||
// Reason: Using remove on String works on bytes instead of the chars.
|
||||
// Using remove would require special care because of char boundaries.
|
||||
|
||||
let current_index = self.cursor_position;
|
||||
let current_index = self.character_index;
|
||||
let from_left_to_current_index = current_index - 1;
|
||||
|
||||
// Getting all characters before the selected character.
|
||||
@@ -105,11 +121,11 @@ impl App {
|
||||
}
|
||||
|
||||
fn clamp_cursor(&self, new_cursor_pos: usize) -> usize {
|
||||
new_cursor_pos.clamp(0, self.input.len())
|
||||
new_cursor_pos.clamp(0, self.input.chars().count())
|
||||
}
|
||||
|
||||
fn reset_cursor(&mut self) {
|
||||
self.cursor_position = 0;
|
||||
self.character_index = 0;
|
||||
}
|
||||
|
||||
fn submit_message(&mut self) {
|
||||
@@ -226,7 +242,7 @@ fn ui(f: &mut Frame, app: &App) {
|
||||
InputMode::Normal => Style::default(),
|
||||
InputMode::Editing => Style::default().fg(Color::Yellow),
|
||||
})
|
||||
.block(Block::default().borders(Borders::ALL).title("Input"));
|
||||
.block(Block::bordered().title("Input"));
|
||||
f.render_widget(input, input_area);
|
||||
match app.input_mode {
|
||||
InputMode::Normal =>
|
||||
@@ -240,7 +256,7 @@ fn ui(f: &mut Frame, app: &App) {
|
||||
f.set_cursor(
|
||||
// Draw the cursor at the current position in the input field.
|
||||
// This position is can be controlled via the left and right arrow key
|
||||
input_area.x + app.cursor_position as u16 + 1,
|
||||
input_area.x + app.character_index as u16 + 1,
|
||||
// Move one line down, from the border to the input line
|
||||
input_area.y + 1,
|
||||
);
|
||||
@@ -256,7 +272,6 @@ fn ui(f: &mut Frame, app: &App) {
|
||||
ListItem::new(content)
|
||||
})
|
||||
.collect();
|
||||
let messages =
|
||||
List::new(messages).block(Block::default().borders(Borders::ALL).title("Messages"));
|
||||
let messages = List::new(messages).block(Block::bordered().title("Messages"));
|
||||
f.render_widget(messages, messages_area);
|
||||
}
|
||||
|
||||
14
examples/vhs/line_gauge.tape
Normal file
14
examples/vhs/line_gauge.tape
Normal file
@@ -0,0 +1,14 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/line_gauge.tape`
|
||||
Output "target/line_gauge.gif"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 1200
|
||||
Set Height 850
|
||||
Hide
|
||||
Type "cargo run --example=line_gauge --features=crossterm"
|
||||
Enter
|
||||
Sleep 2s
|
||||
Show
|
||||
Sleep 2s
|
||||
Enter 1
|
||||
Sleep 15s
|
||||
@@ -1,6 +1,9 @@
|
||||
# configuration for https://rust-lang.github.io/rustfmt/
|
||||
group_imports = "StdExternalCrate"
|
||||
imports_granularity = "Crate"
|
||||
wrap_comments = true
|
||||
comment_width = 100
|
||||
format_code_in_doc_comments = true
|
||||
format_macro_matchers=true
|
||||
group_imports = "StdExternalCrate"
|
||||
imports_granularity = "Crate"
|
||||
normalize_doc_attributes=true
|
||||
use_field_init_shorthand=true
|
||||
wrap_comments = true
|
||||
|
||||
@@ -6,19 +6,19 @@ use std::io::{self, Write};
|
||||
|
||||
#[cfg(feature = "underline-color")]
|
||||
use crossterm::style::SetUnderlineColor;
|
||||
use crossterm::{
|
||||
cursor::{Hide, MoveTo, Show},
|
||||
execute, queue,
|
||||
style::{
|
||||
Attribute as CAttribute, Attributes as CAttributes, Color as CColor, ContentStyle, Print,
|
||||
SetAttribute, SetBackgroundColor, SetForegroundColor,
|
||||
},
|
||||
terminal::{self, Clear},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
backend::{Backend, ClearType, WindowSize},
|
||||
buffer::Cell,
|
||||
crossterm::{
|
||||
cursor::{Hide, MoveTo, Show},
|
||||
execute, queue,
|
||||
style::{
|
||||
Attribute as CAttribute, Attributes as CAttributes, Color as CColor, Colors,
|
||||
ContentStyle, Print, SetAttribute, SetBackgroundColor, SetColors, SetForegroundColor,
|
||||
},
|
||||
terminal::{self, Clear},
|
||||
},
|
||||
layout::Size,
|
||||
prelude::Rect,
|
||||
style::{Color, Modifier, Style},
|
||||
@@ -45,11 +45,15 @@ use crate::{
|
||||
/// ```rust,no_run
|
||||
/// use std::io::{stderr, stdout};
|
||||
///
|
||||
/// use crossterm::{
|
||||
/// terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
/// ExecutableCommand,
|
||||
/// use ratatui::{
|
||||
/// crossterm::{
|
||||
/// terminal::{
|
||||
/// disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
|
||||
/// },
|
||||
/// ExecutableCommand,
|
||||
/// },
|
||||
/// prelude::*,
|
||||
/// };
|
||||
/// use ratatui::prelude::*;
|
||||
///
|
||||
/// let mut backend = CrosstermBackend::new(stdout());
|
||||
/// // or
|
||||
@@ -100,6 +104,27 @@ where
|
||||
pub const fn new(writer: W) -> Self {
|
||||
Self { writer }
|
||||
}
|
||||
|
||||
/// Gets the writer.
|
||||
#[stability::unstable(
|
||||
feature = "backend-writer",
|
||||
issue = "https://github.com/ratatui-org/ratatui/pull/991"
|
||||
)]
|
||||
pub const fn writer(&self) -> &W {
|
||||
&self.writer
|
||||
}
|
||||
|
||||
/// Gets the writer as a mutable reference.
|
||||
///
|
||||
/// Note: writing to the writer may cause incorrect output after the write. This is due to the
|
||||
/// way that the Terminal implements diffing Buffers.
|
||||
#[stability::unstable(
|
||||
feature = "backend-writer",
|
||||
issue = "https://github.com/ratatui-org/ratatui/pull/991"
|
||||
)]
|
||||
pub fn writer_mut(&mut self) -> &mut W {
|
||||
&mut self.writer
|
||||
}
|
||||
}
|
||||
|
||||
impl<W> Write for CrosstermBackend<W>
|
||||
@@ -145,14 +170,12 @@ where
|
||||
diff.queue(&mut self.writer)?;
|
||||
modifier = cell.modifier;
|
||||
}
|
||||
if cell.fg != fg {
|
||||
let color = CColor::from(cell.fg);
|
||||
queue!(self.writer, SetForegroundColor(color))?;
|
||||
if cell.fg != fg || cell.bg != bg {
|
||||
queue!(
|
||||
self.writer,
|
||||
SetColors(Colors::new(cell.fg.into(), cell.bg.into()))
|
||||
)?;
|
||||
fg = cell.fg;
|
||||
}
|
||||
if cell.bg != bg {
|
||||
let color = CColor::from(cell.bg);
|
||||
queue!(self.writer, SetBackgroundColor(color))?;
|
||||
bg = cell.bg;
|
||||
}
|
||||
#[cfg(feature = "underline-color")]
|
||||
@@ -228,7 +251,7 @@ where
|
||||
Ok(Rect::new(0, 0, width, height))
|
||||
}
|
||||
|
||||
fn window_size(&mut self) -> Result<WindowSize, io::Error> {
|
||||
fn window_size(&mut self) -> io::Result<WindowSize> {
|
||||
let crossterm::terminal::WindowSize {
|
||||
columns,
|
||||
rows,
|
||||
|
||||
@@ -9,13 +9,12 @@ 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},
|
||||
termion::{self, color as tcolor, color::Color as _, style as tstyle},
|
||||
};
|
||||
|
||||
/// A [`Backend`] implementation that uses [Termion] to render to the terminal.
|
||||
@@ -40,8 +39,10 @@ use crate::{
|
||||
/// ```rust,no_run
|
||||
/// use std::io::{stderr, stdout};
|
||||
///
|
||||
/// use ratatui::prelude::*;
|
||||
/// use termion::{raw::IntoRawMode, screen::IntoAlternateScreen};
|
||||
/// use ratatui::{
|
||||
/// prelude::*,
|
||||
/// termion::{raw::IntoRawMode, screen::IntoAlternateScreen},
|
||||
/// };
|
||||
///
|
||||
/// let writer = stdout().into_raw_mode()?.into_alternate_screen()?;
|
||||
/// let mut backend = TermionBackend::new(writer);
|
||||
@@ -85,6 +86,26 @@ where
|
||||
pub const fn new(writer: W) -> Self {
|
||||
Self { writer }
|
||||
}
|
||||
|
||||
/// Gets the writer.
|
||||
#[stability::unstable(
|
||||
feature = "backend-writer",
|
||||
issue = "https://github.com/ratatui-org/ratatui/pull/991"
|
||||
)]
|
||||
pub const fn writer(&self) -> &W {
|
||||
&self.writer
|
||||
}
|
||||
|
||||
/// Gets the writer as a mutable reference.
|
||||
/// Note: writing to the writer may cause incorrect output after the write. This is due to the
|
||||
/// way that the Terminal implements diffing Buffers.
|
||||
#[stability::unstable(
|
||||
feature = "backend-writer",
|
||||
issue = "https://github.com/ratatui-org/ratatui/pull/991"
|
||||
)]
|
||||
pub fn writer_mut(&mut self) -> &mut W {
|
||||
&mut self.writer
|
||||
}
|
||||
}
|
||||
|
||||
impl<W> Write for TermionBackend<W>
|
||||
@@ -198,7 +219,7 @@ where
|
||||
Ok(Rect::new(0, 0, terminal.0, terminal.1))
|
||||
}
|
||||
|
||||
fn window_size(&mut self) -> Result<WindowSize, io::Error> {
|
||||
fn window_size(&mut self) -> io::Result<WindowSize> {
|
||||
Ok(WindowSize {
|
||||
columns_rows: termion::terminal_size()?.into(),
|
||||
pixels: termion::terminal_size_pixels()?.into(),
|
||||
@@ -223,7 +244,6 @@ struct ModifierDiff {
|
||||
|
||||
impl fmt::Display for Fg {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
use termion::color::Color as TermionColor;
|
||||
match self.0 {
|
||||
Color::Reset => termion::color::Reset.write_fg(f),
|
||||
Color::Black => termion::color::Black.write_fg(f),
|
||||
@@ -249,7 +269,6 @@ impl fmt::Display for Fg {
|
||||
}
|
||||
impl fmt::Display for Bg {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
use termion::color::Color as TermionColor;
|
||||
match self.0 {
|
||||
Color::Reset => termion::color::Reset.write_bg(f),
|
||||
Color::Black => termion::color::Black.write_bg(f),
|
||||
@@ -275,7 +294,7 @@ impl fmt::Display for Bg {
|
||||
}
|
||||
|
||||
macro_rules! from_termion_for_color {
|
||||
($termion_color:ident, $color: ident) => {
|
||||
($termion_color:ident, $color:ident) => {
|
||||
impl From<tcolor::$termion_color> for Color {
|
||||
fn from(_: tcolor::$termion_color) -> Self {
|
||||
Color::$color
|
||||
@@ -416,7 +435,7 @@ impl fmt::Display for ModifierDiff {
|
||||
}
|
||||
|
||||
macro_rules! from_termion_for_modifier {
|
||||
($termion_modifier:ident, $modifier: ident) => {
|
||||
($termion_modifier:ident, $modifier:ident) => {
|
||||
impl From<tstyle::$termion_modifier> for Modifier {
|
||||
fn from(_: tstyle::$termion_modifier) -> Self {
|
||||
Modifier::$modifier
|
||||
|
||||
@@ -7,20 +7,19 @@
|
||||
|
||||
use std::{error::Error, io};
|
||||
|
||||
use termwiz::{
|
||||
caps::Capabilities,
|
||||
cell::{AttributeChange, Blink, CellAttributes, Intensity, Underline},
|
||||
color::{AnsiColor, ColorAttribute, ColorSpec, LinearRgba, RgbColor, SrgbaTuple},
|
||||
surface::{Change, CursorVisibility, Position},
|
||||
terminal::{buffered::BufferedTerminal, ScreenSize, SystemTerminal, Terminal},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
backend::{Backend, WindowSize},
|
||||
buffer::Cell,
|
||||
layout::Size,
|
||||
prelude::Rect,
|
||||
style::{Color, Modifier, Style},
|
||||
termwiz::{
|
||||
caps::Capabilities,
|
||||
cell::{AttributeChange, Blink, CellAttributes, Intensity, Underline},
|
||||
color::{AnsiColor, ColorAttribute, ColorSpec, LinearRgba, RgbColor, SrgbaTuple},
|
||||
surface::{Change, CursorVisibility, Position},
|
||||
terminal::{buffered::BufferedTerminal, ScreenSize, SystemTerminal, Terminal},
|
||||
},
|
||||
};
|
||||
|
||||
/// A [`Backend`] implementation that uses [Termwiz] to render to the terminal.
|
||||
@@ -111,7 +110,7 @@ impl TermwizBackend {
|
||||
}
|
||||
|
||||
impl Backend for TermwizBackend {
|
||||
fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error>
|
||||
fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
|
||||
where
|
||||
I: Iterator<Item = (u16, u16, &'a Cell)>,
|
||||
{
|
||||
@@ -181,13 +180,13 @@ impl Backend for TermwizBackend {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn hide_cursor(&mut self) -> Result<(), io::Error> {
|
||||
fn hide_cursor(&mut self) -> io::Result<()> {
|
||||
self.buffered_terminal
|
||||
.add_change(Change::CursorVisibility(CursorVisibility::Hidden));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn show_cursor(&mut self) -> Result<(), io::Error> {
|
||||
fn show_cursor(&mut self) -> io::Result<()> {
|
||||
self.buffered_terminal
|
||||
.add_change(Change::CursorVisibility(CursorVisibility::Visible));
|
||||
Ok(())
|
||||
@@ -207,18 +206,18 @@ impl Backend for TermwizBackend {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear(&mut self) -> Result<(), io::Error> {
|
||||
fn clear(&mut self) -> io::Result<()> {
|
||||
self.buffered_terminal
|
||||
.add_change(Change::ClearScreen(termwiz::color::ColorAttribute::Default));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn size(&self) -> Result<Rect, io::Error> {
|
||||
fn size(&self) -> io::Result<Rect> {
|
||||
let (cols, rows) = self.buffered_terminal.dimensions();
|
||||
Ok(Rect::new(0, 0, u16_max(cols), u16_max(rows)))
|
||||
}
|
||||
|
||||
fn window_size(&mut self) -> Result<WindowSize, io::Error> {
|
||||
fn window_size(&mut self) -> io::Result<WindowSize> {
|
||||
let ScreenSize {
|
||||
cols,
|
||||
rows,
|
||||
@@ -241,7 +240,7 @@ impl Backend for TermwizBackend {
|
||||
})
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> Result<(), io::Error> {
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.buffered_terminal
|
||||
.flush()
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
|
||||
@@ -271,7 +270,7 @@ impl From<CellAttributes> for Style {
|
||||
|
||||
style.fg = Some(value.foreground().into());
|
||||
style.bg = Some(value.background().into());
|
||||
#[cfg(feature = "underline_color")]
|
||||
#[cfg(feature = "underline-color")]
|
||||
{
|
||||
style.underline_color = Some(value.underline_color().into());
|
||||
}
|
||||
@@ -407,7 +406,6 @@ fn u16_max(i: usize) -> u16 {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::style::Stylize;
|
||||
|
||||
mod into_color {
|
||||
use Color as C;
|
||||
@@ -576,11 +574,19 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn from_cell_attribute_for_style() {
|
||||
use crate::style::Stylize;
|
||||
|
||||
#[cfg(feature = "underline-color")]
|
||||
const STYLE: Style = Style::new()
|
||||
.underline_color(Color::Reset)
|
||||
.fg(Color::Reset)
|
||||
.bg(Color::Reset);
|
||||
#[cfg(not(feature = "underline-color"))]
|
||||
const STYLE: Style = Style::new().fg(Color::Reset).bg(Color::Reset);
|
||||
|
||||
// default
|
||||
assert_eq!(
|
||||
Style::from(CellAttributes::default()),
|
||||
Style::new().fg(Color::Reset).bg(Color::Reset)
|
||||
);
|
||||
assert_eq!(Style::from(CellAttributes::default()), STYLE);
|
||||
|
||||
// foreground color
|
||||
assert_eq!(
|
||||
Style::from(
|
||||
@@ -588,7 +594,7 @@ mod tests {
|
||||
.set_foreground(ColorAttribute::PaletteIndex(31))
|
||||
.to_owned()
|
||||
),
|
||||
Style::new().fg(Color::Indexed(31)).bg(Color::Reset)
|
||||
STYLE.fg(Color::Indexed(31))
|
||||
);
|
||||
// background color
|
||||
assert_eq!(
|
||||
@@ -597,21 +603,7 @@ mod tests {
|
||||
.set_background(ColorAttribute::PaletteIndex(31))
|
||||
.to_owned()
|
||||
),
|
||||
Style::new().fg(Color::Reset).bg(Color::Indexed(31))
|
||||
);
|
||||
// underline color
|
||||
#[cfg(feature = "underline_color")]
|
||||
assert_eq!(
|
||||
Style::from(
|
||||
CellAttributes::default()
|
||||
.set_underline_color(AnsiColor::Red)
|
||||
.set
|
||||
.to_owned()
|
||||
),
|
||||
Style::new()
|
||||
.fg(Color::Reset)
|
||||
.bg(Color::Reset)
|
||||
.underline_color(Color::Red)
|
||||
STYLE.bg(Color::Indexed(31))
|
||||
);
|
||||
// underlined
|
||||
assert_eq!(
|
||||
@@ -620,12 +612,12 @@ mod tests {
|
||||
.set_underline(Underline::Single)
|
||||
.to_owned()
|
||||
),
|
||||
Style::new().fg(Color::Reset).bg(Color::Reset).underlined()
|
||||
STYLE.underlined()
|
||||
);
|
||||
// blink
|
||||
assert_eq!(
|
||||
Style::from(CellAttributes::default().set_blink(Blink::Slow).to_owned()),
|
||||
Style::new().fg(Color::Reset).bg(Color::Reset).slow_blink()
|
||||
STYLE.slow_blink()
|
||||
);
|
||||
// intensity
|
||||
assert_eq!(
|
||||
@@ -634,27 +626,38 @@ mod tests {
|
||||
.set_intensity(Intensity::Bold)
|
||||
.to_owned()
|
||||
),
|
||||
Style::new().fg(Color::Reset).bg(Color::Reset).bold()
|
||||
STYLE.bold()
|
||||
);
|
||||
// italic
|
||||
assert_eq!(
|
||||
Style::from(CellAttributes::default().set_italic(true).to_owned()),
|
||||
Style::new().fg(Color::Reset).bg(Color::Reset).italic()
|
||||
STYLE.italic()
|
||||
);
|
||||
// reversed
|
||||
assert_eq!(
|
||||
Style::from(CellAttributes::default().set_reverse(true).to_owned()),
|
||||
Style::new().fg(Color::Reset).bg(Color::Reset).reversed()
|
||||
STYLE.reversed()
|
||||
);
|
||||
// strikethrough
|
||||
assert_eq!(
|
||||
Style::from(CellAttributes::default().set_strikethrough(true).to_owned()),
|
||||
Style::new().fg(Color::Reset).bg(Color::Reset).crossed_out()
|
||||
STYLE.crossed_out()
|
||||
);
|
||||
// hidden
|
||||
assert_eq!(
|
||||
Style::from(CellAttributes::default().set_invisible(true).to_owned()),
|
||||
Style::new().fg(Color::Reset).bg(Color::Reset).hidden()
|
||||
STYLE.hidden()
|
||||
);
|
||||
|
||||
// underline color
|
||||
#[cfg(feature = "underline-color")]
|
||||
assert_eq!(
|
||||
Style::from(
|
||||
CellAttributes::default()
|
||||
.set_underline_color(AnsiColor::Red)
|
||||
.to_owned()
|
||||
),
|
||||
STYLE.underline_color(Color::Indexed(9))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,21 +2,19 @@
|
||||
//! It is used in the integration tests to verify the correctness of the library.
|
||||
|
||||
use std::{
|
||||
fmt::{Display, Write},
|
||||
fmt::{self, Write},
|
||||
io,
|
||||
};
|
||||
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{
|
||||
assert_buffer_eq,
|
||||
backend::{Backend, ClearType, WindowSize},
|
||||
buffer::{Buffer, Cell},
|
||||
layout::{Rect, Size},
|
||||
};
|
||||
|
||||
/// A [`Backend`] implementation used for integration testing that that renders to an in memory
|
||||
/// buffer.
|
||||
/// A [`Backend`] implementation used for integration testing that renders to an memory buffer.
|
||||
///
|
||||
/// Note: that although many of the integration and unit tests in ratatui are written using this
|
||||
/// backend, it is preferable to write unit tests for widgets directly against the buffer rather
|
||||
@@ -30,7 +28,7 @@ use crate::{
|
||||
///
|
||||
/// let mut backend = TestBackend::new(10, 2);
|
||||
/// backend.clear()?;
|
||||
/// backend.assert_buffer(&Buffer::with_lines(vec![" "; 2]));
|
||||
/// backend.assert_buffer_lines([" "; 2]);
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
@@ -97,24 +95,46 @@ impl TestBackend {
|
||||
}
|
||||
|
||||
/// Asserts that the `TestBackend`'s buffer is equal to the expected buffer.
|
||||
/// If the buffers are not equal, a panic occurs with a detailed error message
|
||||
/// showing the differences between the expected and actual buffers.
|
||||
///
|
||||
/// This is a shortcut for `assert_eq!(self.buffer(), &expected)`.
|
||||
///
|
||||
/// # Panics
|
||||
/// When they are not equal, a panic occurs with a detailed error message showing the
|
||||
/// differences between the expected and actual buffers.
|
||||
#[allow(deprecated)]
|
||||
#[track_caller]
|
||||
pub fn assert_buffer(&self, expected: &Buffer) {
|
||||
assert_buffer_eq!(&self.buffer, expected);
|
||||
// TODO: use assert_eq!()
|
||||
crate::assert_buffer_eq!(&self.buffer, expected);
|
||||
}
|
||||
|
||||
/// Asserts that the `TestBackend`'s buffer is equal to the expected lines.
|
||||
///
|
||||
/// This is a shortcut for `assert_eq!(self.buffer(), &Buffer::with_lines(expected))`.
|
||||
///
|
||||
/// # Panics
|
||||
/// When they are not equal, a panic occurs with a detailed error message showing the
|
||||
/// differences between the expected and actual buffers.
|
||||
#[track_caller]
|
||||
pub fn assert_buffer_lines<'line, Lines>(&self, expected: Lines)
|
||||
where
|
||||
Lines: IntoIterator,
|
||||
Lines::Item: Into<crate::text::Line<'line>>,
|
||||
{
|
||||
self.assert_buffer(&Buffer::with_lines(expected));
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for TestBackend {
|
||||
impl fmt::Display for TestBackend {
|
||||
/// Formats the `TestBackend` for display by calling the `buffer_view` function
|
||||
/// on its internal buffer.
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", buffer_view(&self.buffer))
|
||||
}
|
||||
}
|
||||
|
||||
impl Backend for TestBackend {
|
||||
fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error>
|
||||
fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
|
||||
where
|
||||
I: Iterator<Item = (u16, u16, &'a Cell)>,
|
||||
{
|
||||
@@ -125,51 +145,54 @@ impl Backend for TestBackend {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn hide_cursor(&mut self) -> Result<(), io::Error> {
|
||||
fn hide_cursor(&mut self) -> io::Result<()> {
|
||||
self.cursor = false;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn show_cursor(&mut self) -> Result<(), io::Error> {
|
||||
fn show_cursor(&mut self) -> io::Result<()> {
|
||||
self.cursor = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_cursor(&mut self) -> Result<(u16, u16), io::Error> {
|
||||
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
|
||||
Ok(self.pos)
|
||||
}
|
||||
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> Result<(), io::Error> {
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
||||
self.pos = (x, y);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear(&mut self) -> Result<(), io::Error> {
|
||||
fn clear(&mut self) -> io::Result<()> {
|
||||
self.buffer.reset();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear_region(&mut self, clear_type: super::ClearType) -> io::Result<()> {
|
||||
match clear_type {
|
||||
ClearType::All => self.clear()?,
|
||||
let region = match clear_type {
|
||||
ClearType::All => return self.clear(),
|
||||
ClearType::AfterCursor => {
|
||||
let index = self.buffer.index_of(self.pos.0, self.pos.1) + 1;
|
||||
self.buffer.content[index..].fill(Cell::default());
|
||||
&mut self.buffer.content[index..]
|
||||
}
|
||||
ClearType::BeforeCursor => {
|
||||
let index = self.buffer.index_of(self.pos.0, self.pos.1);
|
||||
self.buffer.content[..index].fill(Cell::default());
|
||||
&mut self.buffer.content[..index]
|
||||
}
|
||||
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());
|
||||
&mut self.buffer.content[line_start_index..=line_end_index]
|
||||
}
|
||||
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());
|
||||
&mut self.buffer.content[index..=line_end_index]
|
||||
}
|
||||
};
|
||||
for cell in region {
|
||||
cell.reset();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -214,11 +237,11 @@ impl Backend for TestBackend {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn size(&self) -> Result<Rect, io::Error> {
|
||||
fn size(&self) -> io::Result<Rect> {
|
||||
Ok(Rect::new(0, 0, self.width, self.height))
|
||||
}
|
||||
|
||||
fn window_size(&mut self) -> Result<WindowSize, io::Error> {
|
||||
fn window_size(&mut self) -> io::Result<WindowSize> {
|
||||
// Some arbitrary window pixel size, probably doesn't need much testing.
|
||||
static WINDOW_PIXEL_SIZE: Size = Size {
|
||||
width: 640,
|
||||
@@ -230,7 +253,7 @@ impl Backend for TestBackend {
|
||||
})
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> Result<(), io::Error> {
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -246,7 +269,7 @@ mod tests {
|
||||
TestBackend {
|
||||
width: 10,
|
||||
height: 2,
|
||||
buffer: Buffer::with_lines(vec![" "; 2]),
|
||||
buffer: Buffer::with_lines([" "; 2]),
|
||||
cursor: false,
|
||||
pos: (0, 0),
|
||||
}
|
||||
@@ -254,14 +277,14 @@ mod tests {
|
||||
}
|
||||
#[test]
|
||||
fn test_buffer_view() {
|
||||
let buffer = Buffer::with_lines(vec!["aaaa"; 2]);
|
||||
let buffer = Buffer::with_lines(["aaaa"; 2]);
|
||||
assert_eq!(buffer_view(&buffer), "\"aaaa\"\n\"aaaa\"\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_view_with_overwrites() {
|
||||
let multi_byte_char = "👨👩👧👦"; // renders 8 wide
|
||||
let buffer = Buffer::with_lines(vec![multi_byte_char]);
|
||||
let buffer = Buffer::with_lines([multi_byte_char]);
|
||||
assert_eq!(
|
||||
buffer_view(&buffer),
|
||||
format!(
|
||||
@@ -274,29 +297,27 @@ mod tests {
|
||||
#[test]
|
||||
fn buffer() {
|
||||
let backend = TestBackend::new(10, 2);
|
||||
assert_eq!(backend.buffer(), &Buffer::with_lines(vec![" "; 2]));
|
||||
backend.assert_buffer_lines([" "; 2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resize() {
|
||||
let mut backend = TestBackend::new(10, 2);
|
||||
backend.resize(5, 5);
|
||||
assert_eq!(backend.buffer(), &Buffer::with_lines(vec![" "; 5]));
|
||||
backend.assert_buffer_lines([" "; 5]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn assert_buffer() {
|
||||
let backend = TestBackend::new(10, 2);
|
||||
let buffer = Buffer::with_lines(vec![" "; 2]);
|
||||
backend.assert_buffer(&buffer);
|
||||
backend.assert_buffer_lines([" "; 2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic = "buffer contents not equal"]
|
||||
fn assert_buffer_panics() {
|
||||
let backend = TestBackend::new(10, 2);
|
||||
let buffer = Buffer::with_lines(vec!["aaaaaaaaaa"; 2]);
|
||||
backend.assert_buffer(&buffer);
|
||||
backend.assert_buffer_lines(["aaaaaaaaaa"; 2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -308,11 +329,10 @@ mod tests {
|
||||
#[test]
|
||||
fn draw() {
|
||||
let mut backend = TestBackend::new(10, 2);
|
||||
let mut cell = Cell::default();
|
||||
cell.set_symbol("a");
|
||||
let cell = Cell::new("a");
|
||||
backend.draw([(0, 0, &cell)].into_iter()).unwrap();
|
||||
backend.draw([(0, 1, &cell)].into_iter()).unwrap();
|
||||
backend.assert_buffer(&Buffer::with_lines(vec!["a "; 2]));
|
||||
backend.assert_buffer_lines(["a "; 2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -344,24 +364,18 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn clear() {
|
||||
let mut backend = TestBackend::new(10, 4);
|
||||
let mut cell = Cell::default();
|
||||
cell.set_symbol("a");
|
||||
let mut backend = TestBackend::new(4, 2);
|
||||
let cell = Cell::new("a");
|
||||
backend.draw([(0, 0, &cell)].into_iter()).unwrap();
|
||||
backend.draw([(0, 1, &cell)].into_iter()).unwrap();
|
||||
backend.clear().unwrap();
|
||||
backend.assert_buffer(&Buffer::with_lines(vec![
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
]));
|
||||
backend.assert_buffer_lines([" ", " "]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_region_all() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines(vec![
|
||||
backend.buffer = Buffer::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
@@ -370,19 +384,19 @@ mod tests {
|
||||
]);
|
||||
|
||||
backend.clear_region(ClearType::All).unwrap();
|
||||
backend.assert_buffer(&Buffer::with_lines(vec![
|
||||
backend.assert_buffer_lines([
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
]));
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_region_after_cursor() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines(vec![
|
||||
backend.buffer = Buffer::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
@@ -392,19 +406,19 @@ mod tests {
|
||||
|
||||
backend.set_cursor(3, 2).unwrap();
|
||||
backend.clear_region(ClearType::AfterCursor).unwrap();
|
||||
backend.assert_buffer(&Buffer::with_lines(vec![
|
||||
backend.assert_buffer_lines([
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaa ",
|
||||
" ",
|
||||
" ",
|
||||
]));
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_region_before_cursor() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines(vec![
|
||||
backend.buffer = Buffer::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
@@ -414,19 +428,19 @@ mod tests {
|
||||
|
||||
backend.set_cursor(5, 3).unwrap();
|
||||
backend.clear_region(ClearType::BeforeCursor).unwrap();
|
||||
backend.assert_buffer(&Buffer::with_lines(vec![
|
||||
backend.assert_buffer_lines([
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" aaaaa",
|
||||
"aaaaaaaaaa",
|
||||
]));
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_region_current_line() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines(vec![
|
||||
backend.buffer = Buffer::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
@@ -436,19 +450,19 @@ mod tests {
|
||||
|
||||
backend.set_cursor(3, 1).unwrap();
|
||||
backend.clear_region(ClearType::CurrentLine).unwrap();
|
||||
backend.assert_buffer(&Buffer::with_lines(vec![
|
||||
backend.assert_buffer_lines([
|
||||
"aaaaaaaaaa",
|
||||
" ",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
]));
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_region_until_new_line() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines(vec![
|
||||
backend.buffer = Buffer::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
@@ -458,19 +472,19 @@ mod tests {
|
||||
|
||||
backend.set_cursor(3, 0).unwrap();
|
||||
backend.clear_region(ClearType::UntilNewLine).unwrap();
|
||||
backend.assert_buffer(&Buffer::with_lines(vec![
|
||||
backend.assert_buffer_lines([
|
||||
"aaa ",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
]));
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_lines_not_at_last_line() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines(vec![
|
||||
backend.buffer = Buffer::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
@@ -496,19 +510,19 @@ mod tests {
|
||||
assert_eq!(backend.get_cursor().unwrap(), (4, 4));
|
||||
|
||||
// As such the buffer should remain unchanged
|
||||
backend.assert_buffer(&Buffer::with_lines(vec![
|
||||
backend.assert_buffer_lines([
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
"dddddddddd",
|
||||
"eeeeeeeeee",
|
||||
]));
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_lines_at_last_line() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines(vec![
|
||||
backend.buffer = Buffer::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
@@ -522,7 +536,7 @@ mod tests {
|
||||
|
||||
backend.append_lines(1).unwrap();
|
||||
|
||||
backend.buffer = Buffer::with_lines(vec![
|
||||
backend.buffer = Buffer::with_lines([
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
"dddddddddd",
|
||||
@@ -538,7 +552,7 @@ mod tests {
|
||||
#[test]
|
||||
fn append_multiple_lines_not_at_last_line() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines(vec![
|
||||
backend.buffer = Buffer::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
@@ -555,19 +569,19 @@ mod tests {
|
||||
assert_eq!(backend.get_cursor().unwrap(), (1, 4));
|
||||
|
||||
// As such the buffer should remain unchanged
|
||||
backend.assert_buffer(&Buffer::with_lines(vec![
|
||||
backend.assert_buffer_lines([
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
"dddddddddd",
|
||||
"eeeeeeeeee",
|
||||
]));
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_multiple_lines_past_last_line() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines(vec![
|
||||
backend.buffer = Buffer::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
@@ -580,19 +594,19 @@ mod tests {
|
||||
backend.append_lines(3).unwrap();
|
||||
assert_eq!(backend.get_cursor().unwrap(), (1, 4));
|
||||
|
||||
backend.assert_buffer(&Buffer::with_lines(vec![
|
||||
backend.assert_buffer_lines([
|
||||
"cccccccccc",
|
||||
"dddddddddd",
|
||||
"eeeeeeeeee",
|
||||
" ",
|
||||
" ",
|
||||
]));
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_multiple_lines_where_cursor_at_end_appends_height_lines() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines(vec![
|
||||
backend.buffer = Buffer::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
@@ -605,19 +619,19 @@ mod tests {
|
||||
backend.append_lines(5).unwrap();
|
||||
assert_eq!(backend.get_cursor().unwrap(), (1, 4));
|
||||
|
||||
backend.assert_buffer(&Buffer::with_lines(vec![
|
||||
backend.assert_buffer_lines([
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
]));
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_multiple_lines_where_cursor_appends_height_lines() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines(vec![
|
||||
backend.buffer = Buffer::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
@@ -630,13 +644,13 @@ mod tests {
|
||||
backend.append_lines(5).unwrap();
|
||||
assert_eq!(backend.get_cursor().unwrap(), (1, 4));
|
||||
|
||||
backend.assert_buffer(&Buffer::with_lines(vec![
|
||||
backend.assert_buffer_lines([
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
"dddddddddd",
|
||||
"eeeeeeeeee",
|
||||
" ",
|
||||
]));
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,57 +1,44 @@
|
||||
/// Assert that two buffers are equal by comparing their areas and content.
|
||||
///
|
||||
/// On panic, displays the areas or the content and a diff of the contents.
|
||||
/// # Panics
|
||||
/// When the buffers differ this method panics and displays the differences similar to
|
||||
/// `assert_eq!()`.
|
||||
#[deprecated = "use assert_eq!(&actual, &expected)"]
|
||||
#[macro_export]
|
||||
macro_rules! assert_buffer_eq {
|
||||
($actual_expr:expr, $expected_expr:expr) => {
|
||||
match (&$actual_expr, &$expected_expr) {
|
||||
(actual, expected) => {
|
||||
if actual.area != expected.area {
|
||||
panic!(
|
||||
indoc::indoc!(
|
||||
"
|
||||
buffer areas not equal
|
||||
expected: {:?}
|
||||
actual: {:?}"
|
||||
),
|
||||
expected, actual
|
||||
);
|
||||
}
|
||||
let diff = expected.diff(&actual);
|
||||
if !diff.is_empty() {
|
||||
let nice_diff = diff
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, (x, y, cell))| {
|
||||
let expected_cell = expected.get(*x, *y);
|
||||
indoc::formatdoc! {"
|
||||
{i}: at ({x}, {y})
|
||||
expected: {expected_cell:?}
|
||||
actual: {cell:?}
|
||||
"}
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n");
|
||||
panic!(
|
||||
indoc::indoc!(
|
||||
"
|
||||
buffer contents not equal
|
||||
expected: {:?}
|
||||
actual: {:?}
|
||||
diff:
|
||||
{}"
|
||||
),
|
||||
expected, actual, nice_diff
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
actual.area == expected.area,
|
||||
"buffer areas not equal\nexpected: {expected:?}\nactual: {actual:?}",
|
||||
);
|
||||
let nice_diff = expected
|
||||
.diff(actual)
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, (x, y, cell))| {
|
||||
let expected_cell = expected.get(x, y);
|
||||
format!("{i}: at ({x}, {y})\n expected: {expected_cell:?}\n actual: {cell:?}")
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n");
|
||||
assert!(
|
||||
nice_diff.is_empty(),
|
||||
"buffer contents not equal\nexpected: {expected:?}\nactual: {actual:?}\ndiff:\n{nice_diff}",
|
||||
);
|
||||
// shouldn't get here, but this guards against future behavior
|
||||
// that changes equality but not area or content
|
||||
assert_eq!(actual, expected, "buffers not equal");
|
||||
assert_eq!(
|
||||
actual, expected,
|
||||
"buffers are not equal in an unexpected way. Please open an issue about this."
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::prelude::*;
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
use std::{
|
||||
cmp::min,
|
||||
fmt::{Debug, Formatter, Result},
|
||||
};
|
||||
use std::fmt;
|
||||
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
@@ -55,22 +52,21 @@ pub struct Buffer {
|
||||
|
||||
impl Buffer {
|
||||
/// Returns a Buffer with all cells set to the default one
|
||||
#[must_use]
|
||||
pub fn empty(area: Rect) -> Self {
|
||||
let cell = Cell::default();
|
||||
Self::filled(area, &cell)
|
||||
Self::filled(area, Cell::EMPTY)
|
||||
}
|
||||
|
||||
/// Returns a Buffer with all cells initialized with the attributes of the given Cell
|
||||
pub fn filled(area: Rect, cell: &Cell) -> Self {
|
||||
#[must_use]
|
||||
pub fn filled(area: Rect, cell: Cell) -> Self {
|
||||
let size = area.area() as usize;
|
||||
let mut content = Vec::with_capacity(size);
|
||||
for _ in 0..size {
|
||||
content.push(cell.clone());
|
||||
}
|
||||
let content = vec![cell; size];
|
||||
Self { area, content }
|
||||
}
|
||||
|
||||
/// Returns a Buffer containing the given lines
|
||||
#[must_use]
|
||||
pub fn with_lines<'a, Iter>(lines: Iter) -> Self
|
||||
where
|
||||
Iter: IntoIterator,
|
||||
@@ -97,12 +93,14 @@ impl Buffer {
|
||||
}
|
||||
|
||||
/// Returns a reference to Cell at the given coordinates
|
||||
#[track_caller]
|
||||
pub fn get(&self, x: u16, y: u16) -> &Cell {
|
||||
let i = self.index_of(x, y);
|
||||
&self.content[i]
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to Cell at the given coordinates
|
||||
#[track_caller]
|
||||
pub fn get_mut(&mut self, x: u16, y: u16) -> &mut Cell {
|
||||
let i = self.index_of(x, y);
|
||||
&mut self.content[i]
|
||||
@@ -134,6 +132,7 @@ impl Buffer {
|
||||
/// // starts at (200, 100).
|
||||
/// buffer.index_of(0, 0); // Panics
|
||||
/// ```
|
||||
#[track_caller]
|
||||
pub fn index_of(&self, x: u16, y: u16) -> usize {
|
||||
debug_assert!(
|
||||
x >= self.area.left()
|
||||
@@ -184,65 +183,56 @@ impl Buffer {
|
||||
}
|
||||
|
||||
/// Print a string, starting at the position (x, y)
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
pub fn set_string<T, S>(&mut self, x: u16, y: u16, string: T, style: S)
|
||||
where
|
||||
T: AsRef<str>,
|
||||
S: Into<Style>,
|
||||
{
|
||||
self.set_stringn(x, y, string, usize::MAX, style.into());
|
||||
self.set_stringn(x, y, string, usize::MAX, style);
|
||||
}
|
||||
|
||||
/// Print at most the first n characters of a string if enough space is available
|
||||
/// until the end of the line
|
||||
/// until the end of the line.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
/// Use [`Buffer::set_string`] when the maximum amount of characters can be printed.
|
||||
pub fn set_stringn<T, S>(
|
||||
&mut self,
|
||||
x: u16,
|
||||
mut x: u16,
|
||||
y: u16,
|
||||
string: T,
|
||||
width: usize,
|
||||
max_width: usize,
|
||||
style: S,
|
||||
) -> (u16, u16)
|
||||
where
|
||||
T: AsRef<str>,
|
||||
S: Into<Style>,
|
||||
{
|
||||
let max_width = max_width.try_into().unwrap_or(u16::MAX);
|
||||
let mut remaining_width = self.area.right().saturating_sub(x).min(max_width);
|
||||
let graphemes = UnicodeSegmentation::graphemes(string.as_ref(), true)
|
||||
.map(|symbol| (symbol, symbol.width() as u16))
|
||||
.filter(|(_symbol, width)| *width > 0)
|
||||
.map_while(|(symbol, width)| {
|
||||
remaining_width = remaining_width.checked_sub(width)?;
|
||||
Some((symbol, width))
|
||||
});
|
||||
let style = style.into();
|
||||
let mut index = self.index_of(x, y);
|
||||
let mut x_offset = x as usize;
|
||||
let graphemes = UnicodeSegmentation::graphemes(string.as_ref(), true);
|
||||
let max_offset = min(self.area.right() as usize, width.saturating_add(x as usize));
|
||||
for s in graphemes {
|
||||
let width = s.width();
|
||||
if width == 0 {
|
||||
continue;
|
||||
}
|
||||
// `x_offset + width > max_offset` could be integer overflow on 32-bit machines if we
|
||||
// change dimensions to usize or u32 and someone resizes the terminal to 1x2^32.
|
||||
if width > max_offset.saturating_sub(x_offset) {
|
||||
break;
|
||||
}
|
||||
|
||||
self.content[index].set_symbol(s);
|
||||
self.content[index].set_style(style);
|
||||
for (symbol, width) in graphemes {
|
||||
self.get_mut(x, y).set_symbol(symbol).set_style(style);
|
||||
let next_symbol = x + width;
|
||||
x += 1;
|
||||
// Reset following cells if multi-width (they would be hidden by the grapheme),
|
||||
for i in index + 1..index + width {
|
||||
self.content[i].reset();
|
||||
while x < next_symbol {
|
||||
self.get_mut(x, y).reset();
|
||||
x += 1;
|
||||
}
|
||||
index += width;
|
||||
x_offset += width;
|
||||
}
|
||||
(x_offset as u16, y)
|
||||
(x, y)
|
||||
}
|
||||
|
||||
/// Print a line, starting at the position (x, y)
|
||||
pub fn set_line(&mut self, x: u16, y: u16, line: &Line<'_>, width: u16) -> (u16, u16) {
|
||||
let mut remaining_width = width;
|
||||
pub fn set_line(&mut self, x: u16, y: u16, line: &Line<'_>, max_width: u16) -> (u16, u16) {
|
||||
let mut remaining_width = max_width;
|
||||
let mut x = x;
|
||||
for span in line {
|
||||
if remaining_width == 0 {
|
||||
@@ -263,8 +253,8 @@ impl Buffer {
|
||||
}
|
||||
|
||||
/// Print a span, starting at the position (x, y)
|
||||
pub fn set_span(&mut self, x: u16, y: u16, span: &Span<'_>, width: u16) -> (u16, u16) {
|
||||
self.set_stringn(x, y, span.content.as_ref(), width as usize, span.style)
|
||||
pub fn set_span(&mut self, x: u16, y: u16, span: &Span<'_>, max_width: u16) -> (u16, u16) {
|
||||
self.set_stringn(x, y, &span.content, max_width as usize, span.style)
|
||||
}
|
||||
|
||||
/// Set the style of all cells in the given area.
|
||||
@@ -288,23 +278,22 @@ impl Buffer {
|
||||
if self.content.len() > length {
|
||||
self.content.truncate(length);
|
||||
} else {
|
||||
self.content.resize(length, Cell::default());
|
||||
self.content.resize(length, Cell::EMPTY);
|
||||
}
|
||||
self.area = area;
|
||||
}
|
||||
|
||||
/// Reset all cells in the buffer
|
||||
pub fn reset(&mut self) {
|
||||
for c in &mut self.content {
|
||||
c.reset();
|
||||
for cell in &mut self.content {
|
||||
cell.reset();
|
||||
}
|
||||
}
|
||||
|
||||
/// Merge an other buffer into this one
|
||||
pub fn merge(&mut self, other: &Self) {
|
||||
let area = self.area.union(other.area);
|
||||
let cell = Cell::default();
|
||||
self.content.resize(area.area() as usize, cell.clone());
|
||||
self.content.resize(area.area() as usize, Cell::EMPTY);
|
||||
|
||||
// Move original content to the appropriate space
|
||||
let size = self.area.area() as usize;
|
||||
@@ -314,7 +303,7 @@ impl Buffer {
|
||||
let k = ((y - area.y) * area.width + x - area.x) as usize;
|
||||
if i != k {
|
||||
self.content[k] = self.content[i].clone();
|
||||
self.content[i] = cell.clone();
|
||||
self.content[i].reset();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,7 +372,7 @@ impl Buffer {
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Buffer {
|
||||
impl fmt::Debug for Buffer {
|
||||
/// Writes a debug representation of the buffer to the given formatter.
|
||||
///
|
||||
/// The format is like a pretty printed struct, with the following fields:
|
||||
@@ -391,11 +380,14 @@ impl Debug for Buffer {
|
||||
/// * `content`: displayed as a list of strings representing the content of the buffer
|
||||
/// * `styles`: displayed as a list of: `{ x: 1, y: 2, fg: Color::Red, bg: Color::Blue,
|
||||
/// modifier: Modifier::BOLD }` only showing a value when there is a change in style.
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
|
||||
f.write_fmt(format_args!(
|
||||
"Buffer {{\n area: {:?},\n content: [\n",
|
||||
&self.area
|
||||
))?;
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_fmt(format_args!("Buffer {{\n area: {:?}", &self.area))?;
|
||||
|
||||
if self.area.is_empty() {
|
||||
return f.write_str("\n}");
|
||||
}
|
||||
|
||||
f.write_str(",\n content: [\n")?;
|
||||
let mut last_style = None;
|
||||
let mut styles = vec![];
|
||||
for (y, line) in self.content.chunks(self.area.width as usize).enumerate() {
|
||||
@@ -426,12 +418,13 @@ impl Debug for Buffer {
|
||||
}
|
||||
}
|
||||
}
|
||||
f.write_str("\",")?;
|
||||
if !overwritten.is_empty() {
|
||||
f.write_fmt(format_args!(
|
||||
"// hidden by multi-width symbols: {overwritten:?}"
|
||||
" // hidden by multi-width symbols: {overwritten:?}"
|
||||
))?;
|
||||
}
|
||||
f.write_str("\",\n")?;
|
||||
f.write_str("\n")?;
|
||||
}
|
||||
f.write_str(" ],\n styles: [\n")?;
|
||||
for s in styles {
|
||||
@@ -459,19 +452,42 @@ mod tests {
|
||||
use rstest::{fixture, rstest};
|
||||
|
||||
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_empty_buffer() {
|
||||
let buffer = Buffer::empty(Rect::ZERO);
|
||||
let result = format!("{buffer:?}");
|
||||
println!("{result}");
|
||||
let expected = "Buffer {\n area: Rect { x: 0, y: 0, width: 0, height: 0 }\n}";
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
#[cfg(feature = "underline-color")]
|
||||
#[test]
|
||||
fn debug_grapheme_override() {
|
||||
let buffer = Buffer::with_lines(["a🦀b"]);
|
||||
let result = format!("{buffer:?}");
|
||||
println!("{result}");
|
||||
let expected = indoc::indoc!(
|
||||
r#"
|
||||
Buffer {
|
||||
area: Rect { x: 0, y: 0, width: 4, height: 1 },
|
||||
content: [
|
||||
"a🦀b", // hidden by multi-width symbols: [(2, " ")]
|
||||
],
|
||||
styles: [
|
||||
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
]
|
||||
}"#
|
||||
);
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn debug() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 12, 2));
|
||||
buf.set_string(0, 0, "Hello World!", Style::default());
|
||||
buf.set_string(
|
||||
fn debug_some_example() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 12, 2));
|
||||
buffer.set_string(0, 0, "Hello World!", Style::default());
|
||||
buffer.set_string(
|
||||
0,
|
||||
1,
|
||||
"G'day World!",
|
||||
@@ -480,42 +496,40 @@ mod tests {
|
||||
.bg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
);
|
||||
let result = format!("{buffer:?}");
|
||||
println!("{result}");
|
||||
#[cfg(feature = "underline-color")]
|
||||
assert_eq!(
|
||||
format!("{buf:?}"),
|
||||
indoc::indoc!(
|
||||
"
|
||||
Buffer {
|
||||
area: Rect { x: 0, y: 0, width: 12, height: 2 },
|
||||
content: [
|
||||
\"Hello World!\",
|
||||
\"G'day World!\",
|
||||
],
|
||||
styles: [
|
||||
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 0, y: 1, fg: Green, bg: Yellow, underline: Reset, modifier: BOLD,
|
||||
]
|
||||
}"
|
||||
)
|
||||
let expected = indoc::indoc!(
|
||||
r#"
|
||||
Buffer {
|
||||
area: Rect { x: 0, y: 0, width: 12, height: 2 },
|
||||
content: [
|
||||
"Hello World!",
|
||||
"G'day World!",
|
||||
],
|
||||
styles: [
|
||||
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 0, y: 1, fg: Green, bg: Yellow, underline: Reset, modifier: BOLD,
|
||||
]
|
||||
}"#
|
||||
);
|
||||
#[cfg(not(feature = "underline-color"))]
|
||||
assert_eq!(
|
||||
format!("{buf:?}"),
|
||||
indoc::indoc!(
|
||||
"
|
||||
Buffer {
|
||||
area: Rect { x: 0, y: 0, width: 12, height: 2 },
|
||||
content: [
|
||||
\"Hello World!\",
|
||||
\"G'day World!\",
|
||||
],
|
||||
styles: [
|
||||
x: 0, y: 0, fg: Reset, bg: Reset, modifier: NONE,
|
||||
x: 0, y: 1, fg: Green, bg: Yellow, modifier: BOLD,
|
||||
]
|
||||
}"
|
||||
)
|
||||
let expected = indoc::indoc!(
|
||||
r#"
|
||||
Buffer {
|
||||
area: Rect { x: 0, y: 0, width: 12, height: 2 },
|
||||
content: [
|
||||
"Hello World!",
|
||||
"G'day World!",
|
||||
],
|
||||
styles: [
|
||||
x: 0, y: 0, fg: Reset, bg: Reset, modifier: NONE,
|
||||
x: 0, y: 1, fg: Green, bg: Yellow, modifier: BOLD,
|
||||
]
|
||||
}"#
|
||||
);
|
||||
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -559,27 +573,27 @@ mod tests {
|
||||
|
||||
// Zero-width
|
||||
buffer.set_stringn(0, 0, "aaa", 0, Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" "]));
|
||||
assert_eq!(buffer, Buffer::with_lines([" "]));
|
||||
|
||||
buffer.set_string(0, 0, "aaa", Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["aaa "]));
|
||||
assert_eq!(buffer, Buffer::with_lines(["aaa "]));
|
||||
|
||||
// Width limit:
|
||||
buffer.set_stringn(0, 0, "bbbbbbbbbbbbbb", 4, Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["bbbb "]));
|
||||
assert_eq!(buffer, Buffer::with_lines(["bbbb "]));
|
||||
|
||||
buffer.set_string(0, 0, "12345", Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["12345"]));
|
||||
assert_eq!(buffer, Buffer::with_lines(["12345"]));
|
||||
|
||||
// Width truncation:
|
||||
buffer.set_string(0, 0, "123456", Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["12345"]));
|
||||
assert_eq!(buffer, Buffer::with_lines(["12345"]));
|
||||
|
||||
// multi-line
|
||||
buffer = Buffer::empty(Rect::new(0, 0, 5, 2));
|
||||
buffer.set_string(0, 0, "12345", Style::default());
|
||||
buffer.set_string(0, 1, "67890", Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["12345", "67890"]));
|
||||
assert_eq!(buffer, Buffer::with_lines(["12345", "67890"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -590,7 +604,7 @@ mod tests {
|
||||
// multi-width overwrite
|
||||
buffer.set_string(0, 0, "aaaaa", Style::default());
|
||||
buffer.set_string(0, 0, "称号", Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["称号a"]));
|
||||
assert_eq!(buffer, Buffer::with_lines(["称号a"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -601,12 +615,12 @@ mod tests {
|
||||
// Leading grapheme with zero width
|
||||
let s = "\u{1}a";
|
||||
buffer.set_stringn(0, 0, s, 1, Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["a"]));
|
||||
assert_eq!(buffer, Buffer::with_lines(["a"]));
|
||||
|
||||
// Trailing grapheme with zero with
|
||||
let s = "a\u{1}";
|
||||
buffer.set_stringn(0, 0, s, 1, Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["a"]));
|
||||
assert_eq!(buffer, Buffer::with_lines(["a"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -614,11 +628,11 @@ mod tests {
|
||||
let area = Rect::new(0, 0, 5, 1);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
buffer.set_string(0, 0, "コン", Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["コン "]));
|
||||
assert_eq!(buffer, Buffer::with_lines(["コン "]));
|
||||
|
||||
// Only 1 space left.
|
||||
buffer.set_string(0, 0, "コンピ", Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["コン "]));
|
||||
assert_eq!(buffer, Buffer::with_lines(["コン "]));
|
||||
}
|
||||
|
||||
#[fixture]
|
||||
@@ -643,7 +657,7 @@ mod tests {
|
||||
// set_line
|
||||
let mut expected_buffer = Buffer::empty(small_one_line_buffer.area);
|
||||
expected_buffer.set_string(0, 0, expected, Style::default());
|
||||
assert_buffer_eq!(small_one_line_buffer, expected_buffer);
|
||||
assert_eq!(small_one_line_buffer, expected_buffer);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
@@ -684,28 +698,39 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn set_style() {
|
||||
let mut buffer = Buffer::with_lines(vec!["aaaaa", "bbbbb", "ccccc"]);
|
||||
let mut buffer = Buffer::with_lines(["aaaaa", "bbbbb", "ccccc"]);
|
||||
buffer.set_style(Rect::new(0, 1, 5, 1), Style::new().red());
|
||||
assert_buffer_eq!(
|
||||
buffer,
|
||||
Buffer::with_lines(vec!["aaaaa".into(), "bbbbb".red(), "ccccc".into(),])
|
||||
);
|
||||
#[rustfmt::skip]
|
||||
let expected = Buffer::with_lines([
|
||||
"aaaaa".into(),
|
||||
"bbbbb".red(),
|
||||
"ccccc".into(),
|
||||
]);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_style_does_not_panic_when_out_of_area() {
|
||||
let mut buffer = Buffer::with_lines(vec!["aaaaa", "bbbbb", "ccccc"]);
|
||||
let mut buffer = Buffer::with_lines(["aaaaa", "bbbbb", "ccccc"]);
|
||||
buffer.set_style(Rect::new(0, 1, 10, 3), Style::new().red());
|
||||
assert_buffer_eq!(
|
||||
buffer,
|
||||
Buffer::with_lines(vec!["aaaaa".into(), "bbbbb".red(), "ccccc".red(),])
|
||||
);
|
||||
#[rustfmt::skip]
|
||||
let expected = Buffer::with_lines([
|
||||
"aaaaa".into(),
|
||||
"bbbbb".red(),
|
||||
"ccccc".red(),
|
||||
]);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_lines() {
|
||||
let buffer =
|
||||
Buffer::with_lines(vec!["┌────────┐", "│コンピュ│", "│ーa 上で│", "└────────┘"]);
|
||||
#[rustfmt::skip]
|
||||
let buffer = Buffer::with_lines([
|
||||
"┌────────┐",
|
||||
"│コンピュ│",
|
||||
"│ーa 上で│",
|
||||
"└────────┘",
|
||||
]);
|
||||
assert_eq!(buffer.area.x, 0);
|
||||
assert_eq!(buffer.area.y, 0);
|
||||
assert_eq!(buffer.area.width, 10);
|
||||
@@ -718,14 +743,14 @@ mod tests {
|
||||
let prev = Buffer::empty(area);
|
||||
let next = Buffer::empty(area);
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(diff, vec![]);
|
||||
assert_eq!(diff, []);
|
||||
}
|
||||
|
||||
#[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 next = Buffer::filled(area, Cell::new("a"));
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(diff.len(), 40 * 40);
|
||||
}
|
||||
@@ -733,22 +758,22 @@ mod tests {
|
||||
#[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 prev = Buffer::filled(area, Cell::new("a"));
|
||||
let next = Buffer::filled(area, Cell::new("a"));
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(diff, vec![]);
|
||||
assert_eq!(diff, []);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_single_width() {
|
||||
let prev = Buffer::with_lines(vec![
|
||||
let prev = Buffer::with_lines([
|
||||
" ",
|
||||
"┌Title─┐ ",
|
||||
"│ │ ",
|
||||
"│ │ ",
|
||||
"└──────┘ ",
|
||||
]);
|
||||
let next = Buffer::with_lines(vec![
|
||||
let next = Buffer::with_lines([
|
||||
" ",
|
||||
"┌TITLE─┐ ",
|
||||
"│ │ ",
|
||||
@@ -758,116 +783,84 @@ mod tests {
|
||||
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")),
|
||||
[
|
||||
(2, 1, &Cell::new("I")),
|
||||
(3, 1, &Cell::new("T")),
|
||||
(4, 1, &Cell::new("L")),
|
||||
(5, 1, &Cell::new("E")),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[rustfmt::skip]
|
||||
fn diff_multi_width() {
|
||||
let prev = Buffer::with_lines(vec![
|
||||
#[rustfmt::skip]
|
||||
let prev = Buffer::with_lines([
|
||||
"┌Title─┐ ",
|
||||
"└──────┘ ",
|
||||
]);
|
||||
let next = Buffer::with_lines(vec![
|
||||
#[rustfmt::skip]
|
||||
let next = Buffer::with_lines([
|
||||
"┌称号──┐ ",
|
||||
"└──────┘ ",
|
||||
]);
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(
|
||||
diff,
|
||||
vec![
|
||||
(1, 0, &cell("称")),
|
||||
[
|
||||
(1, 0, &Cell::new("称")),
|
||||
// Skipped "i"
|
||||
(3, 0, &cell("号")),
|
||||
(3, 0, &Cell::new("号")),
|
||||
// Skipped "l"
|
||||
(5, 0, &cell("─")),
|
||||
(5, 0, &Cell::new("─")),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_multi_width_offset() {
|
||||
let prev = Buffer::with_lines(vec!["┌称号──┐"]);
|
||||
let next = Buffer::with_lines(vec!["┌─称号─┐"]);
|
||||
let prev = Buffer::with_lines(["┌称号──┐"]);
|
||||
let next = Buffer::with_lines(["┌─称号─┐"]);
|
||||
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(
|
||||
diff,
|
||||
vec![(1, 0, &cell("─")), (2, 0, &cell("称")), (4, 0, &cell("号")),]
|
||||
[
|
||||
(1, 0, &Cell::new("─")),
|
||||
(2, 0, &Cell::new("称")),
|
||||
(4, 0, &Cell::new("号")),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_skip() {
|
||||
let prev = Buffer::with_lines(vec!["123"]);
|
||||
let mut next = Buffer::with_lines(vec!["456"]);
|
||||
let prev = Buffer::with_lines(["123"]);
|
||||
let mut next = Buffer::with_lines(["456"]);
|
||||
for i in 1..3 {
|
||||
next.content[i].set_skip(true);
|
||||
}
|
||||
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(diff, vec![(0, 0, &cell("4"))],);
|
||||
assert_eq!(diff, [(0, 0, &Cell::new("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"),
|
||||
);
|
||||
#[rstest]
|
||||
#[case(Rect::new(0, 0, 2, 2), Rect::new(0, 2, 2, 2), ["11", "11", "22", "22"])]
|
||||
#[case(Rect::new(2, 2, 2, 2), Rect::new(0, 0, 2, 2), ["22 ", "22 ", " 11", " 11"])]
|
||||
fn merge<'line, Lines>(#[case] one: Rect, #[case] two: Rect, #[case] expected: Lines)
|
||||
where
|
||||
Lines: IntoIterator,
|
||||
Lines::Item: Into<Line<'line>>,
|
||||
{
|
||||
let mut one = Buffer::filled(one, Cell::new("1"));
|
||||
let two = Buffer::filled(two, Cell::new("2"));
|
||||
one.merge(&two);
|
||||
assert_buffer_eq!(one, Buffer::with_lines(vec!["11", "11", "22", "22"]));
|
||||
assert_eq!(one, Buffer::with_lines(expected));
|
||||
}
|
||||
|
||||
#[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() {
|
||||
fn merge_with_offset() {
|
||||
let mut one = Buffer::filled(
|
||||
Rect {
|
||||
x: 3,
|
||||
@@ -875,7 +868,7 @@ mod tests {
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("1"),
|
||||
Cell::new("1"),
|
||||
);
|
||||
let two = Buffer::filled(
|
||||
Rect {
|
||||
@@ -884,67 +877,48 @@ mod tests {
|
||||
width: 3,
|
||||
height: 4,
|
||||
},
|
||||
Cell::default().set_symbol("2"),
|
||||
Cell::new("2"),
|
||||
);
|
||||
one.merge(&two);
|
||||
let mut merged = Buffer::with_lines(vec!["222 ", "222 ", "2221", "2221"]);
|
||||
merged.area = Rect {
|
||||
let mut expected = Buffer::with_lines(["222 ", "222 ", "2221", "2221"]);
|
||||
expected.area = Rect {
|
||||
x: 1,
|
||||
y: 1,
|
||||
width: 4,
|
||||
height: 4,
|
||||
};
|
||||
assert_buffer_eq!(one, merged);
|
||||
assert_eq!(one, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_skip() {
|
||||
let mut one = Buffer::filled(
|
||||
Rect {
|
||||
#[rstest]
|
||||
#[case(false, true, [false, false, true, true, true, true])]
|
||||
#[case(true, false, [true, true, false, false, false, false])]
|
||||
fn merge_skip(#[case] skip_one: bool, #[case] skip_two: bool, #[case] expected: [bool; 6]) {
|
||||
let mut one = {
|
||||
let area = Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("1"),
|
||||
);
|
||||
let two = Buffer::filled(
|
||||
Rect {
|
||||
};
|
||||
let mut cell = Cell::new("1");
|
||||
cell.skip = skip_one;
|
||||
Buffer::filled(area, cell)
|
||||
};
|
||||
let two = {
|
||||
let area = Rect {
|
||||
x: 0,
|
||||
y: 1,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("2").set_skip(true),
|
||||
);
|
||||
};
|
||||
let mut cell = Cell::new("2");
|
||||
cell.skip = skip_two;
|
||||
Buffer::filled(area, cell)
|
||||
};
|
||||
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]);
|
||||
let skipped = one.content().iter().map(|c| c.skip).collect::<Vec<_>>();
|
||||
assert_eq!(skipped, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -953,6 +927,6 @@ mod tests {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 3, 2));
|
||||
buf.set_string(0, 0, "foo", Style::new().red());
|
||||
buf.set_string(0, 1, "bar", Style::new().blue());
|
||||
assert_eq!(buf, Buffer::with_lines(vec!["foo".red(), "bar".blue()]));
|
||||
assert_eq!(buf, Buffer::with_lines(["foo".red(), "bar".blue()]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use std::fmt::Debug;
|
||||
|
||||
use compact_str::CompactString;
|
||||
|
||||
use crate::prelude::*;
|
||||
@@ -36,7 +34,29 @@ pub struct Cell {
|
||||
}
|
||||
|
||||
impl Cell {
|
||||
/// An empty `Cell`
|
||||
pub const EMPTY: Self = Self::new(" ");
|
||||
|
||||
/// Creates a new `Cell` with the given symbol.
|
||||
///
|
||||
/// This works at compile time and puts the symbol onto the stack. Fails to build when the
|
||||
/// symbol doesnt fit onto the stack and requires to be placed on the heap. Use
|
||||
/// `Self::default().set_symbol()` in that case. See [`CompactString::new_inline`] for more
|
||||
/// details on this.
|
||||
pub const fn new(symbol: &str) -> Self {
|
||||
Self {
|
||||
symbol: CompactString::new_inline(symbol),
|
||||
fg: Color::Reset,
|
||||
bg: Color::Reset,
|
||||
#[cfg(feature = "underline-color")]
|
||||
underline_color: Color::Reset,
|
||||
modifier: Modifier::empty(),
|
||||
skip: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the symbol of the cell.
|
||||
#[must_use]
|
||||
pub fn symbol(&self) -> &str {
|
||||
self.symbol.as_str()
|
||||
}
|
||||
@@ -88,19 +108,16 @@ impl Cell {
|
||||
}
|
||||
|
||||
/// 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);
|
||||
#[must_use]
|
||||
pub const fn style(&self) -> Style {
|
||||
Style {
|
||||
fg: Some(self.fg),
|
||||
bg: Some(self.bg),
|
||||
#[cfg(feature = "underline-color")]
|
||||
underline_color: Some(self.underline_color),
|
||||
add_modifier: self.modifier,
|
||||
sub_modifier: Modifier::empty(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the cell to be skipped when copying (diffing) the buffer to the screen.
|
||||
@@ -112,7 +129,7 @@ impl Cell {
|
||||
self
|
||||
}
|
||||
|
||||
/// Resets the cell to the default state.
|
||||
/// Resets the cell to the empty state.
|
||||
pub fn reset(&mut self) {
|
||||
self.symbol = CompactString::new_inline(" ");
|
||||
self.fg = Color::Reset;
|
||||
@@ -128,15 +145,7 @@ impl Cell {
|
||||
|
||||
impl Default for Cell {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
symbol: CompactString::new_inline(" "),
|
||||
fg: Color::Reset,
|
||||
bg: Color::Reset,
|
||||
#[cfg(feature = "underline-color")]
|
||||
underline_color: Color::Reset,
|
||||
modifier: Modifier::empty(),
|
||||
skip: false,
|
||||
}
|
||||
Self::EMPTY
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,11 +155,15 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn symbol_field() {
|
||||
let mut cell = Cell::default();
|
||||
let mut cell = Cell::EMPTY;
|
||||
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(), "👨👩👧👦");
|
||||
|
||||
// above Cell::EMPTY is put into a mutable variable and is changed then.
|
||||
// While this looks like it might change the constant, it actually doesnt:
|
||||
assert_eq!(Cell::EMPTY.symbol(), " ");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
mod alignment;
|
||||
mod constraint;
|
||||
mod corner;
|
||||
mod direction;
|
||||
mod flex;
|
||||
#[allow(clippy::module_inception)]
|
||||
@@ -14,7 +13,6 @@ mod size;
|
||||
|
||||
pub use alignment::Alignment;
|
||||
pub use constraint::Constraint;
|
||||
pub use corner::Corner;
|
||||
pub use direction::Direction;
|
||||
pub use flex::Flex;
|
||||
pub use layout::Layout;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::fmt::{self, Display};
|
||||
use std::fmt;
|
||||
|
||||
use itertools::Itertools;
|
||||
use strum::EnumIs;
|
||||
@@ -362,7 +362,7 @@ impl Default for Constraint {
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Constraint {
|
||||
impl fmt::Display for Constraint {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Percentage(p) => write!(f, "Percentage({p})"),
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub enum Corner {
|
||||
#[default]
|
||||
TopLeft,
|
||||
TopRight,
|
||||
BottomRight,
|
||||
BottomLeft,
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use strum::ParseError;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn corner_to_string() {
|
||||
assert_eq!(Corner::BottomLeft.to_string(), "BottomLeft");
|
||||
assert_eq!(Corner::BottomRight.to_string(), "BottomRight");
|
||||
assert_eq!(Corner::TopLeft.to_string(), "TopLeft");
|
||||
assert_eq!(Corner::TopRight.to_string(), "TopRight");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corner_from_str() {
|
||||
assert_eq!("BottomLeft".parse::<Corner>(), Ok(Corner::BottomLeft));
|
||||
assert_eq!("BottomRight".parse::<Corner>(), Ok(Corner::BottomRight));
|
||||
assert_eq!("TopLeft".parse::<Corner>(), Ok(Corner::TopLeft));
|
||||
assert_eq!("TopRight".parse::<Corner>(), Ok(Corner::TopRight));
|
||||
assert_eq!("".parse::<Corner>(), Err(ParseError::VariantNotFound));
|
||||
}
|
||||
}
|
||||
@@ -448,7 +448,7 @@ impl Layout {
|
||||
/// # }
|
||||
pub fn areas<const N: usize>(&self, area: Rect) -> [Rect; N] {
|
||||
let (areas, _) = self.split_with_spacers(area);
|
||||
areas.to_vec().try_into().expect("invalid number of rects")
|
||||
areas.as_ref().try_into().expect("invalid number of rects")
|
||||
}
|
||||
|
||||
/// Split the rect into a number of sub-rects according to the given [`Layout`] and return just
|
||||
@@ -482,7 +482,7 @@ impl Layout {
|
||||
pub fn spacers<const N: usize>(&self, area: Rect) -> [Rect; N] {
|
||||
let (_, spacers) = self.split_with_spacers(area);
|
||||
spacers
|
||||
.to_vec()
|
||||
.as_ref()
|
||||
.try_into()
|
||||
.expect("invalid number of rects")
|
||||
}
|
||||
@@ -608,7 +608,7 @@ impl Layout {
|
||||
// This is equivalent to storing the solver in `Layout` and calling `solver.reset()` here.
|
||||
let mut solver = Solver::new();
|
||||
|
||||
let inner_area = area.inner(&self.margin);
|
||||
let inner_area = area.inner(self.margin);
|
||||
let (area_start, area_end) = match self.direction {
|
||||
Direction::Horizontal => (
|
||||
f64::from(inner_area.x) * FLOAT_PRECISION_MULTIPLIER,
|
||||
@@ -1334,7 +1334,6 @@ mod tests {
|
||||
use rstest::rstest;
|
||||
|
||||
use crate::{
|
||||
assert_buffer_eq,
|
||||
layout::flex::Flex,
|
||||
prelude::{Constraint::*, *},
|
||||
widgets::Paragraph,
|
||||
@@ -1361,8 +1360,7 @@ mod tests {
|
||||
let s = c.to_string().repeat(area.width as usize);
|
||||
Paragraph::new(s).render(layout[i], &mut buffer);
|
||||
}
|
||||
let expected = Buffer::with_lines(vec![expected]);
|
||||
assert_buffer_eq!(buffer, expected);
|
||||
assert_eq!(buffer, Buffer::with_lines([expected]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::fmt::{self, Display};
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub struct Margin {
|
||||
@@ -15,7 +15,7 @@ impl Margin {
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Margin {
|
||||
impl fmt::Display for Margin {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}x{}", self.horizontal, self.vertical)
|
||||
}
|
||||
|
||||
@@ -47,6 +47,14 @@ impl fmt::Display for Rect {
|
||||
}
|
||||
|
||||
impl Rect {
|
||||
/// A zero sized Rect at position 0,0
|
||||
pub const ZERO: Self = Self {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
};
|
||||
|
||||
/// Creates a new `Rect`, with width and height limited to keep the area under max `u16`. If
|
||||
/// clipped, aspect ratio will be preserved.
|
||||
pub fn new(x: u16, y: u16, width: u16, height: u16) -> Self {
|
||||
@@ -112,12 +120,12 @@ impl Rect {
|
||||
///
|
||||
/// If the margin is larger than the `Rect`, the returned `Rect` will have no area.
|
||||
#[must_use = "method returns the modified value"]
|
||||
pub fn inner(self, margin: &Margin) -> Self {
|
||||
pub const fn inner(self, margin: Margin) -> Self {
|
||||
let doubled_margin_horizontal = margin.horizontal.saturating_mul(2);
|
||||
let doubled_margin_vertical = margin.vertical.saturating_mul(2);
|
||||
|
||||
if self.width < doubled_margin_horizontal || self.height < doubled_margin_vertical {
|
||||
Self::default()
|
||||
Self::ZERO
|
||||
} else {
|
||||
Self {
|
||||
x: self.x.saturating_add(margin.horizontal),
|
||||
@@ -313,6 +321,18 @@ impl Rect {
|
||||
height: self.height,
|
||||
}
|
||||
}
|
||||
|
||||
/// indents the x value of the `Rect` by a given `offset`
|
||||
///
|
||||
/// This is pub(crate) for now as we need to stabilize the naming / design of this API.
|
||||
#[must_use]
|
||||
pub(crate) const fn indent_x(self, offset: u16) -> Self {
|
||||
Self {
|
||||
x: self.x.saturating_add(offset),
|
||||
width: self.width.saturating_sub(offset),
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(Position, Size)> for Rect {
|
||||
@@ -385,7 +405,7 @@ mod tests {
|
||||
#[test]
|
||||
fn inner() {
|
||||
assert_eq!(
|
||||
Rect::new(1, 2, 3, 4).inner(&Margin::new(1, 2)),
|
||||
Rect::new(1, 2, 3, 4).inner(Margin::new(1, 2)),
|
||||
Rect::new(2, 4, 1, 0)
|
||||
);
|
||||
}
|
||||
|
||||
78
src/lib.rs
78
src/lib.rs
@@ -20,10 +20,10 @@
|
||||
//!
|
||||
//! ## Installation
|
||||
//!
|
||||
//! Add `ratatui` and `crossterm` as dependencies to your cargo.toml:
|
||||
//! Add `ratatui` as a dependency to your cargo.toml:
|
||||
//!
|
||||
//! ```shell
|
||||
//! cargo add ratatui crossterm
|
||||
//! cargo add ratatui
|
||||
//! ```
|
||||
//!
|
||||
//! Ratatui uses [Crossterm] by default as it works on most platforms. See the [Installation]
|
||||
@@ -103,12 +103,17 @@
|
||||
//! ```rust,no_run
|
||||
//! use std::io::{self, stdout};
|
||||
//!
|
||||
//! use crossterm::{
|
||||
//! event::{self, Event, KeyCode},
|
||||
//! terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
//! ExecutableCommand,
|
||||
//! use ratatui::{
|
||||
//! crossterm::{
|
||||
//! event::{self, Event, KeyCode},
|
||||
//! terminal::{
|
||||
//! disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
|
||||
//! },
|
||||
//! ExecutableCommand,
|
||||
//! },
|
||||
//! prelude::*,
|
||||
//! widgets::*,
|
||||
//! };
|
||||
//! use ratatui::{prelude::*, widgets::*};
|
||||
//!
|
||||
//! fn main() -> io::Result<()> {
|
||||
//! enable_raw_mode()?;
|
||||
@@ -139,8 +144,7 @@
|
||||
//!
|
||||
//! fn ui(frame: &mut Frame) {
|
||||
//! frame.render_widget(
|
||||
//! Paragraph::new("Hello World!")
|
||||
//! .block(Block::default().title("Greeting").borders(Borders::ALL)),
|
||||
//! Paragraph::new("Hello World!").block(Block::bordered().title("Greeting")),
|
||||
//! frame.size(),
|
||||
//! );
|
||||
//! }
|
||||
@@ -184,14 +188,8 @@
|
||||
//! [Constraint::Percentage(50), Constraint::Percentage(50)],
|
||||
//! )
|
||||
//! .split(main_layout[1]);
|
||||
//! frame.render_widget(
|
||||
//! Block::default().borders(Borders::ALL).title("Left"),
|
||||
//! inner_layout[0],
|
||||
//! );
|
||||
//! frame.render_widget(
|
||||
//! Block::default().borders(Borders::ALL).title("Right"),
|
||||
//! inner_layout[1],
|
||||
//! );
|
||||
//! frame.render_widget(Block::bordered().title("Left"), inner_layout[0]);
|
||||
//! frame.render_widget(Block::bordered().title("Right"), inner_layout[1]);
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
@@ -262,22 +260,6 @@
|
||||
//! ![docsrs-styling]
|
||||
#![cfg_attr(feature = "document-features", doc = "\n## Features")]
|
||||
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
|
||||
#![cfg_attr(
|
||||
feature = "document-features",
|
||||
doc = "[`CrossTermBackend`]: backend::CrosstermBackend"
|
||||
)]
|
||||
#![cfg_attr(
|
||||
feature = "document-features",
|
||||
doc = "[`TermionBackend`]: backend::TermionBackend"
|
||||
)]
|
||||
#![cfg_attr(
|
||||
feature = "document-features",
|
||||
doc = "[`TermwizBackend`]: backend::TermwizBackend"
|
||||
)]
|
||||
#![cfg_attr(
|
||||
feature = "document-features",
|
||||
doc = "[`calendar`]: widgets::calendar::Monthly"
|
||||
)]
|
||||
//!
|
||||
//! [Ratatui Website]: https://ratatui.rs/
|
||||
//! [Installation]: https://ratatui.rs/installation/
|
||||
@@ -323,24 +305,20 @@
|
||||
//! [Termwiz]: https://crates.io/crates/termwiz
|
||||
//! [tui-rs]: https://crates.io/crates/tui
|
||||
//! [GitHub Sponsors]: https://github.com/sponsors/ratatui-org
|
||||
//! [Crate Badge]: https://img.shields.io/crates/v/ratatui?logo=rust&style=flat-square
|
||||
//! [License Badge]: https://img.shields.io/crates/l/ratatui?style=flat-square
|
||||
//! [CI Badge]:
|
||||
//! https://img.shields.io/github/actions/workflow/status/ratatui-org/ratatui/ci.yml?style=flat-square&logo=github
|
||||
//! [Crate Badge]: https://img.shields.io/crates/v/ratatui?logo=rust&style=flat-square&logoColor=E05D44&color=E05D44
|
||||
//! [License Badge]: https://img.shields.io/crates/l/ratatui?style=flat-square&color=1370D3
|
||||
//! [CI Badge]: https://img.shields.io/github/actions/workflow/status/ratatui-org/ratatui/ci.yml?style=flat-square&logo=github
|
||||
//! [CI Workflow]: https://github.com/ratatui-org/ratatui/actions/workflows/ci.yml
|
||||
//! [Codecov Badge]:
|
||||
//! https://img.shields.io/codecov/c/github/ratatui-org/ratatui?logo=codecov&style=flat-square&token=BAQ8SOKEST
|
||||
//! [Codecov Badge]: https://img.shields.io/codecov/c/github/ratatui-org/ratatui?logo=codecov&style=flat-square&token=BAQ8SOKEST&color=C43AC3&logoColor=C43AC3
|
||||
//! [Codecov]: https://app.codecov.io/gh/ratatui-org/ratatui
|
||||
//! [Deps.rs Badge]: https://deps.rs/repo/github/ratatui-org/ratatui/status.svg?style=flat-square
|
||||
//! [Deps.rs]: https://deps.rs/repo/github/ratatui-org/ratatui
|
||||
//! [Discord Badge]:
|
||||
//! https://img.shields.io/discord/1070692720437383208?label=discord&logo=discord&style=flat-square
|
||||
//! [Discord Badge]: https://img.shields.io/discord/1070692720437383208?label=discord&logo=discord&style=flat-square&color=1370D3&logoColor=1370D3
|
||||
//! [Discord Server]: https://discord.gg/pMCEU9hNEj
|
||||
//! [Docs Badge]: https://img.shields.io/docsrs/ratatui?logo=rust&style=flat-square
|
||||
//! [Matrix Badge]:
|
||||
//! https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix
|
||||
//! [Docs Badge]: https://img.shields.io/docsrs/ratatui?logo=rust&style=flat-square&logoColor=E05D44
|
||||
//! [Matrix Badge]: https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix&color=C43AC3
|
||||
//! [Matrix]: https://matrix.to/#/#ratatui:matrix.org
|
||||
//! [Sponsors Badge]: https://img.shields.io/github/sponsors/ratatui-org?logo=github&style=flat-square
|
||||
//! [Sponsors Badge]: https://img.shields.io/github/sponsors/ratatui-org?logo=github&style=flat-square&color=1370D3
|
||||
|
||||
// show the feature flags in the generated documentation
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
@@ -362,3 +340,13 @@ pub mod widgets;
|
||||
pub use self::terminal::{CompletedFrame, Frame, Terminal, TerminalOptions, Viewport};
|
||||
|
||||
pub mod prelude;
|
||||
|
||||
/// re-export the `crossterm` crate so that users don't have to add it as a dependency
|
||||
#[cfg(feature = "crossterm")]
|
||||
pub use crossterm;
|
||||
/// re-export the `termion` crate so that users don't have to add it as a dependency
|
||||
#[cfg(feature = "termion")]
|
||||
pub use termion;
|
||||
/// re-export the `termwiz` crate so that users don't have to add it as a dependency
|
||||
#[cfg(feature = "termwiz")]
|
||||
pub use termwiz;
|
||||
|
||||
@@ -27,7 +27,7 @@ pub(crate) use crate::widgets::{StatefulWidgetRef, WidgetRef};
|
||||
pub use crate::{
|
||||
backend::{self, Backend},
|
||||
buffer::{self, Buffer},
|
||||
layout::{self, Alignment, Constraint, Corner, Direction, Layout, Margin, Rect},
|
||||
layout::{self, Alignment, Constraint, Direction, Layout, Margin, Rect},
|
||||
style::{self, Color, Modifier, Style, Styled, Stylize},
|
||||
symbols::{self, Marker},
|
||||
terminal::{CompletedFrame, Frame, Terminal, TerminalOptions, Viewport},
|
||||
|
||||
312
src/style.rs
312
src/style.rs
@@ -68,14 +68,14 @@
|
||||
//! [`prelude`]: crate::prelude
|
||||
//! [`Span`]: crate::text::Span
|
||||
|
||||
use std::fmt::{self, Debug};
|
||||
use std::fmt;
|
||||
|
||||
use bitflags::bitflags;
|
||||
|
||||
mod color;
|
||||
mod stylize;
|
||||
|
||||
pub use color::Color;
|
||||
pub use color::{Color, ParseColorError};
|
||||
pub use stylize::{Styled, Stylize};
|
||||
pub mod palette;
|
||||
|
||||
@@ -119,7 +119,7 @@ impl fmt::Debug for Modifier {
|
||||
if self.is_empty() {
|
||||
return write!(f, "NONE");
|
||||
}
|
||||
fmt::Debug::fmt(&self.0, f)
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,7 +222,7 @@ impl fmt::Debug for Modifier {
|
||||
/// buffer.get(0, 0).style(),
|
||||
/// );
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Style {
|
||||
pub fg: Option<Color>,
|
||||
@@ -233,12 +233,6 @@ pub struct Style {
|
||||
pub sub_modifier: Modifier,
|
||||
}
|
||||
|
||||
impl Default for Style {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for Style {
|
||||
type Item = Self;
|
||||
|
||||
@@ -550,34 +544,30 @@ impl From<(Color, Color, Modifier, Modifier)> for Style {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rstest::rstest;
|
||||
|
||||
fn styles() -> Vec<Style> {
|
||||
vec![
|
||||
Style::default(),
|
||||
Style::default().fg(Color::Yellow),
|
||||
Style::default().bg(Color::Yellow),
|
||||
Style::default().add_modifier(Modifier::BOLD),
|
||||
Style::default().remove_modifier(Modifier::BOLD),
|
||||
Style::default().add_modifier(Modifier::ITALIC),
|
||||
Style::default().remove_modifier(Modifier::ITALIC),
|
||||
Style::default().add_modifier(Modifier::ITALIC | Modifier::BOLD),
|
||||
Style::default().remove_modifier(Modifier::ITALIC | Modifier::BOLD),
|
||||
]
|
||||
}
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn combined_patch_gives_same_result_as_individual_patch() {
|
||||
let styles = styles();
|
||||
let styles = [
|
||||
Style::new(),
|
||||
Style::new().fg(Color::Yellow),
|
||||
Style::new().bg(Color::Yellow),
|
||||
Style::new().add_modifier(Modifier::BOLD),
|
||||
Style::new().remove_modifier(Modifier::BOLD),
|
||||
Style::new().add_modifier(Modifier::ITALIC),
|
||||
Style::new().remove_modifier(Modifier::ITALIC),
|
||||
Style::new().add_modifier(Modifier::ITALIC | Modifier::BOLD),
|
||||
Style::new().remove_modifier(Modifier::ITALIC | Modifier::BOLD),
|
||||
];
|
||||
for &a in &styles {
|
||||
for &b in &styles {
|
||||
for &c in &styles {
|
||||
for &d in &styles {
|
||||
let combined = a.patch(b.patch(c.patch(d)));
|
||||
|
||||
assert_eq!(
|
||||
Style::default().patch(a).patch(b).patch(c).patch(d),
|
||||
Style::default().patch(combined)
|
||||
Style::new().patch(a).patch(b).patch(c).patch(d),
|
||||
Style::new().patch(a.patch(b.patch(c.patch(d))))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -589,7 +579,7 @@ mod tests {
|
||||
fn combine_individual_modifiers() {
|
||||
use crate::{buffer::Buffer, layout::Rect};
|
||||
|
||||
let mods = vec![
|
||||
let mods = [
|
||||
Modifier::BOLD,
|
||||
Modifier::DIM,
|
||||
Modifier::ITALIC,
|
||||
@@ -603,37 +593,30 @@ mod tests {
|
||||
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
|
||||
|
||||
for m in &mods {
|
||||
for m in mods {
|
||||
buffer.get_mut(0, 0).set_style(Style::reset());
|
||||
buffer
|
||||
.get_mut(0, 0)
|
||||
.set_style(Style::default().add_modifier(*m));
|
||||
buffer.get_mut(0, 0).set_style(Style::new().add_modifier(m));
|
||||
let style = buffer.get(0, 0).style();
|
||||
assert!(style.add_modifier.contains(*m));
|
||||
assert!(!style.sub_modifier.contains(*m));
|
||||
assert!(style.add_modifier.contains(m));
|
||||
assert!(!style.sub_modifier.contains(m));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn modifier_debug() {
|
||||
assert_eq!(format!("{:?}", Modifier::empty()), "NONE");
|
||||
assert_eq!(format!("{:?}", Modifier::BOLD), "BOLD");
|
||||
assert_eq!(format!("{:?}", Modifier::DIM), "DIM");
|
||||
assert_eq!(format!("{:?}", Modifier::ITALIC), "ITALIC");
|
||||
assert_eq!(format!("{:?}", Modifier::UNDERLINED), "UNDERLINED");
|
||||
assert_eq!(format!("{:?}", Modifier::SLOW_BLINK), "SLOW_BLINK");
|
||||
assert_eq!(format!("{:?}", Modifier::RAPID_BLINK), "RAPID_BLINK");
|
||||
assert_eq!(format!("{:?}", Modifier::REVERSED), "REVERSED");
|
||||
assert_eq!(format!("{:?}", Modifier::HIDDEN), "HIDDEN");
|
||||
assert_eq!(format!("{:?}", Modifier::CROSSED_OUT), "CROSSED_OUT");
|
||||
assert_eq!(
|
||||
format!("{:?}", Modifier::BOLD | Modifier::DIM),
|
||||
"BOLD | DIM"
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{:?}", Modifier::all()),
|
||||
"BOLD | DIM | ITALIC | UNDERLINED | SLOW_BLINK | RAPID_BLINK | REVERSED | HIDDEN | CROSSED_OUT"
|
||||
);
|
||||
#[rstest]
|
||||
#[case(Modifier::empty(), "NONE")]
|
||||
#[case(Modifier::BOLD, "BOLD")]
|
||||
#[case(Modifier::DIM, "DIM")]
|
||||
#[case(Modifier::ITALIC, "ITALIC")]
|
||||
#[case(Modifier::UNDERLINED, "UNDERLINED")]
|
||||
#[case(Modifier::SLOW_BLINK, "SLOW_BLINK")]
|
||||
#[case(Modifier::RAPID_BLINK, "RAPID_BLINK")]
|
||||
#[case(Modifier::REVERSED, "REVERSED")]
|
||||
#[case(Modifier::HIDDEN, "HIDDEN")]
|
||||
#[case(Modifier::CROSSED_OUT, "CROSSED_OUT")]
|
||||
#[case(Modifier::BOLD | Modifier::DIM, "BOLD | DIM")]
|
||||
#[case(Modifier::all(), "BOLD | DIM | ITALIC | UNDERLINED | SLOW_BLINK | RAPID_BLINK | REVERSED | HIDDEN | CROSSED_OUT")]
|
||||
fn modifier_debug(#[case] modifier: Modifier, #[case] expected: &str) {
|
||||
assert_eq!(format!("{modifier:?}"), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -663,151 +646,80 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
#[rstest]
|
||||
#[case(Style::new().black(), Color::Black)]
|
||||
#[case(Style::new().red(), Color::Red)]
|
||||
#[case(Style::new().green(), Color::Green)]
|
||||
#[case(Style::new().yellow(), Color::Yellow)]
|
||||
#[case(Style::new().blue(), Color::Blue)]
|
||||
#[case(Style::new().magenta(), Color::Magenta)]
|
||||
#[case(Style::new().cyan(), Color::Cyan)]
|
||||
#[case(Style::new().white(), Color::White)]
|
||||
#[case(Style::new().gray(), Color::Gray)]
|
||||
#[case(Style::new().dark_gray(), Color::DarkGray)]
|
||||
#[case(Style::new().light_red(), Color::LightRed)]
|
||||
#[case(Style::new().light_green(), Color::LightGreen)]
|
||||
#[case(Style::new().light_yellow(), Color::LightYellow)]
|
||||
#[case(Style::new().light_blue(), Color::LightBlue)]
|
||||
#[case(Style::new().light_magenta(), Color::LightMagenta)]
|
||||
#[case(Style::new().light_cyan(), Color::LightCyan)]
|
||||
#[case(Style::new().white(), Color::White)]
|
||||
fn fg_can_be_stylized(#[case] stylized: Style, #[case] expected: Color) {
|
||||
assert_eq!(stylized, Style::new().fg(expected));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(Style::new().on_black(), Color::Black)]
|
||||
#[case(Style::new().on_red(), Color::Red)]
|
||||
#[case(Style::new().on_green(), Color::Green)]
|
||||
#[case(Style::new().on_yellow(), Color::Yellow)]
|
||||
#[case(Style::new().on_blue(), Color::Blue)]
|
||||
#[case(Style::new().on_magenta(), Color::Magenta)]
|
||||
#[case(Style::new().on_cyan(), Color::Cyan)]
|
||||
#[case(Style::new().on_white(), Color::White)]
|
||||
#[case(Style::new().on_gray(), Color::Gray)]
|
||||
#[case(Style::new().on_dark_gray(), Color::DarkGray)]
|
||||
#[case(Style::new().on_light_red(), Color::LightRed)]
|
||||
#[case(Style::new().on_light_green(), Color::LightGreen)]
|
||||
#[case(Style::new().on_light_yellow(), Color::LightYellow)]
|
||||
#[case(Style::new().on_light_blue(), Color::LightBlue)]
|
||||
#[case(Style::new().on_light_magenta(), Color::LightMagenta)]
|
||||
#[case(Style::new().on_light_cyan(), Color::LightCyan)]
|
||||
#[case(Style::new().on_white(), Color::White)]
|
||||
fn bg_can_be_stylized(#[case] stylized: Style, #[case] expected: Color) {
|
||||
assert_eq!(stylized, Style::new().bg(expected));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(Style::new().bold(), Modifier::BOLD)]
|
||||
#[case(Style::new().dim(), Modifier::DIM)]
|
||||
#[case(Style::new().italic(), Modifier::ITALIC)]
|
||||
#[case(Style::new().underlined(), Modifier::UNDERLINED)]
|
||||
#[case(Style::new().slow_blink(), Modifier::SLOW_BLINK)]
|
||||
#[case(Style::new().rapid_blink(), Modifier::RAPID_BLINK)]
|
||||
#[case(Style::new().reversed(), Modifier::REVERSED)]
|
||||
#[case(Style::new().hidden(), Modifier::HIDDEN)]
|
||||
#[case(Style::new().crossed_out(), Modifier::CROSSED_OUT)]
|
||||
fn add_modifier_can_be_stylized(#[case] stylized: Style, #[case] expected: Modifier) {
|
||||
assert_eq!(stylized, Style::new().add_modifier(expected));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(Style::new().not_bold(), Modifier::BOLD)]
|
||||
#[case(Style::new().not_dim(), Modifier::DIM)]
|
||||
#[case(Style::new().not_italic(), Modifier::ITALIC)]
|
||||
#[case(Style::new().not_underlined(), Modifier::UNDERLINED)]
|
||||
#[case(Style::new().not_slow_blink(), Modifier::SLOW_BLINK)]
|
||||
#[case(Style::new().not_rapid_blink(), Modifier::RAPID_BLINK)]
|
||||
#[case(Style::new().not_reversed(), Modifier::REVERSED)]
|
||||
#[case(Style::new().not_hidden(), Modifier::HIDDEN)]
|
||||
#[case(Style::new().not_crossed_out(), Modifier::CROSSED_OUT)]
|
||||
fn remove_modifier_can_be_stylized(#[case] stylized: Style, #[case] expected: Modifier) {
|
||||
assert_eq!(stylized, Style::new().remove_modifier(expected));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn style_can_be_stylized() {
|
||||
// foreground colors
|
||||
assert_eq!(Style::new().black(), Style::new().fg(Color::Black));
|
||||
assert_eq!(Style::new().red(), Style::new().fg(Color::Red));
|
||||
assert_eq!(Style::new().green(), Style::new().fg(Color::Green));
|
||||
assert_eq!(Style::new().yellow(), Style::new().fg(Color::Yellow));
|
||||
assert_eq!(Style::new().blue(), Style::new().fg(Color::Blue));
|
||||
assert_eq!(Style::new().magenta(), Style::new().fg(Color::Magenta));
|
||||
assert_eq!(Style::new().cyan(), Style::new().fg(Color::Cyan));
|
||||
assert_eq!(Style::new().white(), Style::new().fg(Color::White));
|
||||
assert_eq!(Style::new().gray(), Style::new().fg(Color::Gray));
|
||||
assert_eq!(Style::new().dark_gray(), Style::new().fg(Color::DarkGray));
|
||||
assert_eq!(Style::new().light_red(), Style::new().fg(Color::LightRed));
|
||||
assert_eq!(
|
||||
Style::new().light_green(),
|
||||
Style::new().fg(Color::LightGreen)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().light_yellow(),
|
||||
Style::new().fg(Color::LightYellow)
|
||||
);
|
||||
assert_eq!(Style::new().light_blue(), Style::new().fg(Color::LightBlue));
|
||||
assert_eq!(
|
||||
Style::new().light_magenta(),
|
||||
Style::new().fg(Color::LightMagenta)
|
||||
);
|
||||
assert_eq!(Style::new().light_cyan(), Style::new().fg(Color::LightCyan));
|
||||
assert_eq!(Style::new().white(), Style::new().fg(Color::White));
|
||||
|
||||
// Background colors
|
||||
assert_eq!(Style::new().on_black(), Style::new().bg(Color::Black));
|
||||
assert_eq!(Style::new().on_red(), Style::new().bg(Color::Red));
|
||||
assert_eq!(Style::new().on_green(), Style::new().bg(Color::Green));
|
||||
assert_eq!(Style::new().on_yellow(), Style::new().bg(Color::Yellow));
|
||||
assert_eq!(Style::new().on_blue(), Style::new().bg(Color::Blue));
|
||||
assert_eq!(Style::new().on_magenta(), Style::new().bg(Color::Magenta));
|
||||
assert_eq!(Style::new().on_cyan(), Style::new().bg(Color::Cyan));
|
||||
assert_eq!(Style::new().on_white(), Style::new().bg(Color::White));
|
||||
assert_eq!(Style::new().on_gray(), Style::new().bg(Color::Gray));
|
||||
assert_eq!(
|
||||
Style::new().on_dark_gray(),
|
||||
Style::new().bg(Color::DarkGray)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().on_light_red(),
|
||||
Style::new().bg(Color::LightRed)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().on_light_green(),
|
||||
Style::new().bg(Color::LightGreen)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().on_light_yellow(),
|
||||
Style::new().bg(Color::LightYellow)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().on_light_blue(),
|
||||
Style::new().bg(Color::LightBlue)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().on_light_magenta(),
|
||||
Style::new().bg(Color::LightMagenta)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().on_light_cyan(),
|
||||
Style::new().bg(Color::LightCyan)
|
||||
);
|
||||
assert_eq!(Style::new().on_white(), Style::new().bg(Color::White));
|
||||
|
||||
// Add Modifiers
|
||||
assert_eq!(
|
||||
Style::new().bold(),
|
||||
Style::new().add_modifier(Modifier::BOLD)
|
||||
);
|
||||
assert_eq!(Style::new().dim(), Style::new().add_modifier(Modifier::DIM));
|
||||
assert_eq!(
|
||||
Style::new().italic(),
|
||||
Style::new().add_modifier(Modifier::ITALIC)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().underlined(),
|
||||
Style::new().add_modifier(Modifier::UNDERLINED)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().slow_blink(),
|
||||
Style::new().add_modifier(Modifier::SLOW_BLINK)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().rapid_blink(),
|
||||
Style::new().add_modifier(Modifier::RAPID_BLINK)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().reversed(),
|
||||
Style::new().add_modifier(Modifier::REVERSED)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().hidden(),
|
||||
Style::new().add_modifier(Modifier::HIDDEN)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().crossed_out(),
|
||||
Style::new().add_modifier(Modifier::CROSSED_OUT)
|
||||
);
|
||||
|
||||
// Remove Modifiers
|
||||
assert_eq!(
|
||||
Style::new().not_bold(),
|
||||
Style::new().remove_modifier(Modifier::BOLD)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().not_dim(),
|
||||
Style::new().remove_modifier(Modifier::DIM)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().not_italic(),
|
||||
Style::new().remove_modifier(Modifier::ITALIC)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().not_underlined(),
|
||||
Style::new().remove_modifier(Modifier::UNDERLINED)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().not_slow_blink(),
|
||||
Style::new().remove_modifier(Modifier::SLOW_BLINK)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().not_rapid_blink(),
|
||||
Style::new().remove_modifier(Modifier::RAPID_BLINK)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().not_reversed(),
|
||||
Style::new().remove_modifier(Modifier::REVERSED)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().not_hidden(),
|
||||
Style::new().remove_modifier(Modifier::HIDDEN)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().not_crossed_out(),
|
||||
Style::new().remove_modifier(Modifier::CROSSED_OUT)
|
||||
);
|
||||
|
||||
// reset
|
||||
fn reset_can_be_stylized() {
|
||||
assert_eq!(Style::new().reset(), Style::reset());
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
#![allow(clippy::unreadable_literal)]
|
||||
|
||||
use std::{
|
||||
fmt::{self, Debug, Display},
|
||||
str::FromStr,
|
||||
};
|
||||
use std::{fmt, str::FromStr};
|
||||
|
||||
/// ANSI Color
|
||||
///
|
||||
@@ -66,7 +63,6 @@ use std::{
|
||||
///
|
||||
/// [ANSI color table]: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
|
||||
pub enum Color {
|
||||
/// Resets the foreground or background color
|
||||
#[default]
|
||||
@@ -141,14 +137,102 @@ impl Color {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl serde::Serialize for Color {
|
||||
/// This utilises the [`fmt::Display`] implementation for serialization.
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl<'de> serde::Deserialize<'de> for Color {
|
||||
/// This is used to deserialize a value into Color via serde.
|
||||
///
|
||||
/// This implementation uses the `FromStr` trait to deserialize strings, so named colours, RGB,
|
||||
/// and indexed values are able to be deserialized. In addition, values that were produced by
|
||||
/// the the older serialization implementation of Color are also able to be deserialized.
|
||||
///
|
||||
/// Prior to v0.26.0, Ratatui would be serialized using a map for indexed and RGB values, for
|
||||
/// examples in json `{"Indexed": 10}` and `{"Rgb": [255, 0, 255]}` respectively. Now they are
|
||||
/// serialized using the string representation of the index and the RGB hex value, for example
|
||||
/// in json it would now be `"10"` and `"#FF00FF"` respectively.
|
||||
///
|
||||
/// See the [`Color`] documentation for more information on color names.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::prelude::*;
|
||||
///
|
||||
/// #[derive(Debug, serde::Deserialize)]
|
||||
/// struct Theme {
|
||||
/// color: Color,
|
||||
/// }
|
||||
///
|
||||
/// # fn get_theme() -> Result<(), serde_json::Error> {
|
||||
/// let theme: Theme = serde_json::from_str(r#"{"color": "bright-white"}"#)?;
|
||||
/// assert_eq!(theme.color, Color::White);
|
||||
///
|
||||
/// let theme: Theme = serde_json::from_str(r##"{"color": "#00FF00"}"##)?;
|
||||
/// assert_eq!(theme.color, Color::Rgb(0, 255, 0));
|
||||
///
|
||||
/// let theme: Theme = serde_json::from_str(r#"{"color": "42"}"#)?;
|
||||
/// assert_eq!(theme.color, Color::Indexed(42));
|
||||
///
|
||||
/// let err = serde_json::from_str::<Theme>(r#"{"color": "invalid"}"#).unwrap_err();
|
||||
/// assert!(err.is_data());
|
||||
/// assert_eq!(
|
||||
/// err.to_string(),
|
||||
/// "Failed to parse Colors at line 1 column 20"
|
||||
/// );
|
||||
///
|
||||
/// // Deserializing from the previous serialization implementation
|
||||
/// let theme: Theme = serde_json::from_str(r#"{"color": {"Rgb":[255,0,255]}}"#)?;
|
||||
/// assert_eq!(theme.color, Color::Rgb(255, 0, 255));
|
||||
///
|
||||
/// let theme: Theme = serde_json::from_str(r#"{"color": {"Indexed":10}}"#)?;
|
||||
/// assert_eq!(theme.color, Color::Indexed(10));
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
FromStr::from_str(&s).map_err(serde::de::Error::custom)
|
||||
/// Colors are currently serialized with the `Display` implementation, so
|
||||
/// RGB values are serialized via hex, for example "#FFFFFF".
|
||||
///
|
||||
/// Previously they were serialized using serde derive, which encoded
|
||||
/// RGB values as a map, for example { "rgb": [255, 255, 255] }.
|
||||
///
|
||||
/// The deserialization implementation utilises a `Helper` struct
|
||||
/// to be able to support both formats for backwards compatibility.
|
||||
#[derive(serde::Deserialize)]
|
||||
enum ColorWrapper {
|
||||
Rgb(u8, u8, u8),
|
||||
Indexed(u8),
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum ColorFormat {
|
||||
V2(String),
|
||||
V1(ColorWrapper),
|
||||
}
|
||||
|
||||
let multi_type = ColorFormat::deserialize(deserializer)
|
||||
.map_err(|err| serde::de::Error::custom(format!("Failed to parse Colors: {err}")))?;
|
||||
match multi_type {
|
||||
ColorFormat::V2(s) => FromStr::from_str(&s).map_err(serde::de::Error::custom),
|
||||
ColorFormat::V1(color_wrapper) => match color_wrapper {
|
||||
ColorWrapper::Rgb(red, green, blue) => Ok(Self::Rgb(red, green, blue)),
|
||||
ColorWrapper::Indexed(index) => Ok(Self::Indexed(index)),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,8 +240,8 @@ impl<'de> serde::Deserialize<'de> for Color {
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub struct ParseColorError;
|
||||
|
||||
impl std::fmt::Display for ParseColorError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
impl fmt::Display for ParseColorError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "Failed to parse Colors")
|
||||
}
|
||||
}
|
||||
@@ -229,16 +313,7 @@ impl FromStr for Color {
|
||||
_ => {
|
||||
if let Ok(index) = s.parse::<u8>() {
|
||||
Self::Indexed(index)
|
||||
} else if let (Ok(r), Ok(g), Ok(b)) = {
|
||||
if !s.starts_with('#') || s.len() != 7 {
|
||||
return Err(ParseColorError);
|
||||
}
|
||||
(
|
||||
u8::from_str_radix(&s[1..3], 16),
|
||||
u8::from_str_radix(&s[3..5], 16),
|
||||
u8::from_str_radix(&s[5..7], 16),
|
||||
)
|
||||
} {
|
||||
} else if let Some((r, g, b)) = parse_hex_color(s) {
|
||||
Self::Rgb(r, g, b)
|
||||
} else {
|
||||
return Err(ParseColorError);
|
||||
@@ -249,7 +324,17 @@ impl FromStr for Color {
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Color {
|
||||
fn parse_hex_color(input: &str) -> Option<(u8, u8, u8)> {
|
||||
if !input.starts_with('#') || input.len() != 7 {
|
||||
return None;
|
||||
}
|
||||
let r = u8::from_str_radix(input.get(1..3)?, 16).ok()?;
|
||||
let g = u8::from_str_radix(input.get(3..5)?, 16).ok()?;
|
||||
let b = u8::from_str_radix(input.get(5..7)?, 16).ok()?;
|
||||
Some((r, g, b))
|
||||
}
|
||||
|
||||
impl fmt::Display for Color {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Reset => write!(f, "Reset"),
|
||||
@@ -503,6 +588,7 @@ mod tests {
|
||||
"abcdef0", // 7 chars is not a color
|
||||
" bcdefa", // doesn't start with a '#'
|
||||
"#abcdef00", // too many chars
|
||||
"#1🦀2", // len 7 but on char boundaries shouldnt panic
|
||||
"resett", // typo
|
||||
"lightblackk", // typo
|
||||
];
|
||||
@@ -579,4 +665,42 @@ mod tests {
|
||||
Color::deserialize("#00000000".into_deserializer());
|
||||
assert!(color.is_err());
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
#[test]
|
||||
fn serialize_then_deserialize() -> Result<(), serde_json::Error> {
|
||||
let json_rgb = serde_json::to_string(&Color::Rgb(255, 0, 255))?;
|
||||
assert_eq!(json_rgb, r##""#FF00FF""##);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<Color>(&json_rgb)?,
|
||||
Color::Rgb(255, 0, 255)
|
||||
);
|
||||
|
||||
let json_white = serde_json::to_string(&Color::White)?;
|
||||
assert_eq!(json_white, r#""White""#);
|
||||
|
||||
let json_indexed = serde_json::to_string(&Color::Indexed(10))?;
|
||||
assert_eq!(json_indexed, r#""10""#);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<Color>(&json_indexed)?,
|
||||
Color::Indexed(10)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
#[test]
|
||||
fn deserialize_with_previous_format() -> Result<(), serde_json::Error> {
|
||||
assert_eq!(Color::White, serde_json::from_str::<Color>("\"White\"")?);
|
||||
assert_eq!(
|
||||
Color::Rgb(255, 0, 255),
|
||||
serde_json::from_str::<Color>(r#"{"Rgb":[255,0,255]}"#)?
|
||||
);
|
||||
assert_eq!(
|
||||
Color::Indexed(10),
|
||||
serde_json::from_str::<Color>(r#"{"Indexed":10}"#)?
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,17 +133,13 @@ macro_rules! modifier {
|
||||
/// "world".green().on_yellow().not_bold(),
|
||||
/// ]);
|
||||
/// let paragraph = Paragraph::new(line).italic().underlined();
|
||||
/// let block = Block::default()
|
||||
/// .title("Title")
|
||||
/// .borders(Borders::ALL)
|
||||
/// .on_white()
|
||||
/// .bold();
|
||||
/// let block = Block::bordered().title("Title").on_white().bold();
|
||||
/// ```
|
||||
pub trait Stylize<'a, T>: Sized {
|
||||
#[must_use = "`bg` returns the modified style without modifying the original"]
|
||||
fn bg(self, color: Color) -> T;
|
||||
fn bg<C: Into<Color>>(self, color: C) -> T;
|
||||
#[must_use = "`fg` returns the modified style without modifying the original"]
|
||||
fn fg<S: Into<Color>>(self, color: S) -> T;
|
||||
fn fg<C: Into<Color>>(self, color: C) -> 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"]
|
||||
@@ -183,12 +179,12 @@ impl<'a, T, U> Stylize<'a, T> for U
|
||||
where
|
||||
U: Styled<Item = T>,
|
||||
{
|
||||
fn bg(self, color: Color) -> T {
|
||||
let style = self.style().bg(color);
|
||||
fn bg<C: Into<Color>>(self, color: C) -> T {
|
||||
let style = self.style().bg(color.into());
|
||||
self.set_style(style)
|
||||
}
|
||||
|
||||
fn fg<S: Into<Color>>(self, color: S) -> T {
|
||||
fn fg<C: Into<Color>>(self, color: C) -> T {
|
||||
let style = self.style().fg(color.into());
|
||||
self.set_style(style)
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ impl Frame<'_> {
|
||||
/// # let backend = TestBackend::new(5, 5);
|
||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||
/// # let mut frame = terminal.get_frame();
|
||||
/// let block = Block::default();
|
||||
/// let block = Block::new();
|
||||
/// let area = Rect::new(0, 0, 5, 5);
|
||||
/// frame.render_widget(block, area);
|
||||
/// ```
|
||||
@@ -83,13 +83,15 @@ impl Frame<'_> {
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # #[cfg(feature = "unstable-widget-ref")] {
|
||||
/// # use ratatui::{backend::TestBackend, prelude::*, widgets::Block};
|
||||
/// # let backend = TestBackend::new(5, 5);
|
||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||
/// # let mut frame = terminal.get_frame();
|
||||
/// let block = Block::default();
|
||||
/// let block = Block::new();
|
||||
/// let area = Rect::new(0, 0, 5, 5);
|
||||
/// frame.render_widget_ref(block, area);
|
||||
/// # }
|
||||
/// ```
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
#[stability::unstable(feature = "widget-ref")]
|
||||
@@ -138,6 +140,7 @@ impl Frame<'_> {
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # #[cfg(feature = "unstable-widget-ref")] {
|
||||
/// # use ratatui::{backend::TestBackend, prelude::*, widgets::*};
|
||||
/// # let backend = TestBackend::new(5, 5);
|
||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||
@@ -146,6 +149,7 @@ impl Frame<'_> {
|
||||
/// 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_ref(list, area, &mut state);
|
||||
/// # }
|
||||
/// ```
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
#[stability::unstable(feature = "widget-ref")]
|
||||
|
||||
@@ -25,21 +25,20 @@
|
||||
//! // Converted to Line(vec![
|
||||
//! // Span { content: Cow::Borrowed("My title"), style: Style { .. } }
|
||||
//! // ])
|
||||
//! let block = Block::default().title("My title");
|
||||
//! let block = Block::new().title("My title");
|
||||
//!
|
||||
//! // A simple string with a unique style.
|
||||
//! // Converted to Line(vec![
|
||||
//! // Span { content: Cow::Borrowed("My title"), style: Style { fg: Some(Color::Yellow), .. }
|
||||
//! // ])
|
||||
//! let block =
|
||||
//! Block::default().title(Span::styled("My title", Style::default().fg(Color::Yellow)));
|
||||
//! let block = Block::new().title(Span::styled("My title", Style::default().fg(Color::Yellow)));
|
||||
//!
|
||||
//! // A string with multiple styles.
|
||||
//! // Converted to Line(vec![
|
||||
//! // Span { content: Cow::Borrowed("My"), style: Style { fg: Some(Color::Yellow), .. } },
|
||||
//! // Span { content: Cow::Borrowed(" title"), .. }
|
||||
//! // ])
|
||||
//! let block = Block::default().title(vec![
|
||||
//! let block = Block::new().title(vec![
|
||||
//! Span::styled("My", Style::default().fg(Color::Yellow)),
|
||||
//! Span::raw(" title"),
|
||||
//! ]);
|
||||
|
||||
388
src/text/line.rs
388
src/text/line.rs
@@ -1,5 +1,8 @@
|
||||
#![deny(missing_docs)]
|
||||
use std::borrow::Cow;
|
||||
#![warn(clippy::pedantic, clippy::nursery, clippy::arithmetic_side_effects)]
|
||||
use std::{borrow::Cow, fmt};
|
||||
|
||||
use unicode_truncate::UnicodeTruncateStr;
|
||||
|
||||
use super::StyledGrapheme;
|
||||
use crate::prelude::*;
|
||||
@@ -552,32 +555,100 @@ impl Widget for Line<'_> {
|
||||
impl WidgetRef for Line<'_> {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
let area = area.intersection(buf.area);
|
||||
buf.set_style(area, self.style);
|
||||
let width = self.width() as u16;
|
||||
let offset = match self.alignment {
|
||||
Some(Alignment::Center) => (area.width.saturating_sub(width)) / 2,
|
||||
Some(Alignment::Right) => area.width.saturating_sub(width),
|
||||
Some(Alignment::Left) | None => 0,
|
||||
};
|
||||
let mut x = area.left().saturating_add(offset);
|
||||
for span in &self.spans {
|
||||
let span_width = span.width() as u16;
|
||||
let span_area = Rect {
|
||||
x,
|
||||
width: span_width.min(area.right() - x),
|
||||
..area
|
||||
};
|
||||
span.render(span_area, buf);
|
||||
x = x.saturating_add(span_width);
|
||||
if x >= area.right() {
|
||||
break;
|
||||
}
|
||||
if area.is_empty() {
|
||||
return;
|
||||
}
|
||||
let line_width = self.width();
|
||||
if line_width == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
buf.set_style(area, self.style);
|
||||
|
||||
let area_width = usize::from(area.width);
|
||||
let can_render_complete_line = line_width <= area_width;
|
||||
if can_render_complete_line {
|
||||
let indent_width = match self.alignment {
|
||||
Some(Alignment::Center) => (area_width.saturating_sub(line_width)) / 2,
|
||||
Some(Alignment::Right) => area_width.saturating_sub(line_width),
|
||||
Some(Alignment::Left) | None => 0,
|
||||
};
|
||||
let indent_width = u16::try_from(indent_width).unwrap_or(u16::MAX);
|
||||
let area = area.indent_x(indent_width);
|
||||
render_spans(&self.spans, area, buf, 0);
|
||||
} else {
|
||||
// There is not enough space to render the whole line. As the right side is truncated by
|
||||
// the area width, only truncate the left.
|
||||
let skip_width = match self.alignment {
|
||||
Some(Alignment::Center) => (line_width.saturating_sub(area_width)) / 2,
|
||||
Some(Alignment::Right) => line_width.saturating_sub(area_width),
|
||||
Some(Alignment::Left) | None => 0,
|
||||
};
|
||||
render_spans(&self.spans, area, buf, skip_width);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Line<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
/// Renders all the spans of the line that should be visible.
|
||||
fn render_spans(spans: &[Span], mut area: Rect, buf: &mut Buffer, span_skip_width: usize) {
|
||||
for (span, span_width, offset) in spans_after_width(spans, span_skip_width) {
|
||||
area = area.indent_x(offset);
|
||||
if area.is_empty() {
|
||||
break;
|
||||
}
|
||||
span.render_ref(area, buf);
|
||||
let span_width = u16::try_from(span_width).unwrap_or(u16::MAX);
|
||||
area = area.indent_x(span_width);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an iterator over the spans that lie after a given skip widtch from the start of the
|
||||
/// `Line` (including a partially visible span if the `skip_width` lands within a span).
|
||||
fn spans_after_width<'a>(
|
||||
spans: &'a [Span],
|
||||
mut skip_width: usize,
|
||||
) -> impl Iterator<Item = (Span<'a>, usize, u16)> {
|
||||
spans
|
||||
.iter()
|
||||
.map(|span| (span, span.width()))
|
||||
// Filter non visible spans out.
|
||||
.filter_map(move |(span, span_width)| {
|
||||
// Ignore spans that are completely before the offset. Decrement `span_skip_width` by
|
||||
// the span width until we find a span that is partially or completely visible.
|
||||
if skip_width >= span_width {
|
||||
skip_width = skip_width.saturating_sub(span_width);
|
||||
return None;
|
||||
}
|
||||
|
||||
// Apply the skip from the start of the span, not the end as the end will be trimmed
|
||||
// when rendering the span to the buffer.
|
||||
let available_width = span_width.saturating_sub(skip_width);
|
||||
skip_width = 0; // ensure the next span is rendered in full
|
||||
Some((span, span_width, available_width))
|
||||
})
|
||||
.map(|(span, span_width, available_width)| {
|
||||
if span_width <= available_width {
|
||||
// Span is fully visible. Clone here is fast as the underlying content is `Cow`.
|
||||
return (span.clone(), span_width, 0u16);
|
||||
}
|
||||
// Span is only partially visible. As the end is truncated by the area width, only
|
||||
// truncate the start of the span.
|
||||
let (content, actual_width) = span.content.unicode_truncate_start(available_width);
|
||||
|
||||
// When the first grapheme of the span was truncated, start rendering from a position
|
||||
// that takes that into account by indenting the start of the area
|
||||
let first_grapheme_offset = available_width.saturating_sub(actual_width);
|
||||
let first_grapheme_offset = u16::try_from(first_grapheme_offset).unwrap_or(u16::MAX);
|
||||
(
|
||||
Span::styled(content, span.style),
|
||||
actual_width,
|
||||
first_grapheme_offset,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
impl fmt::Display for Line<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
for span in &self.spans {
|
||||
write!(f, "{span}")?;
|
||||
}
|
||||
@@ -878,8 +949,12 @@ mod tests {
|
||||
}
|
||||
|
||||
mod widget {
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use super::*;
|
||||
use crate::assert_buffer_eq;
|
||||
use crate::buffer::Cell;
|
||||
|
||||
const BLUE: Style = Style::new().fg(Color::Blue);
|
||||
const GREEN: Style = Style::new().fg(Color::Green);
|
||||
const ITALIC: Style = Style::new().add_modifier(Modifier::ITALIC);
|
||||
@@ -897,37 +972,36 @@ mod tests {
|
||||
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! "]);
|
||||
let mut expected = Buffer::with_lines(["Hello world! "]);
|
||||
expected.set_style(Rect::new(0, 0, 15, 1), ITALIC);
|
||||
expected.set_style(Rect::new(0, 0, 6, 1), BLUE);
|
||||
expected.set_style(Rect::new(6, 0, 6, 1), GREEN);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
assert_eq!(buf, expected);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn render_out_of_bounds(hello_world: Line<'static>, mut small_buf: Buffer) {
|
||||
let out_of_bounds = Rect::new(20, 20, 10, 1);
|
||||
hello_world.render(out_of_bounds, &mut small_buf);
|
||||
assert_buffer_eq!(small_buf, Buffer::empty(small_buf.area));
|
||||
assert_eq!(small_buf, Buffer::empty(small_buf.area));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_only_styles_line_area() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 1));
|
||||
hello_world().render(Rect::new(0, 0, 15, 1), &mut buf);
|
||||
let mut expected = Buffer::with_lines(vec!["Hello world! "]);
|
||||
let mut expected = Buffer::with_lines(["Hello world! "]);
|
||||
expected.set_style(Rect::new(0, 0, 15, 1), ITALIC);
|
||||
expected.set_style(Rect::new(0, 0, 6, 1), BLUE);
|
||||
expected.set_style(Rect::new(6, 0, 6, 1), GREEN);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
assert_eq!(buf, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_truncates() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 1));
|
||||
Line::from("Hello world!").render(Rect::new(0, 0, 5, 1), &mut buf);
|
||||
let expected = Buffer::with_lines(vec!["Hello "]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
assert_eq!(buf, Buffer::with_lines(["Hello "]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -935,11 +1009,11 @@ mod tests {
|
||||
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! "]);
|
||||
let mut expected = Buffer::with_lines([" Hello world! "]);
|
||||
expected.set_style(Rect::new(0, 0, 15, 1), ITALIC);
|
||||
expected.set_style(Rect::new(1, 0, 6, 1), BLUE);
|
||||
expected.set_style(Rect::new(7, 0, 6, 1), GREEN);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
assert_eq!(buf, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -947,11 +1021,255 @@ mod tests {
|
||||
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!"]);
|
||||
let mut expected = Buffer::with_lines([" Hello world!"]);
|
||||
expected.set_style(Rect::new(0, 0, 15, 1), ITALIC);
|
||||
expected.set_style(Rect::new(3, 0, 6, 1), BLUE);
|
||||
expected.set_style(Rect::new(9, 0, 6, 1), GREEN);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
assert_eq!(buf, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_truncates_left() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 5, 1));
|
||||
Line::from("Hello world")
|
||||
.left_aligned()
|
||||
.render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["Hello"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_truncates_right() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 5, 1));
|
||||
Line::from("Hello world")
|
||||
.right_aligned()
|
||||
.render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["world"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_truncates_center() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 5, 1));
|
||||
Line::from("Hello world")
|
||||
.centered()
|
||||
.render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["lo wo"]));
|
||||
}
|
||||
|
||||
/// Part of a regression test for <https://github.com/ratatui-org/ratatui/issues/1032> which
|
||||
/// found panics with truncating lines that contained multi-byte characters.
|
||||
#[test]
|
||||
fn regression_1032() {
|
||||
let line = Line::from(
|
||||
"🦀 RFC8628 OAuth 2.0 Device Authorization GrantでCLIからGithubのaccess tokenを取得する"
|
||||
);
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 83, 1));
|
||||
line.render_ref(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines([
|
||||
"🦀 RFC8628 OAuth 2.0 Device Authorization GrantでCLIからGithubのaccess tokenを取得 "
|
||||
]));
|
||||
}
|
||||
|
||||
/// Documentary test to highlight the crab emoji width / length discrepancy
|
||||
///
|
||||
/// Part of a regression test for <https://github.com/ratatui-org/ratatui/issues/1032> which
|
||||
/// found panics with truncating lines that contained multi-byte characters.
|
||||
#[test]
|
||||
fn crab_emoji_width() {
|
||||
let crab = "🦀";
|
||||
assert_eq!(crab.len(), 4); // bytes
|
||||
assert_eq!(crab.chars().count(), 1);
|
||||
assert_eq!(crab.graphemes(true).count(), 1);
|
||||
assert_eq!(crab.width(), 2); // display width
|
||||
}
|
||||
|
||||
/// Part of a regression test for <https://github.com/ratatui-org/ratatui/issues/1032> which
|
||||
/// found panics with truncating lines that contained multi-byte characters.
|
||||
#[rstest]
|
||||
#[case::left_4(Alignment::Left, 4, "1234")]
|
||||
#[case::left_5(Alignment::Left, 5, "1234 ")]
|
||||
#[case::left_6(Alignment::Left, 6, "1234🦀")]
|
||||
#[case::left_7(Alignment::Left, 7, "1234🦀7")]
|
||||
#[case::right_4(Alignment::Right, 4, "7890")]
|
||||
#[case::right_5(Alignment::Right, 5, " 7890")]
|
||||
#[case::right_6(Alignment::Right, 6, "🦀7890")]
|
||||
#[case::right_7(Alignment::Right, 7, "4🦀7890")]
|
||||
fn render_truncates_emoji(
|
||||
#[case] alignment: Alignment,
|
||||
#[case] buf_width: u16,
|
||||
#[case] expected: &str,
|
||||
) {
|
||||
let line = Line::from("1234🦀7890").alignment(alignment);
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, buf_width, 1));
|
||||
line.render_ref(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines([expected]));
|
||||
}
|
||||
|
||||
/// Part of a regression test for <https://github.com/ratatui-org/ratatui/issues/1032> which
|
||||
/// found panics with truncating lines that contained multi-byte characters.
|
||||
///
|
||||
/// centering is tricky because there's an ambiguity about whether to take one more char
|
||||
/// from the left or the right when the line width is odd. This interacts with the width of
|
||||
/// the crab emoji, which is 2 characters wide by hitting the left or right side of the
|
||||
/// emoji.
|
||||
#[rstest]
|
||||
#[case::center_6_0(6, 0, "")]
|
||||
#[case::center_6_1(6, 1, " ")] // lef side of "🦀"
|
||||
#[case::center_6_2(6, 2, "🦀")]
|
||||
#[case::center_6_3(6, 3, "b🦀")]
|
||||
#[case::center_6_4(6, 4, "b🦀c")]
|
||||
#[case::center_7_0(7, 0, "")]
|
||||
#[case::center_7_1(7, 1, " ")] // right side of "🦀"
|
||||
#[case::center_7_2(7, 2, "🦀")]
|
||||
#[case::center_7_3(7, 3, "🦀c")]
|
||||
#[case::center_7_4(7, 4, "b🦀c")]
|
||||
#[case::center_8_0(8, 0, "")]
|
||||
#[case::center_8_1(8, 1, " ")] // right side of "🦀"
|
||||
#[case::center_8_2(8, 2, " c")] // right side of "🦀c"
|
||||
#[case::center_8_3(8, 3, "🦀c")]
|
||||
#[case::center_8_4(8, 4, "🦀cd")]
|
||||
#[case::center_8_5(8, 5, "b🦀cd")]
|
||||
#[case::center_9_0(9, 0, "")]
|
||||
#[case::center_9_1(9, 1, "c")]
|
||||
#[case::center_9_2(9, 2, " c")] // right side of "🦀c"
|
||||
#[case::center_9_3(9, 3, " cd")]
|
||||
#[case::center_9_4(9, 4, "🦀cd")]
|
||||
#[case::center_9_5(9, 5, "🦀cde")]
|
||||
#[case::center_9_6(9, 6, "b🦀cde")]
|
||||
fn render_truncates_emoji_center(
|
||||
#[case] line_width: u16,
|
||||
#[case] buf_width: u16,
|
||||
#[case] expected: &str,
|
||||
) {
|
||||
// because the crab emoji is 2 characters wide, it will can cause the centering tests
|
||||
// intersect with either the left or right part of the emoji, which causes the emoji to
|
||||
// be not rendered. Checking for four different widths of the line is enough to cover
|
||||
// all the possible cases.
|
||||
let value = match line_width {
|
||||
6 => "ab🦀cd",
|
||||
7 => "ab🦀cde",
|
||||
8 => "ab🦀cdef",
|
||||
9 => "ab🦀cdefg",
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let line = Line::from(value).centered();
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, buf_width, 1));
|
||||
line.render_ref(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines([expected]));
|
||||
}
|
||||
|
||||
/// Ensures the rendering also works away from the 0x0 position.
|
||||
///
|
||||
/// Particularly of note is that an emoji that is truncated will not overwrite the
|
||||
/// characters that are already in the buffer. This is inentional (consider how a line
|
||||
/// that is rendered on a border should not overwrite the border with a partial emoji).
|
||||
#[rstest]
|
||||
#[case::left(Alignment::Left, "XXa🦀bcXXX")]
|
||||
#[case::center(Alignment::Center, "XX🦀bc🦀XX")]
|
||||
#[case::right(Alignment::Right, "XXXbc🦀dXX")]
|
||||
fn render_truncates_away_from_0x0(#[case] alignment: Alignment, #[case] expected: &str) {
|
||||
let line = Line::from(vec![Span::raw("a🦀b"), Span::raw("c🦀d")]).alignment(alignment);
|
||||
// Fill buffer with stuff to ensure the output is indeed padded
|
||||
let mut buf = Buffer::filled(Rect::new(0, 0, 10, 1), Cell::new("X"));
|
||||
let area = Rect::new(2, 0, 6, 1);
|
||||
line.render_ref(area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines([expected]));
|
||||
}
|
||||
|
||||
/// When two spans are rendered after each other the first needs to be padded in accordance
|
||||
/// to the skipped unicode width. In this case the first crab does not fit at width 6 which
|
||||
/// takes a front white space.
|
||||
#[rstest]
|
||||
#[case::right_4(4, "c🦀d")]
|
||||
#[case::right_5(5, "bc🦀d")]
|
||||
#[case::right_6(6, "Xbc🦀d")]
|
||||
#[case::right_7(7, "🦀bc🦀d")]
|
||||
#[case::right_8(8, "a🦀bc🦀d")]
|
||||
fn render_right_aligned_multi_span(#[case] buf_width: u16, #[case] expected: &str) {
|
||||
let line = Line::from(vec![Span::raw("a🦀b"), Span::raw("c🦀d")]).right_aligned();
|
||||
let area = Rect::new(0, 0, buf_width, 1);
|
||||
// Fill buffer with stuff to ensure the output is indeed padded
|
||||
let mut buf = Buffer::filled(area, Cell::new("X"));
|
||||
line.render_ref(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines([expected]));
|
||||
}
|
||||
|
||||
/// Part of a regression test for <https://github.com/ratatui-org/ratatui/issues/1032> which
|
||||
/// found panics with truncating lines that contained multi-byte characters.
|
||||
///
|
||||
/// Flag emoji are actually two independent characters, so they can be truncated in the
|
||||
/// middle of the emoji. This test documents just the emoji part of the test.
|
||||
#[test]
|
||||
fn flag_emoji() {
|
||||
let str = "🇺🇸1234";
|
||||
assert_eq!(str.len(), 12); // flag is 4 bytes
|
||||
assert_eq!(str.chars().count(), 6); // flag is 2 chars
|
||||
assert_eq!(str.graphemes(true).count(), 5); // flag is 1 grapheme
|
||||
assert_eq!(str.width(), 6); // flag is 2 display width
|
||||
}
|
||||
|
||||
/// Part of a regression test for <https://github.com/ratatui-org/ratatui/issues/1032> which
|
||||
/// found panics with truncating lines that contained multi-byte characters.
|
||||
#[rstest]
|
||||
#[case::flag_1(1, " ")]
|
||||
#[case::flag_2(2, "🇺🇸")]
|
||||
#[case::flag_3(3, "🇺🇸1")]
|
||||
#[case::flag_4(4, "🇺🇸12")]
|
||||
#[case::flag_5(5, "🇺🇸123")]
|
||||
#[case::flag_6(6, "🇺🇸1234")]
|
||||
#[case::flag_7(7, "🇺🇸1234 ")]
|
||||
fn render_truncates_flag(#[case] buf_width: u16, #[case] expected: &str) {
|
||||
let line = Line::from("🇺🇸1234");
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, buf_width, 1));
|
||||
line.render_ref(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines([expected]));
|
||||
}
|
||||
|
||||
// Buffer width is `u16`. A line can be longer.
|
||||
#[rstest]
|
||||
#[case::left(Alignment::Left, "This is some content with a some")]
|
||||
#[case::right(Alignment::Right, "horribly long Line over u16::MAX")]
|
||||
fn render_truncates_very_long_line_of_many_spans(
|
||||
#[case] alignment: Alignment,
|
||||
#[case] expected: &str,
|
||||
) {
|
||||
let part = "This is some content with a somewhat long width to be repeated over and over again to create horribly long Line over u16::MAX";
|
||||
let min_width = usize::from(u16::MAX).saturating_add(1);
|
||||
|
||||
// width == len as only ASCII is used here
|
||||
let factor = min_width.div_ceil(part.len());
|
||||
|
||||
let line = Line::from(vec![Span::raw(part); factor]).alignment(alignment);
|
||||
|
||||
dbg!(line.width());
|
||||
assert!(line.width() >= min_width);
|
||||
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 32, 1));
|
||||
line.render_ref(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines([expected]));
|
||||
}
|
||||
|
||||
// Buffer width is `u16`. A single span inside a line can be longer.
|
||||
#[rstest]
|
||||
#[case::left(Alignment::Left, "This is some content with a some")]
|
||||
#[case::right(Alignment::Right, "horribly long Line over u16::MAX")]
|
||||
fn render_truncates_very_long_single_span_line(
|
||||
#[case] alignment: Alignment,
|
||||
#[case] expected: &str,
|
||||
) {
|
||||
let part = "This is some content with a somewhat long width to be repeated over and over again to create horribly long Line over u16::MAX";
|
||||
let min_width = usize::from(u16::MAX).saturating_add(1);
|
||||
|
||||
// width == len as only ASCII is used here
|
||||
let factor = min_width.div_ceil(part.len());
|
||||
|
||||
let line = Line::from(vec![Span::raw(part.repeat(factor))]).alignment(alignment);
|
||||
|
||||
dbg!(line.width());
|
||||
assert!(line.width() >= min_width);
|
||||
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 32, 1));
|
||||
line.render_ref(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines([expected]));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
fmt::{self, Debug, Display},
|
||||
};
|
||||
use std::{borrow::Cow, fmt};
|
||||
|
||||
use super::Text;
|
||||
|
||||
@@ -19,7 +16,7 @@ use super::Text;
|
||||
/// let password = Masked::new("12345", 'x');
|
||||
///
|
||||
/// Paragraph::new(password).render(buffer.area, &mut buffer);
|
||||
/// assert_eq!(buffer, Buffer::with_lines(vec!["xxxxx"]));
|
||||
/// assert_eq!(buffer, Buffer::with_lines(["xxxxx"]));
|
||||
/// ```
|
||||
#[derive(Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Masked<'a> {
|
||||
@@ -46,17 +43,18 @@ impl<'a> Masked<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Masked<'_> {
|
||||
impl fmt::Debug for Masked<'_> {
|
||||
/// Debug representation of a masked string is the underlying string
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
Display::fmt(&self.inner, f)
|
||||
// note that calling display instead of Debug here is intentional
|
||||
fmt::Display::fmt(&self.inner, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Masked<'_> {
|
||||
impl fmt::Display for Masked<'_> {
|
||||
/// Display representation of a masked string is the masked string
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
Display::fmt(&self.value(), f)
|
||||
fmt::Display::fmt(&self.value(), f)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,12 +110,14 @@ mod tests {
|
||||
fn debug() {
|
||||
let masked = Masked::new("12345", 'x');
|
||||
assert_eq!(format!("{masked:?}"), "12345");
|
||||
assert_eq!(format!("{masked:.3?}"), "123", "Debug truncates");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display() {
|
||||
let masked = Masked::new("12345", 'x');
|
||||
assert_eq!(format!("{masked}"), "xxxxx");
|
||||
assert_eq!(format!("{masked:.3}"), "xxx", "Display truncates");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{borrow::Cow, fmt::Debug};
|
||||
use std::{borrow::Cow, fmt};
|
||||
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
@@ -395,9 +395,9 @@ impl WidgetRef for Span<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Span<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", &self.content)
|
||||
impl fmt::Display for Span<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
fmt::Display::fmt(&self.content, f)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -529,15 +529,15 @@ mod tests {
|
||||
#[test]
|
||||
fn display_span() {
|
||||
let span = Span::raw("test content");
|
||||
|
||||
assert_eq!(format!("{span}"), "test content");
|
||||
assert_eq!(format!("{span:.4}"), "test");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_styled_span() {
|
||||
let stylized_span = Span::styled("stylized test content", Style::new().green());
|
||||
|
||||
assert_eq!(format!("{stylized_span}"), "stylized test content");
|
||||
assert_eq!(format!("{stylized_span:.8}"), "stylized");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -565,7 +565,6 @@ mod tests {
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
use crate::assert_buffer_eq;
|
||||
|
||||
#[test]
|
||||
fn render() {
|
||||
@@ -573,12 +572,11 @@ mod tests {
|
||||
let span = Span::styled("test content", style);
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
|
||||
span.render(buf.area, &mut buf);
|
||||
|
||||
let expected = Buffer::with_lines(vec![Line::from(vec![
|
||||
let expected = Buffer::with_lines([Line::from(vec![
|
||||
"test content".green().on_yellow(),
|
||||
" ".into(),
|
||||
])]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
assert_eq!(buf, expected);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
@@ -598,10 +596,11 @@ mod tests {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 1));
|
||||
span.render(Rect::new(0, 0, 5, 1), &mut buf);
|
||||
|
||||
let mut expected = Buffer::with_lines(vec![Line::from("test ")]);
|
||||
expected.set_style(Rect::new(0, 0, 5, 1), (Color::Green, Color::Yellow));
|
||||
|
||||
assert_buffer_eq!(buf, expected);
|
||||
let expected = Buffer::with_lines([Line::from(vec![
|
||||
"test ".green().on_yellow(),
|
||||
" ".into(),
|
||||
])]);
|
||||
assert_eq!(buf, expected);
|
||||
}
|
||||
|
||||
/// When there is already a style set on the buffer, the style of the span should be
|
||||
@@ -613,12 +612,11 @@ mod tests {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
|
||||
buf.set_style(buf.area, Style::new().italic());
|
||||
span.render(buf.area, &mut buf);
|
||||
|
||||
let expected = Buffer::with_lines(vec![Line::from(vec![
|
||||
let expected = Buffer::with_lines([Line::from(vec![
|
||||
"test content".green().on_yellow().italic(),
|
||||
" ".italic(),
|
||||
])]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
assert_eq!(buf, expected);
|
||||
}
|
||||
|
||||
/// When the span contains a multi-width grapheme, the grapheme will ensure that the cells
|
||||
@@ -629,12 +627,11 @@ mod tests {
|
||||
let span = Span::styled("test 😃 content", style);
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
|
||||
span.render(buf.area, &mut buf);
|
||||
|
||||
// The existing code in buffer.set_line() handles multi-width graphemes by clearing the
|
||||
// cells of the hidden characters. This test ensures that the existing behavior is
|
||||
// preserved.
|
||||
let expected = Buffer::with_lines(vec!["test 😃 content".green().on_yellow()]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
let expected = Buffer::with_lines(["test 😃 content".green().on_yellow()]);
|
||||
assert_eq!(buf, expected);
|
||||
}
|
||||
|
||||
/// When the span contains a multi-width grapheme that does not fit in the area passed to
|
||||
@@ -647,11 +644,9 @@ mod tests {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 6, 1));
|
||||
span.render(buf.area, &mut buf);
|
||||
|
||||
let expected = Buffer::with_lines(vec![Line::from(vec![
|
||||
"test ".green().on_yellow(),
|
||||
" ".into(),
|
||||
])]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
let expected =
|
||||
Buffer::with_lines([Line::from(vec!["test ".green().on_yellow(), " ".into()])]);
|
||||
assert_eq!(buf, expected);
|
||||
}
|
||||
|
||||
/// When the area passed to render overflows the buffer, the content should be truncated
|
||||
@@ -663,11 +658,11 @@ mod tests {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
|
||||
span.render(Rect::new(10, 0, 20, 1), &mut buf);
|
||||
|
||||
let expected = Buffer::with_lines(vec![Line::from(vec![
|
||||
let expected = Buffer::with_lines([Line::from(vec![
|
||||
" ".into(),
|
||||
"test ".green().on_yellow(),
|
||||
])]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
assert_eq!(buf, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#![warn(missing_docs)]
|
||||
use std::borrow::Cow;
|
||||
use std::{borrow::Cow, fmt};
|
||||
|
||||
use itertools::{Itertools, Position};
|
||||
|
||||
@@ -580,8 +580,8 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Text<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
impl fmt::Display for Text<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
for (position, line) in self.iter().with_position() {
|
||||
if position == Position::Last {
|
||||
write!(f, "{line}")?;
|
||||
@@ -962,19 +962,14 @@ mod tests {
|
||||
|
||||
mod widget {
|
||||
use super::*;
|
||||
use crate::assert_buffer_eq;
|
||||
|
||||
#[test]
|
||||
fn render() {
|
||||
let text = Text::from("foo");
|
||||
|
||||
let area = Rect::new(0, 0, 5, 1);
|
||||
let mut buf = Buffer::empty(area);
|
||||
text.render(area, &mut buf);
|
||||
|
||||
let expected_buf = Buffer::with_lines(vec!["foo "]);
|
||||
|
||||
assert_buffer_eq!(buf, expected_buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["foo "]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
@@ -987,40 +982,28 @@ mod tests {
|
||||
#[test]
|
||||
fn render_right_aligned() {
|
||||
let text = Text::from("foo").alignment(Alignment::Right);
|
||||
|
||||
let area = Rect::new(0, 0, 5, 1);
|
||||
let mut buf = Buffer::empty(area);
|
||||
text.render(area, &mut buf);
|
||||
|
||||
let expected_buf = Buffer::with_lines(vec![" foo"]);
|
||||
|
||||
assert_buffer_eq!(buf, expected_buf);
|
||||
assert_eq!(buf, Buffer::with_lines([" foo"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_centered_odd() {
|
||||
let text = Text::from("foo").alignment(Alignment::Center);
|
||||
|
||||
let area = Rect::new(0, 0, 5, 1);
|
||||
let mut buf = Buffer::empty(area);
|
||||
text.render(area, &mut buf);
|
||||
|
||||
let expected_buf = Buffer::with_lines(vec![" foo "]);
|
||||
|
||||
assert_buffer_eq!(buf, expected_buf);
|
||||
assert_eq!(buf, Buffer::with_lines([" foo "]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_centered_even() {
|
||||
let text = Text::from("foo").alignment(Alignment::Center);
|
||||
|
||||
let area = Rect::new(0, 0, 6, 1);
|
||||
let mut buf = Buffer::empty(area);
|
||||
text.render(area, &mut buf);
|
||||
|
||||
let expected_buf = Buffer::with_lines(vec![" foo "]);
|
||||
|
||||
assert_buffer_eq!(buf, expected_buf);
|
||||
assert_eq!(buf, Buffer::with_lines([" foo "]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1030,14 +1013,10 @@ mod tests {
|
||||
Line::from("bar").alignment(Alignment::Center),
|
||||
])
|
||||
.alignment(Alignment::Right);
|
||||
|
||||
let area = Rect::new(0, 0, 5, 2);
|
||||
let mut buf = Buffer::empty(area);
|
||||
text.render(area, &mut buf);
|
||||
|
||||
let expected_buf = Buffer::with_lines(vec![" foo", " bar "]);
|
||||
|
||||
assert_buffer_eq!(buf, expected_buf);
|
||||
assert_eq!(buf, Buffer::with_lines([" foo", " bar "]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1046,10 +1025,9 @@ mod tests {
|
||||
let mut buf = Buffer::empty(area);
|
||||
Text::from("foo".on_blue()).render(area, &mut buf);
|
||||
|
||||
let mut expected = Buffer::with_lines(vec!["foo "]);
|
||||
let mut expected = Buffer::with_lines(["foo "]);
|
||||
expected.set_style(Rect::new(0, 0, 3, 1), Style::new().bg(Color::Blue));
|
||||
|
||||
assert_buffer_eq!(buf, expected);
|
||||
assert_eq!(buf, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1057,10 +1035,9 @@ mod tests {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 6, 1));
|
||||
Text::from("foobar".on_blue()).render(Rect::new(0, 0, 3, 1), &mut buf);
|
||||
|
||||
let mut expected = Buffer::with_lines(vec!["foo "]);
|
||||
let mut expected = Buffer::with_lines(["foo "]);
|
||||
expected.set_style(Rect::new(0, 0, 3, 1), Style::new().bg(Color::Blue));
|
||||
|
||||
assert_buffer_eq!(buf, expected);
|
||||
assert_eq!(buf, expected);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
426
src/widgets.rs
426
src/widgets.rs
@@ -245,9 +245,15 @@ pub trait StatefulWidget {
|
||||
/// provided. This is a convenience approach to make it easier to attach child widgets to parent
|
||||
/// widgets. It allows you to render an optional widget by reference.
|
||||
///
|
||||
/// A blanket [implementation of `WidgetRef` for `Fn(Rect, &mut
|
||||
/// Buffer)`](WidgetRef#impl-WidgetRef-for-F) is provided. This allows you to treat any function or
|
||||
/// closure that takes a `Rect` and a mutable reference to a `Buffer` as a widget in situations
|
||||
/// where you don't want to create a new type for a widget.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # #[cfg(feature = "unstable-widget-ref")] {
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
///
|
||||
/// struct Greeting;
|
||||
@@ -294,6 +300,7 @@ pub trait StatefulWidget {
|
||||
/// widget.render_ref(area, buf);
|
||||
/// }
|
||||
/// # }
|
||||
/// # }
|
||||
/// ```
|
||||
#[stability::unstable(feature = "widget-ref")]
|
||||
pub trait WidgetRef {
|
||||
@@ -302,13 +309,40 @@ pub trait WidgetRef {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer);
|
||||
}
|
||||
|
||||
/// This allows you to render a widget by reference.
|
||||
// /// This allows you to render a widget by reference.
|
||||
impl<W: WidgetRef> Widget for &W {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.render_ref(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
/// A blanket implementation of `WidgetRef` for `Fn(Rect, &mut Buffer)`.
|
||||
///
|
||||
/// This allows you to treat any function that takes a `Rect` and a mutable reference to a `Buffer`
|
||||
/// as a widget in situations where you don't want to create a new type for a widget.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
/// fn hello(area: Rect, buf: &mut Buffer) {
|
||||
/// Line::raw("Hello").render(area, buf);
|
||||
/// }
|
||||
///
|
||||
/// fn draw(mut frame: Frame) {
|
||||
/// frame.render_widget(&hello, frame.size());
|
||||
/// frame.render_widget_ref(hello, frame.size());
|
||||
/// }
|
||||
/// ```
|
||||
impl<F> WidgetRef for F
|
||||
where
|
||||
F: Fn(Rect, &mut Buffer),
|
||||
{
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
self(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
/// A blanket implementation of `WidgetExt` for `Option<W>` where `W` implements `WidgetRef`.
|
||||
///
|
||||
/// This is a convenience implementation that makes it easy to attach child widgets to parent
|
||||
@@ -321,6 +355,7 @@ impl<W: WidgetRef> Widget for &W {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # #[cfg(feature = "unstable-widget-ref")] {
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
///
|
||||
/// struct Parent {
|
||||
@@ -340,6 +375,7 @@ impl<W: WidgetRef> Widget for &W {
|
||||
/// self.child.render_ref(area, buf);
|
||||
/// }
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
impl<W: WidgetRef> WidgetRef for Option<W> {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
@@ -368,6 +404,7 @@ impl<W: WidgetRef> WidgetRef for Option<W> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # #[cfg(feature = "unstable-widget-ref")] {
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
///
|
||||
/// struct PersonalGreeting;
|
||||
@@ -386,10 +423,11 @@ impl<W: WidgetRef> WidgetRef for Option<W> {
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// # fn render(area: Rect, buf: &mut Buffer) {
|
||||
/// let widget = PersonalGreeting;
|
||||
/// let mut state = "world".to_string();
|
||||
/// widget.render(area, buf, &mut state);
|
||||
/// fn render(area: Rect, buf: &mut Buffer) {
|
||||
/// let widget = PersonalGreeting;
|
||||
/// let mut state = "world".to_string();
|
||||
/// widget.render(area, buf, &mut state);
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
#[stability::unstable(feature = "widget-ref")]
|
||||
@@ -470,90 +508,79 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::prelude::*;
|
||||
|
||||
struct Greeting;
|
||||
struct Farewell;
|
||||
struct PersonalGreeting;
|
||||
|
||||
impl Widget for Greeting {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.render_ref(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetRef for Greeting {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
Line::from("Hello").render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Farewell {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.render_ref(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetRef for Farewell {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
Line::from("Goodbye").right_aligned().render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl StatefulWidget for PersonalGreeting {
|
||||
type State = String;
|
||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||
self.render_ref(area, buf, state);
|
||||
}
|
||||
}
|
||||
|
||||
impl StatefulWidgetRef for PersonalGreeting {
|
||||
type State = String;
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||
Line::from(format!("Hello {state}")).render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
#[fixture]
|
||||
fn buf() -> Buffer {
|
||||
Buffer::empty(Rect::new(0, 0, 20, 1))
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn widget_render(mut buf: Buffer) {
|
||||
let widget = Greeting;
|
||||
widget.render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["Hello "]));
|
||||
}
|
||||
mod widget {
|
||||
use super::*;
|
||||
|
||||
#[rstest]
|
||||
fn widget_ref_render(mut buf: Buffer) {
|
||||
let widget = Greeting;
|
||||
widget.render_ref(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["Hello "]));
|
||||
}
|
||||
struct Greeting;
|
||||
|
||||
/// This test is to ensure that the blanket implementation of `Widget` for `&W` where `W`
|
||||
/// implements `WidgetRef` works as expected.
|
||||
#[rstest]
|
||||
fn widget_blanket_render(mut buf: Buffer) {
|
||||
let widget = &Greeting;
|
||||
widget.render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["Hello "]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn widget_box_render_ref(mut buf: Buffer) {
|
||||
let widget: Box<dyn WidgetRef> = Box::new(Greeting);
|
||||
widget.render_ref(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["Hello "]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn widget_vec_box_render(mut buf: Buffer) {
|
||||
let widgets: Vec<Box<dyn WidgetRef>> = vec![Box::new(Greeting), Box::new(Farewell)];
|
||||
for widget in widgets {
|
||||
widget.render_ref(buf.area, &mut buf);
|
||||
impl Widget for Greeting {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
Line::from("Hello").render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn render(mut buf: Buffer) {
|
||||
let widget = Greeting;
|
||||
widget.render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["Hello "]));
|
||||
}
|
||||
}
|
||||
|
||||
mod widget_ref {
|
||||
use super::*;
|
||||
|
||||
struct Greeting;
|
||||
struct Farewell;
|
||||
|
||||
impl WidgetRef for Greeting {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
Line::from("Hello").render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetRef for Farewell {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
Line::from("Goodbye").right_aligned().render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn render_ref(mut buf: Buffer) {
|
||||
let widget = Greeting;
|
||||
widget.render_ref(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["Hello "]));
|
||||
}
|
||||
|
||||
/// Ensure that the blanket implementation of `Widget` for `&W` where `W` implements
|
||||
/// `WidgetRef` works as expected.
|
||||
#[rstest]
|
||||
fn blanket_render(mut buf: Buffer) {
|
||||
let widget = &Greeting;
|
||||
widget.render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["Hello "]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn box_render_ref(mut buf: Buffer) {
|
||||
let widget: Box<dyn WidgetRef> = Box::new(Greeting);
|
||||
widget.render_ref(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["Hello "]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn vec_box_render(mut buf: Buffer) {
|
||||
let widgets: Vec<Box<dyn WidgetRef>> = vec![Box::new(Greeting), Box::new(Farewell)];
|
||||
for widget in widgets {
|
||||
widget.render_ref(buf.area, &mut buf);
|
||||
}
|
||||
assert_eq!(buf, Buffer::with_lines(["Hello Goodbye"]));
|
||||
}
|
||||
assert_eq!(buf, Buffer::with_lines(["Hello Goodbye"]));
|
||||
}
|
||||
|
||||
#[fixture]
|
||||
@@ -561,98 +588,181 @@ mod tests {
|
||||
"world".to_string()
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn stateful_widget_render(mut buf: Buffer, mut state: String) {
|
||||
let widget = PersonalGreeting;
|
||||
widget.render(buf.area, &mut buf, &mut state);
|
||||
assert_eq!(buf, Buffer::with_lines(["Hello world "]));
|
||||
mod stateful_widget {
|
||||
use super::*;
|
||||
|
||||
struct PersonalGreeting;
|
||||
|
||||
impl StatefulWidget for PersonalGreeting {
|
||||
type State = String;
|
||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||
Line::from(format!("Hello {state}")).render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn render(mut buf: Buffer, mut state: String) {
|
||||
let widget = PersonalGreeting;
|
||||
widget.render(buf.area, &mut buf, &mut state);
|
||||
assert_eq!(buf, Buffer::with_lines(["Hello world "]));
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn stateful_widget_ref_render(mut buf: Buffer, mut state: String) {
|
||||
let widget = PersonalGreeting;
|
||||
widget.render_ref(buf.area, &mut buf, &mut state);
|
||||
assert_eq!(buf, Buffer::with_lines(["Hello world "]));
|
||||
mod stateful_widget_ref {
|
||||
use super::*;
|
||||
|
||||
struct PersonalGreeting;
|
||||
|
||||
impl StatefulWidgetRef for PersonalGreeting {
|
||||
type State = String;
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||
Line::from(format!("Hello {state}")).render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn render_ref(mut buf: Buffer, mut state: String) {
|
||||
let widget = PersonalGreeting;
|
||||
widget.render_ref(buf.area, &mut buf, &mut state);
|
||||
assert_eq!(buf, Buffer::with_lines(["Hello world "]));
|
||||
}
|
||||
|
||||
// Note this cannot be tested until the blanket implementation of StatefulWidget for &W
|
||||
// where W implements StatefulWidgetRef is added. (see the comment in the blanket
|
||||
// implementation for more).
|
||||
// /// This test is to ensure that the blanket implementation of `StatefulWidget` for `&W`
|
||||
// where /// `W` implements `StatefulWidgetRef` works as expected.
|
||||
// #[rstest]
|
||||
// fn stateful_widget_blanket_render(mut buf: Buffer, mut state: String) {
|
||||
// let widget = &PersonalGreeting;
|
||||
// widget.render(buf.area, &mut buf, &mut state);
|
||||
// assert_eq!(buf, Buffer::with_lines(["Hello world "]));
|
||||
// }
|
||||
|
||||
#[rstest]
|
||||
fn box_render_render(mut buf: Buffer, mut state: String) {
|
||||
let widget = Box::new(PersonalGreeting);
|
||||
widget.render_ref(buf.area, &mut buf, &mut state);
|
||||
assert_eq!(buf, Buffer::with_lines(["Hello world "]));
|
||||
}
|
||||
}
|
||||
|
||||
// Note this cannot be tested until the blanket implementation of StatefulWidget for &W where W
|
||||
// implements StatefulWidgetRef is added. (see the comment in the blanket implementation for
|
||||
// more).
|
||||
// /// This test is to ensure that the blanket implementation of `StatefulWidget` for `&W` where
|
||||
// /// `W` implements `StatefulWidgetRef` works as expected.
|
||||
// #[rstest]
|
||||
// fn stateful_widget_blanket_render(mut buf: Buffer, mut state: String) {
|
||||
// let widget = &PersonalGreeting;
|
||||
// widget.render(buf.area, &mut buf, &mut state);
|
||||
// assert_eq!(buf, Buffer::with_lines(["Hello world "]));
|
||||
// }
|
||||
mod option_widget_ref {
|
||||
use super::*;
|
||||
|
||||
#[rstest]
|
||||
fn stateful_widget_box_render(mut buf: Buffer, mut state: String) {
|
||||
let widget = Box::new(PersonalGreeting);
|
||||
widget.render(buf.area, &mut buf, &mut state);
|
||||
assert_eq!(buf, Buffer::with_lines(["Hello world "]));
|
||||
struct Greeting;
|
||||
|
||||
impl WidgetRef for Greeting {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
Line::from("Hello").render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn render_ref_some(mut buf: Buffer) {
|
||||
let widget = Some(Greeting);
|
||||
widget.render_ref(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["Hello "]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn render_ref_none(mut buf: Buffer) {
|
||||
let widget: Option<Greeting> = None;
|
||||
widget.render_ref(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines([" "]));
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn widget_option_render_ref_some(mut buf: Buffer) {
|
||||
let widget = Some(Greeting);
|
||||
widget.render_ref(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["Hello "]));
|
||||
mod str {
|
||||
use super::*;
|
||||
|
||||
#[rstest]
|
||||
fn render(mut buf: Buffer) {
|
||||
"hello world".render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["hello world "]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn render_ref(mut buf: Buffer) {
|
||||
"hello world".render_ref(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["hello world "]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn option_render(mut buf: Buffer) {
|
||||
Some("hello world").render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["hello world "]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn option_render_ref(mut buf: Buffer) {
|
||||
Some("hello world").render_ref(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["hello world "]));
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn widget_option_render_ref_none(mut buf: Buffer) {
|
||||
let widget: Option<Greeting> = None;
|
||||
widget.render_ref(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines([" "]));
|
||||
mod string {
|
||||
use super::*;
|
||||
#[rstest]
|
||||
fn render(mut buf: Buffer) {
|
||||
String::from("hello world").render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["hello world "]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn render_ref(mut buf: Buffer) {
|
||||
String::from("hello world").render_ref(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["hello world "]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn option_render(mut buf: Buffer) {
|
||||
Some(String::from("hello world")).render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["hello world "]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn option_render_ref(mut buf: Buffer) {
|
||||
Some(String::from("hello world")).render_ref(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["hello world "]));
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn str_render(mut buf: Buffer) {
|
||||
"hello world".render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["hello world "]));
|
||||
}
|
||||
mod function_widget {
|
||||
use super::*;
|
||||
|
||||
#[rstest]
|
||||
fn str_render_ref(mut buf: Buffer) {
|
||||
"hello world".render_ref(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["hello world "]));
|
||||
}
|
||||
fn widget_function(area: Rect, buf: &mut Buffer) {
|
||||
"Hello".render(area, buf);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn str_option_render(mut buf: Buffer) {
|
||||
Some("hello world").render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["hello world "]));
|
||||
}
|
||||
#[rstest]
|
||||
fn render(mut buf: Buffer) {
|
||||
widget_function.render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["Hello "]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn str_option_render_ref(mut buf: Buffer) {
|
||||
Some("hello world").render_ref(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["hello world "]));
|
||||
}
|
||||
#[rstest]
|
||||
fn render_ref(mut buf: Buffer) {
|
||||
widget_function.render_ref(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["Hello "]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn string_render(mut buf: Buffer) {
|
||||
String::from("hello world").render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["hello world "]));
|
||||
}
|
||||
#[rstest]
|
||||
fn render_closure(mut buf: Buffer) {
|
||||
let widget = |area: Rect, buf: &mut Buffer| {
|
||||
"Hello".render(area, buf);
|
||||
};
|
||||
widget.render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["Hello "]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn string_render_ref(mut buf: Buffer) {
|
||||
String::from("hello world").render_ref(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["hello world "]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn string_option_render(mut buf: Buffer) {
|
||||
Some(String::from("hello world")).render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["hello world "]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn string_option_render_ref(mut buf: Buffer) {
|
||||
Some(String::from("hello world")).render_ref(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["hello world "]),);
|
||||
#[rstest]
|
||||
fn render_closure_ref(mut buf: Buffer) {
|
||||
let widget = |area: Rect, buf: &mut Buffer| {
|
||||
"Hello".render(area, buf);
|
||||
};
|
||||
widget.render_ref(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["Hello "]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ pub use bar_group::BarGroup;
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
///
|
||||
/// BarChart::default()
|
||||
/// .block(Block::default().title("BarChart").borders(Borders::ALL))
|
||||
/// .block(Block::bordered().title("BarChart"))
|
||||
/// .bar_width(3)
|
||||
/// .bar_gap(1)
|
||||
/// .group_gap(3)
|
||||
@@ -615,151 +615,140 @@ mod tests {
|
||||
use itertools::iproduct;
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
assert_buffer_eq,
|
||||
widgets::{BorderType, Borders},
|
||||
};
|
||||
use crate::widgets::BorderType;
|
||||
|
||||
#[test]
|
||||
fn default() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
|
||||
let widget = BarChart::default();
|
||||
widget.render(buffer.area, &mut buffer);
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" "; 3]));
|
||||
assert_eq!(buffer, Buffer::with_lines([" "; 3]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
|
||||
let widget = BarChart::default().data(&[("foo", 1), ("bar", 2)]);
|
||||
widget.render(buffer.area, &mut buffer);
|
||||
assert_buffer_eq!(
|
||||
buffer,
|
||||
Buffer::with_lines(vec![
|
||||
" █ ",
|
||||
"1 2 ",
|
||||
"f b ",
|
||||
])
|
||||
);
|
||||
#[rustfmt::skip]
|
||||
let expected = Buffer::with_lines([
|
||||
" █ ",
|
||||
"1 2 ",
|
||||
"f b ",
|
||||
]);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 5));
|
||||
let block = Block::default()
|
||||
.title("Block")
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 5));
|
||||
let block = Block::bordered()
|
||||
.border_type(BorderType::Double)
|
||||
.borders(Borders::ALL);
|
||||
.title("Block");
|
||||
let widget = BarChart::default()
|
||||
.data(&[("foo", 1), ("bar", 2)])
|
||||
.block(block);
|
||||
widget.render(buffer.area, &mut buffer);
|
||||
assert_buffer_eq!(
|
||||
buffer,
|
||||
Buffer::with_lines(vec![
|
||||
"╔Block════════╗",
|
||||
"║ █ ║",
|
||||
"║1 2 ║",
|
||||
"║f b ║",
|
||||
"╚═════════════╝",
|
||||
])
|
||||
);
|
||||
let expected = Buffer::with_lines([
|
||||
"╔Block═══╗",
|
||||
"║ █ ║",
|
||||
"║1 2 ║",
|
||||
"║f b ║",
|
||||
"╚════════╝",
|
||||
]);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
|
||||
let without_max = BarChart::default().data(&[("foo", 1), ("bar", 2), ("baz", 100)]);
|
||||
without_max.render(buffer.area, &mut buffer);
|
||||
assert_buffer_eq!(
|
||||
buffer,
|
||||
Buffer::with_lines(vec![
|
||||
" █ ",
|
||||
" █ ",
|
||||
"f b b ",
|
||||
])
|
||||
);
|
||||
#[rustfmt::skip]
|
||||
let expected = Buffer::with_lines([
|
||||
" █ ",
|
||||
" █ ",
|
||||
"f b b ",
|
||||
]);
|
||||
assert_eq!(buffer, expected);
|
||||
let with_max = BarChart::default()
|
||||
.data(&[("foo", 1), ("bar", 2), ("baz", 100)])
|
||||
.max(2);
|
||||
with_max.render(buffer.area, &mut buffer);
|
||||
assert_buffer_eq!(
|
||||
buffer,
|
||||
Buffer::with_lines(vec![
|
||||
" █ █ ",
|
||||
"1 2 █ ",
|
||||
"f b b ",
|
||||
])
|
||||
);
|
||||
#[rustfmt::skip]
|
||||
let expected = Buffer::with_lines([
|
||||
" █ █ ",
|
||||
"1 2 █ ",
|
||||
"f b b ",
|
||||
]);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bar_style() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
|
||||
let widget = BarChart::default()
|
||||
.data(&[("foo", 1), ("bar", 2)])
|
||||
.bar_style(Style::new().red());
|
||||
widget.render(buffer.area, &mut buffer);
|
||||
let mut expected = Buffer::with_lines(vec![
|
||||
" █ ",
|
||||
"1 2 ",
|
||||
"f b ",
|
||||
#[rustfmt::skip]
|
||||
let mut expected = Buffer::with_lines([
|
||||
" █ ",
|
||||
"1 2 ",
|
||||
"f b ",
|
||||
]);
|
||||
for (x, y) in iproduct!([0, 2], [0, 1]) {
|
||||
expected.get_mut(x, y).set_fg(Color::Red);
|
||||
}
|
||||
assert_buffer_eq!(buffer, expected);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bar_width() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
|
||||
let widget = BarChart::default()
|
||||
.data(&[("foo", 1), ("bar", 2)])
|
||||
.bar_width(3);
|
||||
widget.render(buffer.area, &mut buffer);
|
||||
assert_buffer_eq!(
|
||||
buffer,
|
||||
Buffer::with_lines(vec![
|
||||
" ███ ",
|
||||
"█1█ █2█ ",
|
||||
"foo bar ",
|
||||
])
|
||||
);
|
||||
#[rustfmt::skip]
|
||||
let expected = Buffer::with_lines([
|
||||
" ███ ",
|
||||
"█1█ █2█ ",
|
||||
"foo bar ",
|
||||
]);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bar_gap() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
|
||||
let widget = BarChart::default()
|
||||
.data(&[("foo", 1), ("bar", 2)])
|
||||
.bar_gap(2);
|
||||
widget.render(buffer.area, &mut buffer);
|
||||
assert_buffer_eq!(
|
||||
buffer,
|
||||
Buffer::with_lines(vec![
|
||||
" █ ",
|
||||
"1 2 ",
|
||||
"f b ",
|
||||
])
|
||||
);
|
||||
#[rustfmt::skip]
|
||||
let expected = Buffer::with_lines([
|
||||
" █ ",
|
||||
"1 2 ",
|
||||
"f b ",
|
||||
]);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bar_set() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
|
||||
let widget = BarChart::default()
|
||||
.data(&[("foo", 0), ("bar", 1), ("baz", 3)])
|
||||
.bar_set(symbols::bar::THREE_LEVELS);
|
||||
widget.render(buffer.area, &mut buffer);
|
||||
assert_buffer_eq!(
|
||||
buffer,
|
||||
Buffer::with_lines(vec![
|
||||
" █ ",
|
||||
" ▄ 3 ",
|
||||
"f b b ",
|
||||
])
|
||||
);
|
||||
#[rustfmt::skip]
|
||||
let expected = Buffer::with_lines([
|
||||
" █ ",
|
||||
" ▄ 3 ",
|
||||
"f b b ",
|
||||
]);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -779,67 +768,68 @@ mod tests {
|
||||
])
|
||||
.bar_set(symbols::bar::NINE_LEVELS);
|
||||
widget.render(Rect::new(0, 1, 18, 2), &mut buffer);
|
||||
assert_buffer_eq!(
|
||||
buffer,
|
||||
Buffer::with_lines(vec![
|
||||
" ",
|
||||
" ▁ ▂ ▃ ▄ ▅ ▆ ▇ 8 ",
|
||||
"a b c d e f g h i ",
|
||||
])
|
||||
);
|
||||
let expected = Buffer::with_lines([
|
||||
" ",
|
||||
" ▁ ▂ ▃ ▄ ▅ ▆ ▇ 8 ",
|
||||
"a b c d e f g h i ",
|
||||
]);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn value_style() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
|
||||
let widget = BarChart::default()
|
||||
.data(&[("foo", 1), ("bar", 2)])
|
||||
.bar_width(3)
|
||||
.value_style(Style::new().red());
|
||||
widget.render(buffer.area, &mut buffer);
|
||||
let mut expected = Buffer::with_lines(vec![
|
||||
" ███ ",
|
||||
"█1█ █2█ ",
|
||||
"foo bar ",
|
||||
#[rustfmt::skip]
|
||||
let mut expected = Buffer::with_lines([
|
||||
" ███ ",
|
||||
"█1█ █2█ ",
|
||||
"foo bar ",
|
||||
]);
|
||||
expected.get_mut(1, 1).set_fg(Color::Red);
|
||||
expected.get_mut(5, 1).set_fg(Color::Red);
|
||||
assert_buffer_eq!(buffer, expected);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn label_style() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
|
||||
let widget = BarChart::default()
|
||||
.data(&[("foo", 1), ("bar", 2)])
|
||||
.label_style(Style::new().red());
|
||||
widget.render(buffer.area, &mut buffer);
|
||||
let mut expected = Buffer::with_lines(vec![
|
||||
" █ ",
|
||||
"1 2 ",
|
||||
"f b ",
|
||||
#[rustfmt::skip]
|
||||
let mut expected = Buffer::with_lines([
|
||||
" █ ",
|
||||
"1 2 ",
|
||||
"f b ",
|
||||
]);
|
||||
expected.get_mut(0, 2).set_fg(Color::Red);
|
||||
expected.get_mut(2, 2).set_fg(Color::Red);
|
||||
assert_buffer_eq!(buffer, expected);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn style() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
|
||||
let widget = BarChart::default()
|
||||
.data(&[("foo", 1), ("bar", 2)])
|
||||
.style(Style::new().red());
|
||||
widget.render(buffer.area, &mut buffer);
|
||||
let mut expected = Buffer::with_lines(vec![
|
||||
" █ ",
|
||||
"1 2 ",
|
||||
"f b ",
|
||||
#[rustfmt::skip]
|
||||
let mut expected = Buffer::with_lines([
|
||||
" █ ",
|
||||
"1 2 ",
|
||||
"f b ",
|
||||
]);
|
||||
for (x, y) in iproduct!(0..15, 0..3) {
|
||||
for (x, y) in iproduct!(0..10, 0..3) {
|
||||
expected.get_mut(x, y).set_fg(Color::Red);
|
||||
}
|
||||
assert_buffer_eq!(buffer, expected);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -865,8 +855,13 @@ mod tests {
|
||||
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 3, 3));
|
||||
chart.render(buffer.area, &mut buffer);
|
||||
let expected = Buffer::with_lines(vec![" █", "1 2", "G "]);
|
||||
assert_buffer_eq!(buffer, expected);
|
||||
#[rustfmt::skip]
|
||||
let expected = Buffer::with_lines([
|
||||
" █",
|
||||
"1 2",
|
||||
"G ",
|
||||
]);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
fn build_test_barchart<'a>() -> BarChart<'a> {
|
||||
@@ -892,7 +887,7 @@ mod tests {
|
||||
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 5, 8));
|
||||
chart.render(buffer.area, &mut buffer);
|
||||
let expected = Buffer::with_lines(vec![
|
||||
let expected = Buffer::with_lines([
|
||||
"2█ ",
|
||||
"3██ ",
|
||||
"4███ ",
|
||||
@@ -902,8 +897,7 @@ mod tests {
|
||||
"5████",
|
||||
"G2 ",
|
||||
]);
|
||||
|
||||
assert_buffer_eq!(buffer, expected);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -912,7 +906,7 @@ mod tests {
|
||||
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 5, 7));
|
||||
chart.render(buffer.area, &mut buffer);
|
||||
let expected = Buffer::with_lines(vec![
|
||||
let expected = Buffer::with_lines([
|
||||
"2█ ",
|
||||
"3██ ",
|
||||
"4███ ",
|
||||
@@ -921,8 +915,7 @@ mod tests {
|
||||
"4███ ",
|
||||
"5████",
|
||||
]);
|
||||
|
||||
assert_buffer_eq!(buffer, expected);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -931,9 +924,15 @@ mod tests {
|
||||
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 5, 5));
|
||||
chart.render(buffer.area, &mut buffer);
|
||||
let expected = Buffer::with_lines(vec!["2█ ", "3██ ", "4███ ", "G1 ", "3██ "]);
|
||||
|
||||
assert_buffer_eq!(buffer, expected);
|
||||
#[rustfmt::skip]
|
||||
let expected = Buffer::with_lines([
|
||||
"2█ ",
|
||||
"3██ ",
|
||||
"4███ ",
|
||||
"G1 ",
|
||||
"3██ ",
|
||||
]);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
fn test_horizontal_bars_label_width_greater_than_bar(bar_color: Option<Color>) {
|
||||
@@ -956,7 +955,7 @@ mod tests {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 5, 2));
|
||||
chart.render(buffer.area, &mut buffer);
|
||||
|
||||
let mut expected = Buffer::with_lines(vec!["label", "5████"]);
|
||||
let mut expected = Buffer::with_lines(["label", "5████"]);
|
||||
|
||||
// first line has a yellow foreground. first cell contains italic "5"
|
||||
expected.get_mut(0, 1).modifier.insert(Modifier::ITALIC);
|
||||
@@ -964,11 +963,7 @@ mod tests {
|
||||
expected.get_mut(x, 1).set_fg(Color::Yellow);
|
||||
}
|
||||
|
||||
let expected_color = if let Some(color) = bar_color {
|
||||
color
|
||||
} else {
|
||||
Color::Yellow
|
||||
};
|
||||
let expected_color = bar_color.unwrap_or(Color::Yellow);
|
||||
|
||||
// second line contains the word "label". Since the bar value is 2,
|
||||
// then the first 2 characters of "label" are italic red.
|
||||
@@ -981,7 +976,7 @@ mod tests {
|
||||
expected.get_mut(3, 0).set_fg(expected_color);
|
||||
expected.get_mut(4, 0).set_fg(expected_color);
|
||||
|
||||
assert_buffer_eq!(buffer, expected);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1004,9 +999,13 @@ mod tests {
|
||||
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
|
||||
chart.render(buffer.area, &mut buffer);
|
||||
let expected = Buffer::with_lines(vec!["Jan 10█ ", "Feb 20████", "Mar 5 "]);
|
||||
|
||||
assert_buffer_eq!(buffer, expected);
|
||||
#[rustfmt::skip]
|
||||
let expected = Buffer::with_lines([
|
||||
"Jan 10█ ",
|
||||
"Feb 20████",
|
||||
"Mar 5 ",
|
||||
]);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1027,13 +1026,13 @@ mod tests {
|
||||
// G1 should have the bold red style
|
||||
// bold: because of BarChart::label_style
|
||||
// red: is included with the label itself
|
||||
let mut expected = Buffer::with_lines(vec!["2████", "G1 "]);
|
||||
let mut expected = Buffer::with_lines(["2████", "G1 "]);
|
||||
let cell = expected.get_mut(0, 1).set_fg(Color::Red);
|
||||
cell.modifier.insert(Modifier::BOLD);
|
||||
let cell = expected.get_mut(1, 1).set_fg(Color::Red);
|
||||
cell.modifier.insert(Modifier::BOLD);
|
||||
|
||||
assert_buffer_eq!(buffer, expected);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1050,17 +1049,14 @@ mod tests {
|
||||
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 13, 5));
|
||||
chart.render(buffer.area, &mut buffer);
|
||||
|
||||
assert_buffer_eq!(
|
||||
buffer,
|
||||
Buffer::with_lines(vec![
|
||||
" ▂ █ ▂",
|
||||
" ▄ █ █ ▄ █",
|
||||
"▆ 2 3 4 ▆ 2 3",
|
||||
"a b c c a b c",
|
||||
" G1 G2 ",
|
||||
])
|
||||
);
|
||||
let expected = Buffer::with_lines([
|
||||
" ▂ █ ▂",
|
||||
" ▄ █ █ ▄ █",
|
||||
"▆ 2 3 4 ▆ 2 3",
|
||||
"a b c c a b c",
|
||||
" G1 G2 ",
|
||||
]);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1073,9 +1069,13 @@ mod tests {
|
||||
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 3, 3));
|
||||
chart.render(buffer.area, &mut buffer);
|
||||
|
||||
let expected = Buffer::with_lines(vec![" █", "▆ 5", " G"]);
|
||||
assert_buffer_eq!(buffer, expected);
|
||||
#[rustfmt::skip]
|
||||
let expected = Buffer::with_lines([
|
||||
" █",
|
||||
"▆ 5",
|
||||
" G",
|
||||
]);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1098,15 +1098,14 @@ mod tests {
|
||||
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 11, 5));
|
||||
chart.render(buffer.area, &mut buffer);
|
||||
|
||||
let expected = Buffer::with_lines(vec![
|
||||
let expected = Buffer::with_lines([
|
||||
" ▆▆▆ ███",
|
||||
" ███ ███",
|
||||
"▃▃▃ ███ ███",
|
||||
"写█ 写█ 写█",
|
||||
"B1 B2 B2 ",
|
||||
]);
|
||||
assert_buffer_eq!(buffer, expected);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1118,7 +1117,7 @@ mod tests {
|
||||
.bar_gap(0);
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 0, 10));
|
||||
chart.render(buffer.area, &mut buffer);
|
||||
assert_buffer_eq!(buffer, Buffer::empty(Rect::new(0, 0, 0, 10)));
|
||||
assert_eq!(buffer, Buffer::empty(Rect::new(0, 0, 0, 10)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1143,8 +1142,7 @@ mod tests {
|
||||
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 17, 1));
|
||||
chart.render(buffer.area, &mut buffer);
|
||||
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" ▁ ▂ ▃ ▄ ▅ ▆ ▇ 8"]));
|
||||
assert_eq!(buffer, Buffer::with_lines([" ▁ ▂ ▃ ▄ ▅ ▆ ▇ 8"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1169,15 +1167,12 @@ mod tests {
|
||||
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 17, 3));
|
||||
chart.render(Rect::new(0, 1, buffer.area.width, 2), &mut buffer);
|
||||
|
||||
assert_buffer_eq!(
|
||||
buffer,
|
||||
Buffer::with_lines(vec![
|
||||
" ",
|
||||
" ▁ ▂ ▃ ▄ ▅ ▆ ▇ 8",
|
||||
"a b c d e f g h i",
|
||||
])
|
||||
);
|
||||
let expected = Buffer::with_lines([
|
||||
" ",
|
||||
" ▁ ▂ ▃ ▄ ▅ ▆ ▇ 8",
|
||||
"a b c d e f g h i",
|
||||
]);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1202,15 +1197,12 @@ mod tests {
|
||||
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 17, 3));
|
||||
chart.render(buffer.area, &mut buffer);
|
||||
|
||||
assert_buffer_eq!(
|
||||
buffer,
|
||||
Buffer::with_lines(vec![
|
||||
" ▁ ▂ ▃ ▄ ▅ ▆ ▇ 8",
|
||||
"a b c d e f g h i",
|
||||
" Group ",
|
||||
])
|
||||
);
|
||||
let expected = Buffer::with_lines([
|
||||
" ▁ ▂ ▃ ▄ ▅ ▆ ▇ 8",
|
||||
"a b c d e f g h i",
|
||||
" Group ",
|
||||
]);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1235,15 +1227,12 @@ mod tests {
|
||||
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 26, 3));
|
||||
chart.render(buffer.area, &mut buffer);
|
||||
|
||||
assert_buffer_eq!(
|
||||
buffer,
|
||||
Buffer::with_lines(vec![
|
||||
" 1▁ 2▂ 3▃ 4▄ 5▅ 6▆ 7▇ 8█",
|
||||
"a b c d e f g h i ",
|
||||
" Group ",
|
||||
])
|
||||
);
|
||||
let expected = Buffer::with_lines([
|
||||
" 1▁ 2▂ 3▃ 4▄ 5▅ 6▆ 7▇ 8█",
|
||||
"a b c d e f g h i ",
|
||||
" Group ",
|
||||
]);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1268,16 +1257,13 @@ mod tests {
|
||||
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 17, 4));
|
||||
chart.render(buffer.area, &mut buffer);
|
||||
|
||||
assert_buffer_eq!(
|
||||
buffer,
|
||||
Buffer::with_lines(vec![
|
||||
" ▂ ▄ ▆ █",
|
||||
" ▂ ▄ ▆ 4 5 6 7 8",
|
||||
"a b c d e f g h i",
|
||||
" Group ",
|
||||
])
|
||||
);
|
||||
let expected = Buffer::with_lines([
|
||||
" ▂ ▄ ▆ █",
|
||||
" ▂ ▄ ▆ 4 5 6 7 8",
|
||||
"a b c d e f g h i",
|
||||
" Group ",
|
||||
]);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1300,15 +1286,12 @@ mod tests {
|
||||
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 17, 3));
|
||||
chart.render(Rect::new(0, 1, buffer.area.width, 2), &mut buffer);
|
||||
|
||||
assert_buffer_eq!(
|
||||
buffer,
|
||||
Buffer::with_lines(vec![
|
||||
" ",
|
||||
" ▁ ▂ ▃ ▄ ▅ ▆ ▇ 8",
|
||||
" Group ",
|
||||
])
|
||||
);
|
||||
let expected = Buffer::with_lines([
|
||||
" ",
|
||||
" ▁ ▂ ▃ ▄ ▅ ▆ ▇ 8",
|
||||
" Group ",
|
||||
]);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1319,13 +1302,9 @@ mod tests {
|
||||
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 59, 1));
|
||||
chart.render(buffer.area, &mut buffer);
|
||||
|
||||
assert_buffer_eq!(
|
||||
buffer,
|
||||
Buffer::with_lines(vec![
|
||||
" ▁ ▁ ▁ ▁ ▂ ▂ ▂ ▃ ▃ ▃ ▃ ▄ ▄ ▄ ▄ ▅ ▅ ▅ ▆ ▆ ▆ ▆ ▇ ▇ ▇ █",
|
||||
])
|
||||
);
|
||||
let expected =
|
||||
Buffer::with_lines([" ▁ ▁ ▁ ▁ ▂ ▂ ▂ ▃ ▃ ▃ ▃ ▄ ▄ ▄ ▄ ▅ ▅ ▅ ▆ ▆ ▆ ▆ ▇ ▇ ▇ █"]);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1337,17 +1316,14 @@ mod tests {
|
||||
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 7, 6));
|
||||
chart.render(buffer.area, &mut buffer);
|
||||
|
||||
assert_buffer_eq!(
|
||||
buffer,
|
||||
Buffer::with_lines(vec![
|
||||
" ██ ",
|
||||
" ██ ",
|
||||
"▄▄ ██ ",
|
||||
"██ ██ ",
|
||||
"1█ 2█ ",
|
||||
"a b ",
|
||||
])
|
||||
);
|
||||
let expected = Buffer::with_lines([
|
||||
" ██ ",
|
||||
" ██ ",
|
||||
"▄▄ ██ ",
|
||||
"██ ██ ",
|
||||
"1█ 2█ ",
|
||||
"a b ",
|
||||
]);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -35,6 +35,14 @@ pub struct Padding {
|
||||
}
|
||||
|
||||
impl Padding {
|
||||
/// `Padding` with all fields set to `0`
|
||||
pub const ZERO: Self = Self {
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
};
|
||||
|
||||
/// Creates a new `Padding` by specifying every field individually.
|
||||
///
|
||||
/// Note: the order of the fields does not match the order of the CSS properties.
|
||||
@@ -48,13 +56,9 @@ impl Padding {
|
||||
}
|
||||
|
||||
/// Creates a `Padding` with all fields set to `0`.
|
||||
#[deprecated = "use Padding::ZERO"]
|
||||
pub const fn zero() -> Self {
|
||||
Self {
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
}
|
||||
Self::ZERO
|
||||
}
|
||||
|
||||
/// Creates a `Padding` with the same value for `left` and `right`.
|
||||
@@ -173,7 +177,6 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn constructors() {
|
||||
assert_eq!(Padding::zero(), Padding::new(0, 0, 0, 0));
|
||||
assert_eq!(Padding::horizontal(1), Padding::new(1, 1, 0, 0));
|
||||
assert_eq!(Padding::vertical(1), Padding::new(0, 0, 1, 1));
|
||||
assert_eq!(Padding::uniform(1), Padding::new(1, 1, 1, 1));
|
||||
@@ -189,7 +192,6 @@ mod tests {
|
||||
const fn can_be_const() {
|
||||
const _PADDING: Padding = Padding::new(1, 1, 1, 1);
|
||||
const _UNI_PADDING: Padding = Padding::uniform(1);
|
||||
const _NO_PADDING: Padding = Padding::zero();
|
||||
const _HORIZONTAL: Padding = Padding::horizontal(1);
|
||||
const _VERTICAL: Padding = Padding::vertical(1);
|
||||
const _PROPORTIONAL: Padding = Padding::proportional(1);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::fmt::{self, Debug};
|
||||
use std::fmt;
|
||||
|
||||
use bitflags::bitflags;
|
||||
|
||||
@@ -24,7 +24,7 @@ bitflags! {
|
||||
/// Implement the `Debug` trait for the `Borders` bitflags. This is a manual implementation to
|
||||
/// display the flags in a more readable way. The default implementation would display the
|
||||
/// flags as 'Border(0x0)' for `Borders::NONE` for example.
|
||||
impl Debug for Borders {
|
||||
impl fmt::Debug for Borders {
|
||||
/// Display the Borders bitflags as a list of names. For example, `Borders::NONE` will be
|
||||
/// displayed as `NONE` and `Borders::ALL` will be displayed as `ALL`. If multiple flags are
|
||||
/// set, they will be displayed separated by a pipe character.
|
||||
|
||||
@@ -28,14 +28,14 @@ pub struct Monthly<'a, DS: DateStyler> {
|
||||
|
||||
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: DS) -> Self {
|
||||
pub const fn new(display_date: Date, events: DS) -> Self {
|
||||
Self {
|
||||
display_date,
|
||||
events,
|
||||
show_surrounding: None,
|
||||
show_weekday: None,
|
||||
show_month: None,
|
||||
default_style: Style::default(),
|
||||
default_style: Style::new(),
|
||||
block: None,
|
||||
}
|
||||
}
|
||||
@@ -90,10 +90,10 @@ impl<'a, DS: DateStyler> Monthly<'a, DS> {
|
||||
}
|
||||
|
||||
/// Return a style with only the background from the default style
|
||||
fn default_bg(&self) -> Style {
|
||||
const fn default_bg(&self) -> Style {
|
||||
match self.default_style.bg {
|
||||
None => Style::default(),
|
||||
Some(c) => Style::default().bg(c),
|
||||
None => Style::new(),
|
||||
Some(c) => Style::new().bg(c),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,7 +164,7 @@ impl<DS: DateStyler> Monthly<'_, DS> {
|
||||
|
||||
let mut y = days_area.y;
|
||||
// go through all the weeks containing a day in the target month.
|
||||
while curr_day.month() as u8 != self.display_date.month().next() as u8 {
|
||||
while curr_day.month() != self.display_date.month().next() {
|
||||
let mut spans = Vec::with_capacity(14);
|
||||
for i in 0..7 {
|
||||
// Draw the gutter. Do it here so we can avoid worrying about
|
||||
@@ -203,7 +203,7 @@ impl CalendarEventStore {
|
||||
let mut res = Self::default();
|
||||
res.add(
|
||||
OffsetDateTime::now_local()
|
||||
.unwrap_or(OffsetDateTime::now_utc())
|
||||
.unwrap_or_else(|_| OffsetDateTime::now_utc())
|
||||
.date(),
|
||||
style.into(),
|
||||
);
|
||||
|
||||
@@ -19,7 +19,7 @@ mod points;
|
||||
mod rectangle;
|
||||
mod world;
|
||||
|
||||
use std::{fmt::Debug, iter::zip};
|
||||
use std::{fmt, iter::zip};
|
||||
|
||||
use itertools::Itertools;
|
||||
|
||||
@@ -70,7 +70,7 @@ struct Layer {
|
||||
/// resolution of the grid might exceed the number of rows and columns. For example, a grid of
|
||||
/// Braille patterns will have a resolution of 2x4 dots per cell. This means that a grid of 10x10
|
||||
/// cells will have a resolution of 20x40 dots.
|
||||
trait Grid: Debug {
|
||||
trait Grid: fmt::Debug {
|
||||
/// Get the resolution of the grid in number of dots.
|
||||
///
|
||||
/// This doesn't have to be the same as the number of rows and columns of the grid. For example,
|
||||
@@ -567,7 +567,7 @@ impl<'a> Context<'a> {
|
||||
/// };
|
||||
///
|
||||
/// Canvas::default()
|
||||
/// .block(Block::default().title("Canvas").borders(Borders::ALL))
|
||||
/// .block(Block::bordered().title("Canvas"))
|
||||
/// .x_bounds([-180.0, 180.0])
|
||||
/// .y_bounds([-90.0, 90.0])
|
||||
/// .paint(|ctx| {
|
||||
@@ -817,9 +817,7 @@ mod tests {
|
||||
// results in the expected output
|
||||
fn test_marker(marker: Marker, expected: &str) {
|
||||
let area = Rect::new(0, 0, 5, 5);
|
||||
let mut cell = Cell::default();
|
||||
cell.set_char('x');
|
||||
let mut buf = Buffer::filled(area, &cell);
|
||||
let mut buf = Buffer::filled(area, Cell::new("x"));
|
||||
let horizontal_line = Line {
|
||||
x1: 0.0,
|
||||
y1: 0.0,
|
||||
|
||||
@@ -58,7 +58,7 @@ mod tests {
|
||||
.x_bounds([-10.0, 10.0])
|
||||
.y_bounds([-10.0, 10.0]);
|
||||
canvas.render(buffer.area, &mut buffer);
|
||||
let expected = Buffer::with_lines(vec![
|
||||
let expected = Buffer::with_lines([
|
||||
" ⢀⣠⢤⣀ ",
|
||||
" ⢰⠋ ⠈⣇",
|
||||
" ⠘⣆⡀ ⣠⠇",
|
||||
|
||||
@@ -112,154 +112,111 @@ fn draw_line_high(painter: &mut Painter, x1: usize, y1: usize, x2: usize, y2: us
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Line;
|
||||
use crate::{assert_buffer_eq, prelude::*, widgets::canvas::Canvas};
|
||||
use rstest::rstest;
|
||||
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
#[track_caller]
|
||||
fn test(line: Line, expected_lines: Vec<&str>) {
|
||||
use super::{super::*, *};
|
||||
use crate::{buffer::Buffer, layout::Rect};
|
||||
|
||||
#[rstest]
|
||||
#[case::off_grid(&Line::new(-1.0, -1.0, 10.0, 10.0, Color::Red), [" "; 10])]
|
||||
#[case::off_grid(&Line::new(0.0, 0.0, 11.0, 11.0, Color::Red), [" "; 10])]
|
||||
#[case::horizontal(&Line::new(0.0, 0.0, 10.0, 0.0, Color::Red), [
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
"••••••••••",
|
||||
])]
|
||||
#[case::horizontal(&Line::new(10.0, 10.0, 0.0, 10.0, Color::Red), [
|
||||
"••••••••••",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
])]
|
||||
#[case::vertical(&Line::new(0.0, 0.0, 0.0, 10.0, Color::Red), ["• "; 10])]
|
||||
#[case::vertical(&Line::new(10.0, 10.0, 10.0, 0.0, Color::Red), [" •"; 10])]
|
||||
// dy < dx, x1 < x2
|
||||
#[case::diagonal(&Line::new(0.0, 0.0, 10.0, 5.0, Color::Red), [
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" •",
|
||||
" •• ",
|
||||
" •• ",
|
||||
" •• ",
|
||||
" •• ",
|
||||
"• ",
|
||||
])]
|
||||
// dy < dx, x1 > x2
|
||||
#[case::diagonal(&Line::new(10.0, 0.0, 0.0, 5.0, Color::Red), [
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
"• ",
|
||||
" •• ",
|
||||
" •• ",
|
||||
" •• ",
|
||||
" •• ",
|
||||
" •",
|
||||
])]
|
||||
// dy > dx, y1 < y2
|
||||
#[case::diagonal(&Line::new(0.0, 0.0, 5.0, 10.0, Color::Red), [
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
"• ",
|
||||
"• ",
|
||||
])]
|
||||
// dy > dx, y1 > y2
|
||||
#[case::diagonal(&Line::new(0.0, 10.0, 5.0, 0.0, Color::Red), [
|
||||
"• ",
|
||||
"• ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
])]
|
||||
fn tests<'expected_line, ExpectedLines>(#[case] line: &Line, #[case] expected: ExpectedLines)
|
||||
where
|
||||
ExpectedLines: IntoIterator,
|
||||
ExpectedLines::Item: Into<crate::text::Line<'expected_line>>,
|
||||
{
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 10));
|
||||
let canvas = Canvas::default()
|
||||
.marker(Marker::Dot)
|
||||
.x_bounds([0.0, 10.0])
|
||||
.y_bounds([0.0, 10.0])
|
||||
.paint(|context| {
|
||||
context.draw(&line);
|
||||
});
|
||||
.paint(|context| context.draw(line));
|
||||
canvas.render(buffer.area, &mut buffer);
|
||||
|
||||
let mut expected = Buffer::with_lines(expected_lines);
|
||||
let mut expected = Buffer::with_lines(expected);
|
||||
for cell in &mut expected.content {
|
||||
if cell.symbol() == "•" {
|
||||
cell.set_style(Style::new().red());
|
||||
}
|
||||
}
|
||||
assert_buffer_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn off_grid() {
|
||||
test(
|
||||
Line::new(-1.0, -1.0, 10.0, 10.0, Color::Red),
|
||||
vec![" "; 10],
|
||||
);
|
||||
test(
|
||||
Line::new(0.0, 0.0, 11.0, 11.0, Color::Red),
|
||||
vec![" "; 10],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn horizontal() {
|
||||
test(
|
||||
Line::new(0.0, 0.0, 10.0, 0.0, Color::Red),
|
||||
vec![
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
"••••••••••",
|
||||
],
|
||||
);
|
||||
test(
|
||||
Line::new(10.0, 10.0, 0.0, 10.0, Color::Red),
|
||||
vec![
|
||||
"••••••••••",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vertical() {
|
||||
test(
|
||||
Line::new(0.0, 0.0, 0.0, 10.0, Color::Red),
|
||||
vec!["• "; 10],
|
||||
);
|
||||
test(
|
||||
Line::new(10.0, 10.0, 10.0, 0.0, Color::Red),
|
||||
vec![" •"; 10],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diagonal() {
|
||||
// dy < dx, x1 < x2
|
||||
test(
|
||||
Line::new(0.0, 0.0, 10.0, 5.0, Color::Red),
|
||||
vec![
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" •",
|
||||
" •• ",
|
||||
" •• ",
|
||||
" •• ",
|
||||
" •• ",
|
||||
"• ",
|
||||
],
|
||||
);
|
||||
// dy < dx, x1 > x2
|
||||
test(
|
||||
Line::new(10.0, 0.0, 0.0, 5.0, Color::Red),
|
||||
vec![
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
"• ",
|
||||
" •• ",
|
||||
" •• ",
|
||||
" •• ",
|
||||
" •• ",
|
||||
" •",
|
||||
],
|
||||
);
|
||||
// dy > dx, y1 < y2
|
||||
test(
|
||||
Line::new(0.0, 0.0, 5.0, 10.0, Color::Red),
|
||||
vec![
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
"• ",
|
||||
"• ",
|
||||
],
|
||||
);
|
||||
// dy > dx, y1 > y2
|
||||
test(
|
||||
Line::new(0.0, 10.0, 5.0, 0.0, Color::Red),
|
||||
vec![
|
||||
"• ",
|
||||
"• ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
],
|
||||
);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ pub enum MapResolution {
|
||||
}
|
||||
|
||||
impl MapResolution {
|
||||
fn data(self) -> &'static [(f64, f64)] {
|
||||
const fn data(self) -> &'static [(f64, f64)] {
|
||||
match self {
|
||||
Self::Low => &WORLD_LOW_RESOLUTION,
|
||||
Self::High => &WORLD_HIGH_RESOLUTION,
|
||||
@@ -65,7 +65,7 @@ mod tests {
|
||||
use strum::ParseError;
|
||||
|
||||
use super::*;
|
||||
use crate::{assert_buffer_eq, prelude::*, widgets::canvas::Canvas};
|
||||
use crate::{prelude::*, widgets::canvas::Canvas};
|
||||
|
||||
#[test]
|
||||
fn map_resolution_to_string() {
|
||||
@@ -101,7 +101,7 @@ mod tests {
|
||||
context.draw(&Map::default());
|
||||
});
|
||||
canvas.render(buffer.area, &mut buffer);
|
||||
let expected = Buffer::with_lines(vec![
|
||||
let expected = Buffer::with_lines([
|
||||
" ",
|
||||
" ••••••• •• •• •• • ",
|
||||
" •••••••••••••• ••• •••• ••• •• •••• ",
|
||||
@@ -143,7 +143,7 @@ mod tests {
|
||||
" • ",
|
||||
" ",
|
||||
]);
|
||||
assert_buffer_eq!(buffer, expected);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -160,7 +160,7 @@ mod tests {
|
||||
});
|
||||
});
|
||||
canvas.render(buffer.area, &mut buffer);
|
||||
let expected = Buffer::with_lines(vec![
|
||||
let expected = Buffer::with_lines([
|
||||
" ",
|
||||
" ⢀⣠⠤⠤⠤⠔⢤⣤⡄⠤⡠⣄⠢⠂⢢⠰⣠⡄⣀⡀ ⣀ ",
|
||||
" ⢀⣀⡤⣦⠲⢶⣿⣮⣿⡉⣰⢶⢏⡂ ⢀⣟⠁ ⢺⣻⢿⠏ ⠈⠉⠁ ⢀⣀ ⠈⠓⢳⣢⣂⡀ ",
|
||||
@@ -202,6 +202,6 @@ mod tests {
|
||||
"⠶⠔⠲⠤⠠⠜⢗⠤⠄ ⠘⠉ ⠁ ⠈⠉⠒⠔⠤",
|
||||
" ",
|
||||
]);
|
||||
assert_buffer_eq!(buffer, expected);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ impl Shape for Rectangle {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{assert_buffer_eq, prelude::*, widgets::canvas::Canvas};
|
||||
use crate::{prelude::*, widgets::canvas::Canvas};
|
||||
|
||||
#[test]
|
||||
fn draw_block_lines() {
|
||||
@@ -85,7 +85,7 @@ mod tests {
|
||||
});
|
||||
});
|
||||
canvas.render(buffer.area, &mut buffer);
|
||||
let mut expected = Buffer::with_lines(vec![
|
||||
let mut expected = Buffer::with_lines([
|
||||
"██████████",
|
||||
"█ █",
|
||||
"█ █",
|
||||
@@ -98,8 +98,8 @@ mod tests {
|
||||
"██████████",
|
||||
]);
|
||||
expected.set_style(buffer.area, Style::new().red());
|
||||
expected.set_style(buffer.area.inner(&Margin::new(1, 1)), Style::reset());
|
||||
assert_buffer_eq!(buffer, expected);
|
||||
expected.set_style(buffer.area.inner(Margin::new(1, 1)), Style::reset());
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -119,7 +119,7 @@ mod tests {
|
||||
});
|
||||
});
|
||||
canvas.render(buffer.area, &mut buffer);
|
||||
let mut expected = Buffer::with_lines(vec![
|
||||
let mut expected = Buffer::with_lines([
|
||||
"█▀▀▀▀▀▀▀▀█",
|
||||
"█ █",
|
||||
"█ █",
|
||||
@@ -132,9 +132,9 @@ mod tests {
|
||||
"█▄▄▄▄▄▄▄▄█",
|
||||
]);
|
||||
expected.set_style(buffer.area, Style::new().red().on_red());
|
||||
expected.set_style(buffer.area.inner(&Margin::new(1, 0)), Style::reset().red());
|
||||
expected.set_style(buffer.area.inner(&Margin::new(1, 1)), Style::reset());
|
||||
assert_buffer_eq!(buffer, expected);
|
||||
expected.set_style(buffer.area.inner(Margin::new(1, 0)), Style::reset().red());
|
||||
expected.set_style(buffer.area.inner(Margin::new(1, 1)), Style::reset());
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -163,7 +163,7 @@ mod tests {
|
||||
});
|
||||
});
|
||||
canvas.render(buffer.area, &mut buffer);
|
||||
let mut expected = Buffer::with_lines(vec![
|
||||
let mut expected = Buffer::with_lines([
|
||||
"⡏⠉⠉⠉⠉⠉⠉⠉⠉⢹",
|
||||
"⡇⢠⠤⠤⠤⠤⠤⠤⡄⢸",
|
||||
"⡇⢸ ⡇⢸",
|
||||
@@ -176,8 +176,8 @@ mod tests {
|
||||
"⣇⣀⣀⣀⣀⣀⣀⣀⣀⣸",
|
||||
]);
|
||||
expected.set_style(buffer.area, Style::new().red());
|
||||
expected.set_style(buffer.area.inner(&Margin::new(1, 1)), Style::new().green());
|
||||
expected.set_style(buffer.area.inner(&Margin::new(2, 2)), Style::reset());
|
||||
assert_buffer_eq!(buffer, expected);
|
||||
expected.set_style(buffer.area.inner(Margin::new(1, 1)), Style::new().green());
|
||||
expected.set_style(buffer.area.inner(Margin::new(2, 2)), Style::reset());
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/// [Source data](http://www.gnuplotting.org/plotting-the-world-revisited)
|
||||
pub static WORLD_HIGH_RESOLUTION: [(f64, f64); 5125] = [
|
||||
pub const WORLD_HIGH_RESOLUTION: [(f64, f64); 5125] = [
|
||||
(-163.7128, -78.5956),
|
||||
(-163.1058, -78.2233),
|
||||
(-161.2451, -78.3801),
|
||||
@@ -5127,7 +5127,7 @@ pub static WORLD_HIGH_RESOLUTION: [(f64, f64); 5125] = [
|
||||
(180.0, -84.71338),
|
||||
];
|
||||
|
||||
pub static WORLD_LOW_RESOLUTION: [(f64, f64); 1166] = [
|
||||
pub const WORLD_LOW_RESOLUTION: [(f64, f64); 1166] = [
|
||||
(-92.32, 48.24),
|
||||
(-88.13, 48.92),
|
||||
(-83.11, 46.27),
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::{
|
||||
prelude::*,
|
||||
widgets::{
|
||||
canvas::{Canvas, Line as CanvasLine, Points},
|
||||
Block, Borders,
|
||||
Block,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -475,7 +475,7 @@ struct ChartLayout {
|
||||
///
|
||||
/// // Create the chart and link all the parts together
|
||||
/// let chart = Chart::new(datasets)
|
||||
/// .block(Block::default().title("Chart"))
|
||||
/// .block(Block::new().title("Chart"))
|
||||
/// .x_axis(x_axis)
|
||||
/// .y_axis(y_axis);
|
||||
/// ```
|
||||
@@ -1050,9 +1050,7 @@ impl WidgetRef for Chart<'_> {
|
||||
|
||||
if let Some(legend_area) = layout.legend_area {
|
||||
buf.set_style(legend_area, original_style);
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.render(legend_area, buf);
|
||||
Block::bordered().render(legend_area, buf);
|
||||
|
||||
for (i, (dataset_name, dataset_style)) in self
|
||||
.datasets
|
||||
@@ -1113,10 +1111,10 @@ impl<'a> Styled for Chart<'a> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rstest::rstest;
|
||||
use strum::ParseError;
|
||||
|
||||
use super::*;
|
||||
use crate::assert_buffer_eq;
|
||||
|
||||
struct LegendTestCase {
|
||||
chart_area: Rect,
|
||||
@@ -1211,7 +1209,6 @@ mod tests {
|
||||
.x_axis(Axis::default().title("xxxxxxxxxxxxxxxx"));
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 8, 4));
|
||||
widget.render(buffer.area, &mut buffer);
|
||||
|
||||
assert_eq!(buffer, Buffer::with_lines(vec![" ".repeat(8); 4]));
|
||||
}
|
||||
|
||||
@@ -1246,30 +1243,25 @@ mod tests {
|
||||
let widget = Chart::new(vec![long_dataset_name, short_dataset])
|
||||
.hidden_legend_constraints((100.into(), 100.into()));
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 20, 5));
|
||||
|
||||
widget.render(buffer.area, &mut buffer);
|
||||
|
||||
let expected = Buffer::with_lines(vec![
|
||||
let expected = Buffer::with_lines([
|
||||
" ┌──────────────┐",
|
||||
" │Very long name│",
|
||||
" │ Short name│",
|
||||
" └──────────────┘",
|
||||
" ",
|
||||
]);
|
||||
assert_buffer_eq!(buffer, expected);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chart_have_a_topleft_legend() {
|
||||
let chart = Chart::new(vec![Dataset::default().name("Ds1")])
|
||||
.legend_position(Some(LegendPosition::TopLeft));
|
||||
|
||||
let area = Rect::new(0, 0, 30, 20);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
|
||||
chart.render(buffer.area, &mut buffer);
|
||||
|
||||
let expected = Buffer::with_lines(vec![
|
||||
let expected = Buffer::with_lines([
|
||||
"┌───┐ ",
|
||||
"│Ds1│ ",
|
||||
"└───┘ ",
|
||||
@@ -1291,7 +1283,6 @@ mod tests {
|
||||
" ",
|
||||
" ",
|
||||
]);
|
||||
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
@@ -1299,13 +1290,10 @@ mod tests {
|
||||
fn test_chart_have_a_long_y_axis_title_overlapping_legend() {
|
||||
let chart = Chart::new(vec![Dataset::default().name("Ds1")])
|
||||
.y_axis(Axis::default().title("The title overlap a legend."));
|
||||
|
||||
let area = Rect::new(0, 0, 30, 20);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
|
||||
chart.render(buffer.area, &mut buffer);
|
||||
|
||||
let expected = Buffer::with_lines(vec![
|
||||
let expected = Buffer::with_lines([
|
||||
"The title overlap a legend. ",
|
||||
" ┌───┐",
|
||||
" │Ds1│",
|
||||
@@ -1327,7 +1315,6 @@ mod tests {
|
||||
" ",
|
||||
" ",
|
||||
]);
|
||||
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
@@ -1335,13 +1322,10 @@ mod tests {
|
||||
fn test_chart_have_overflowed_y_axis() {
|
||||
let chart = Chart::new(vec![Dataset::default().name("Ds1")])
|
||||
.y_axis(Axis::default().title("The title overlap a legend."));
|
||||
|
||||
let area = Rect::new(0, 0, 10, 10);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
|
||||
chart.render(buffer.area, &mut buffer);
|
||||
|
||||
let expected = Buffer::with_lines(vec![
|
||||
let expected = Buffer::with_lines([
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
@@ -1353,7 +1337,6 @@ mod tests {
|
||||
" ",
|
||||
" ",
|
||||
]);
|
||||
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
@@ -1362,12 +1345,8 @@ mod tests {
|
||||
let name = "Data";
|
||||
let chart = Chart::new(vec![Dataset::default().name(name)])
|
||||
.hidden_legend_constraints((Constraint::Percentage(100), Constraint::Percentage(100)));
|
||||
|
||||
let area = Rect::new(0, 0, name.len() as u16 + 2, 3);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
|
||||
let expected = Buffer::with_lines(vec!["┌────┐", "│Data│", "└────┘"]);
|
||||
|
||||
for position in [
|
||||
LegendPosition::TopLeft,
|
||||
LegendPosition::Top,
|
||||
@@ -1381,171 +1360,103 @@ mod tests {
|
||||
let chart = chart.clone().legend_position(Some(position));
|
||||
buffer.reset();
|
||||
chart.render(buffer.area, &mut buffer);
|
||||
#[rustfmt::skip]
|
||||
let expected = Buffer::with_lines([
|
||||
"┌────┐",
|
||||
"│Data│",
|
||||
"└────┘",
|
||||
]);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
#[test]
|
||||
fn test_legend_of_chart_have_odd_margin_size() {
|
||||
#[rstest]
|
||||
#[case(Some(LegendPosition::TopLeft), [
|
||||
"┌────┐ ",
|
||||
"│Data│ ",
|
||||
"└────┘ ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
])]
|
||||
#[case(Some(LegendPosition::Top), [
|
||||
" ┌────┐ ",
|
||||
" │Data│ ",
|
||||
" └────┘ ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
])]
|
||||
#[case(Some(LegendPosition::TopRight), [
|
||||
" ┌────┐",
|
||||
" │Data│",
|
||||
" └────┘",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
])]
|
||||
#[case(Some(LegendPosition::Left), [
|
||||
" ",
|
||||
"┌────┐ ",
|
||||
"│Data│ ",
|
||||
"└────┘ ",
|
||||
" ",
|
||||
" ",
|
||||
])]
|
||||
#[case(Some(LegendPosition::Right), [
|
||||
" ",
|
||||
" ┌────┐",
|
||||
" │Data│",
|
||||
" └────┘",
|
||||
" ",
|
||||
" ",
|
||||
])]
|
||||
#[case(Some(LegendPosition::BottomLeft), [
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
"┌────┐ ",
|
||||
"│Data│ ",
|
||||
"└────┘ ",
|
||||
])]
|
||||
#[case(Some(LegendPosition::Bottom), [
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ┌────┐ ",
|
||||
" │Data│ ",
|
||||
" └────┘ ",
|
||||
])]
|
||||
#[case(Some(LegendPosition::BottomRight), [
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ┌────┐",
|
||||
" │Data│",
|
||||
" └────┘",
|
||||
])]
|
||||
#[case(None, [
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
])]
|
||||
fn test_legend_of_chart_have_odd_margin_size<'line, Lines>(
|
||||
#[case] legend_position: Option<LegendPosition>,
|
||||
#[case] expected: Lines,
|
||||
) where
|
||||
Lines: IntoIterator,
|
||||
Lines::Item: Into<Line<'line>>,
|
||||
{
|
||||
let name = "Data";
|
||||
let base_chart = Chart::new(vec![Dataset::default().name(name)])
|
||||
.hidden_legend_constraints((Constraint::Percentage(100), Constraint::Percentage(100)));
|
||||
|
||||
let area = Rect::new(0, 0, name.len() as u16 + 2 + 3, 3 + 3);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
|
||||
let chart = base_chart
|
||||
.clone()
|
||||
.legend_position(Some(LegendPosition::TopLeft));
|
||||
buffer.reset();
|
||||
let chart = Chart::new(vec![Dataset::default().name(name)])
|
||||
.legend_position(legend_position)
|
||||
.hidden_legend_constraints((Constraint::Percentage(100), Constraint::Percentage(100)));
|
||||
chart.render(buffer.area, &mut buffer);
|
||||
assert_eq!(
|
||||
buffer,
|
||||
Buffer::with_lines(vec![
|
||||
"┌────┐ ",
|
||||
"│Data│ ",
|
||||
"└────┘ ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
])
|
||||
);
|
||||
buffer.reset();
|
||||
|
||||
let chart = base_chart
|
||||
.clone()
|
||||
.legend_position(Some(LegendPosition::Top));
|
||||
buffer.reset();
|
||||
chart.render(buffer.area, &mut buffer);
|
||||
assert_eq!(
|
||||
buffer,
|
||||
Buffer::with_lines(vec![
|
||||
" ┌────┐ ",
|
||||
" │Data│ ",
|
||||
" └────┘ ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
])
|
||||
);
|
||||
|
||||
let chart = base_chart
|
||||
.clone()
|
||||
.legend_position(Some(LegendPosition::TopRight));
|
||||
buffer.reset();
|
||||
chart.render(buffer.area, &mut buffer);
|
||||
assert_eq!(
|
||||
buffer,
|
||||
Buffer::with_lines(vec![
|
||||
" ┌────┐",
|
||||
" │Data│",
|
||||
" └────┘",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
])
|
||||
);
|
||||
|
||||
let chart = base_chart
|
||||
.clone()
|
||||
.legend_position(Some(LegendPosition::Left));
|
||||
buffer.reset();
|
||||
chart.render(buffer.area, &mut buffer);
|
||||
assert_eq!(
|
||||
buffer,
|
||||
Buffer::with_lines(vec![
|
||||
" ",
|
||||
"┌────┐ ",
|
||||
"│Data│ ",
|
||||
"└────┘ ",
|
||||
" ",
|
||||
" ",
|
||||
])
|
||||
);
|
||||
buffer.reset();
|
||||
|
||||
let chart = base_chart
|
||||
.clone()
|
||||
.legend_position(Some(LegendPosition::Right));
|
||||
buffer.reset();
|
||||
chart.render(buffer.area, &mut buffer);
|
||||
assert_eq!(
|
||||
buffer,
|
||||
Buffer::with_lines(vec![
|
||||
" ",
|
||||
" ┌────┐",
|
||||
" │Data│",
|
||||
" └────┘",
|
||||
" ",
|
||||
" ",
|
||||
])
|
||||
);
|
||||
|
||||
let chart = base_chart
|
||||
.clone()
|
||||
.legend_position(Some(LegendPosition::BottomLeft));
|
||||
buffer.reset();
|
||||
chart.render(buffer.area, &mut buffer);
|
||||
assert_eq!(
|
||||
buffer,
|
||||
Buffer::with_lines(vec![
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
"┌────┐ ",
|
||||
"│Data│ ",
|
||||
"└────┘ ",
|
||||
])
|
||||
);
|
||||
|
||||
let chart = base_chart
|
||||
.clone()
|
||||
.legend_position(Some(LegendPosition::Bottom));
|
||||
buffer.reset();
|
||||
chart.render(buffer.area, &mut buffer);
|
||||
assert_eq!(
|
||||
buffer,
|
||||
Buffer::with_lines(vec![
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ┌────┐ ",
|
||||
" │Data│ ",
|
||||
" └────┘ ",
|
||||
])
|
||||
);
|
||||
|
||||
let chart = base_chart
|
||||
.clone()
|
||||
.legend_position(Some(LegendPosition::BottomRight));
|
||||
buffer.reset();
|
||||
chart.render(buffer.area, &mut buffer);
|
||||
assert_eq!(
|
||||
buffer,
|
||||
Buffer::with_lines(vec![
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ┌────┐",
|
||||
" │Data│",
|
||||
" └────┘",
|
||||
])
|
||||
);
|
||||
|
||||
let chart = base_chart.clone().legend_position(None);
|
||||
buffer.reset();
|
||||
chart.render(buffer.area, &mut buffer);
|
||||
assert_eq!(
|
||||
buffer,
|
||||
Buffer::with_lines(vec![
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
])
|
||||
);
|
||||
assert_eq!(buffer, Buffer::with_lines(expected));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::prelude::*;
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
///
|
||||
/// fn draw_on_clear(f: &mut Frame, area: Rect) {
|
||||
/// let block = Block::default().title("Block").borders(Borders::ALL);
|
||||
/// let block = Block::bordered().title("Block");
|
||||
/// f.render_widget(Clear, area); // <- this will clear/reset the area first
|
||||
/// f.render_widget(block, area); // now render the block widget
|
||||
/// }
|
||||
@@ -43,24 +43,21 @@ impl WidgetRef for Clear {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::assert_buffer_eq;
|
||||
|
||||
#[test]
|
||||
fn render() {
|
||||
let mut buf = Buffer::with_lines(vec!["xxxxxxxxxxxxxxx"; 7]);
|
||||
let mut buffer = Buffer::with_lines(["xxxxxxxxxxxxxxx"; 7]);
|
||||
let clear = Clear;
|
||||
clear.render(Rect::new(1, 2, 3, 4), &mut buf);
|
||||
assert_buffer_eq!(
|
||||
buf,
|
||||
Buffer::with_lines(vec![
|
||||
"xxxxxxxxxxxxxxx",
|
||||
"xxxxxxxxxxxxxxx",
|
||||
"x xxxxxxxxxxx",
|
||||
"x xxxxxxxxxxx",
|
||||
"x xxxxxxxxxxx",
|
||||
"x xxxxxxxxxxx",
|
||||
"xxxxxxxxxxxxxxx",
|
||||
])
|
||||
);
|
||||
clear.render(Rect::new(1, 2, 3, 4), &mut buffer);
|
||||
let expected = Buffer::with_lines([
|
||||
"xxxxxxxxxxxxxxx",
|
||||
"xxxxxxxxxxxxxxx",
|
||||
"x xxxxxxxxxxx",
|
||||
"x xxxxxxxxxxx",
|
||||
"x xxxxxxxxxxx",
|
||||
"x xxxxxxxxxxx",
|
||||
"xxxxxxxxxxxxxxx",
|
||||
]);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,10 @@ use crate::{prelude::*, widgets::Block};
|
||||
/// A `Gauge` renders a bar filled according to the value given to [`Gauge::percent`] or
|
||||
/// [`Gauge::ratio`]. The bar width and height are defined by the [`Rect`] it is
|
||||
/// [rendered](Widget::render) in.
|
||||
///
|
||||
/// The associated label is always centered horizontally and vertically. If not set with
|
||||
/// [`Gauge::label`], the label is the percentage of the bar filled.
|
||||
///
|
||||
/// You might want to have a higher precision bar using [`Gauge::use_unicode`].
|
||||
///
|
||||
/// This can be useful to indicate the progression of a task, like a download.
|
||||
@@ -17,7 +19,7 @@ use crate::{prelude::*, widgets::Block};
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
///
|
||||
/// Gauge::default()
|
||||
/// .block(Block::default().borders(Borders::ALL).title("Progress"))
|
||||
/// .block(Block::bordered().title("Progress"))
|
||||
/// .gauge_style(
|
||||
/// Style::default()
|
||||
/// .fg(Color::White)
|
||||
@@ -30,7 +32,8 @@ use crate::{prelude::*, widgets::Block};
|
||||
/// # See also
|
||||
///
|
||||
/// - [`LineGauge`] for a thin progress bar
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[allow(clippy::struct_field_names)] // gauge_style needs to be differentiated to style
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
pub struct Gauge<'a> {
|
||||
block: Option<Block<'a>>,
|
||||
ratio: f64,
|
||||
@@ -40,19 +43,6 @@ pub struct Gauge<'a> {
|
||||
gauge_style: Style,
|
||||
}
|
||||
|
||||
impl<'a> Default for Gauge<'a> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
block: None,
|
||||
ratio: 0.0,
|
||||
label: None,
|
||||
use_unicode: false,
|
||||
style: Style::default(),
|
||||
gauge_style: Style::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Gauge<'a> {
|
||||
/// Surrounds the `Gauge` with a [`Block`].
|
||||
///
|
||||
@@ -164,12 +154,12 @@ impl WidgetRef for Gauge<'_> {
|
||||
buf.set_style(area, self.style);
|
||||
self.block.render_ref(area, buf);
|
||||
let inner = self.block.inner_if_some(area);
|
||||
self.render_gague(inner, buf);
|
||||
self.render_gauge(inner, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl Gauge<'_> {
|
||||
fn render_gague(&self, gauge_area: Rect, buf: &mut Buffer) {
|
||||
fn render_gauge(&self, gauge_area: Rect, buf: &mut Buffer) {
|
||||
if gauge_area.is_empty() {
|
||||
return;
|
||||
}
|
||||
@@ -234,14 +224,20 @@ fn get_unicode_block<'a>(frac: f64) -> &'a str {
|
||||
|
||||
/// A compact widget to display a progress bar over a single thin line.
|
||||
///
|
||||
/// This can be useful to indicate the progression of a task, like a download.
|
||||
///
|
||||
/// A `LineGauge` renders a thin line filled according to the value given to [`LineGauge::ratio`].
|
||||
/// Unlike [`Gauge`], only the width can be defined by the [rendering](Widget::render) [`Rect`].
|
||||
/// The height is always 1.
|
||||
/// The associated label is always left-aligned. If not set with [`LineGauge::label`], the label
|
||||
/// is the percentage of the bar filled.
|
||||
/// Unlike [`Gauge`], only the width can be defined by the [rendering](Widget::render) [`Rect`]. The
|
||||
/// height is always 1.
|
||||
///
|
||||
/// The associated label is always left-aligned. If not set with [`LineGauge::label`], the label is
|
||||
/// the percentage of the bar filled.
|
||||
///
|
||||
/// You can also set the symbols used to draw the bar with [`LineGauge::line_set`].
|
||||
///
|
||||
/// This can be useful to indicate the progression of a task, like a download.
|
||||
/// To style the gauge line use [`LineGauge::filled_style`] and [`LineGauge::unfilled_style`] which
|
||||
/// let you pick a color for foreground (i.e. line) and background of the filled and unfilled part
|
||||
/// of gauge respectively.
|
||||
///
|
||||
/// # Examples:
|
||||
///
|
||||
@@ -249,8 +245,8 @@ fn get_unicode_block<'a>(frac: f64) -> &'a str {
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
///
|
||||
/// LineGauge::default()
|
||||
/// .block(Block::default().borders(Borders::ALL).title("Progress"))
|
||||
/// .gauge_style(
|
||||
/// .block(Block::bordered().title("Progress"))
|
||||
/// .filled_style(
|
||||
/// Style::default()
|
||||
/// .fg(Color::White)
|
||||
/// .bg(Color::Black)
|
||||
@@ -270,7 +266,8 @@ pub struct LineGauge<'a> {
|
||||
label: Option<Line<'a>>,
|
||||
line_set: symbols::line::Set,
|
||||
style: Style,
|
||||
gauge_style: Style,
|
||||
filled_style: Style,
|
||||
unfilled_style: Style,
|
||||
}
|
||||
|
||||
impl<'a> LineGauge<'a> {
|
||||
@@ -342,9 +339,40 @@ impl<'a> LineGauge<'a> {
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
#[deprecated(
|
||||
since = "0.27.0",
|
||||
note = "You should use `LineGauge::filled_style` instead."
|
||||
)]
|
||||
#[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();
|
||||
let style: Style = style.into();
|
||||
|
||||
// maintain backward compatibility, which used the background color of the style as the
|
||||
// unfilled part of the gauge and the foreground color as the filled part of the gauge
|
||||
let filled_color = style.fg.unwrap_or(Color::Reset);
|
||||
let unfilled_color = style.bg.unwrap_or(Color::Reset);
|
||||
self.filled_style = style.fg(filled_color).bg(Color::Reset);
|
||||
self.unfilled_style = style.fg(unfilled_color).bg(Color::Reset);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of filled part of the bar.
|
||||
///
|
||||
/// `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 filled_style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.filled_style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the unfilled part of the bar.
|
||||
///
|
||||
/// `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 unfilled_style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.unfilled_style = style.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -378,26 +406,12 @@ impl WidgetRef for LineGauge<'_> {
|
||||
for col in start..end {
|
||||
buf.get_mut(col, row)
|
||||
.set_symbol(self.line_set.horizontal)
|
||||
.set_style(Style {
|
||||
fg: self.gauge_style.fg,
|
||||
bg: None,
|
||||
#[cfg(feature = "underline-color")]
|
||||
underline_color: self.gauge_style.underline_color,
|
||||
add_modifier: self.gauge_style.add_modifier,
|
||||
sub_modifier: self.gauge_style.sub_modifier,
|
||||
});
|
||||
.set_style(self.filled_style);
|
||||
}
|
||||
for col in end..gauge_area.right() {
|
||||
buf.get_mut(col, row)
|
||||
.set_symbol(self.line_set.horizontal)
|
||||
.set_style(Style {
|
||||
fg: self.gauge_style.bg,
|
||||
bg: None,
|
||||
#[cfg(feature = "underline-color")]
|
||||
underline_color: self.gauge_style.underline_color,
|
||||
add_modifier: self.gauge_style.add_modifier,
|
||||
sub_modifier: self.gauge_style.sub_modifier,
|
||||
});
|
||||
.set_style(self.unfilled_style);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -477,24 +491,36 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
#[test]
|
||||
fn line_gauge_can_be_stylized_with_deprecated_gauge_style() {
|
||||
let gauge =
|
||||
LineGauge::default().gauge_style(Style::default().fg(Color::Red).bg(Color::Blue));
|
||||
|
||||
assert_eq!(
|
||||
gauge.filled_style,
|
||||
Style::default().fg(Color::Red).bg(Color::Reset)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
gauge.unfilled_style,
|
||||
Style::default().fg(Color::Blue).bg(Color::Reset)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_gauge_default() {
|
||||
// TODO: replace to `assert_eq!(LineGauge::default(), LineGauge::default())`
|
||||
// when `Eq` or `PartialEq` is implemented for `LineGauge`.
|
||||
assert_eq!(
|
||||
format!("{:?}", LineGauge::default()),
|
||||
format!(
|
||||
"{:?}",
|
||||
LineGauge {
|
||||
block: None,
|
||||
ratio: 0.0,
|
||||
label: None,
|
||||
style: Style::default(),
|
||||
line_set: symbols::line::NORMAL,
|
||||
gauge_style: Style::default(),
|
||||
}
|
||||
),
|
||||
"LineGauge::default() should have correct default values."
|
||||
LineGauge::default(),
|
||||
LineGauge {
|
||||
block: None,
|
||||
ratio: 0.0,
|
||||
label: None,
|
||||
style: Style::default(),
|
||||
line_set: symbols::line::NORMAL,
|
||||
filled_style: Style::default(),
|
||||
unfilled_style: Style::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user