Compare commits
85 Commits
v0.27.0-al
...
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 | ||
|
|
26af65043e | ||
|
|
07da90a718 | ||
|
|
125ee929ee | ||
|
|
742a5ead06 | ||
|
|
8719608bda | ||
|
|
f6c4e447e6 | ||
|
|
c56f49b9fb | ||
|
|
078e97e4ff |
@@ -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
|
||||
* @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
FUNDING.json
Normal file
7
FUNDING.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"drips": {
|
||||
"ethereum": {
|
||||
"ownedBy": "0x6053C8984f4F214Ad12c4653F28514E1E09213B5"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
_ => {}
|
||||
},
|
||||
_ => {}
|
||||
@@ -282,7 +288,7 @@ impl App {
|
||||
|
||||
fn header() -> impl Widget {
|
||||
let text = "Constraint Explorer";
|
||||
text.bold().fg(Self::HEADER_COLOR).to_centered_line()
|
||||
text.bold().fg(Self::HEADER_COLOR).into_centered_line()
|
||||
}
|
||||
|
||||
fn instructions() -> impl Widget {
|
||||
@@ -436,7 +442,7 @@ impl ConstraintBlock {
|
||||
} else {
|
||||
main_color
|
||||
};
|
||||
Block::default()
|
||||
Block::new()
|
||||
.fg(Self::TEXT_COLOR)
|
||||
.bg(selected_color)
|
||||
.render(area, buf);
|
||||
@@ -541,7 +547,7 @@ impl SpacerBlock {
|
||||
/// A label that says "Spacer" if there is enough space
|
||||
fn spacer_label(width: u16) -> impl Widget {
|
||||
let label = if width >= 6 { "Spacer" } else { "" };
|
||||
label.fg(Self::TEXT_COLOR).to_centered_line()
|
||||
label.fg(Self::TEXT_COLOR).into_centered_line()
|
||||
}
|
||||
|
||||
/// A label that says "8 px" if there is enough space
|
||||
|
||||
@@ -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"),
|
||||
//! ]);
|
||||
|
||||
546
src/text/line.rs
546
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::*;
|
||||
@@ -48,6 +51,7 @@ use crate::prelude::*;
|
||||
/// - [`Line::reset_style`] resets the style of the line.
|
||||
/// - [`Line::width`] returns the unicode width of the content held by this line.
|
||||
/// - [`Line::styled_graphemes`] returns an iterator over the graphemes held by this line.
|
||||
/// - [`Line::push_span`] adds a span to the line.
|
||||
///
|
||||
/// # Compatibility Notes
|
||||
///
|
||||
@@ -451,6 +455,23 @@ impl<'a> Line<'a> {
|
||||
pub fn iter_mut(&mut self) -> std::slice::IterMut<Span<'a>> {
|
||||
self.spans.iter_mut()
|
||||
}
|
||||
|
||||
/// Adds a span to the line.
|
||||
///
|
||||
/// `span` can be any type that is convertible into a `Span`. For example, you can pass a
|
||||
/// `&str`, a `String`, or a `Span`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let mut line = Line::from("Hello, ");
|
||||
/// line.push_span(Span::raw("world!"));
|
||||
/// line.push_span(" How are you?");
|
||||
/// ```
|
||||
pub fn push_span<T: Into<Span<'a>>>(&mut self, span: T) {
|
||||
self.spans.push(span.into());
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for Line<'a> {
|
||||
@@ -534,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}")?;
|
||||
}
|
||||
@@ -587,6 +676,11 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[fixture]
|
||||
fn small_buf() -> Buffer {
|
||||
Buffer::empty(Rect::new(0, 0, 10, 1))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_str() {
|
||||
let line = Line::raw("test content");
|
||||
@@ -825,76 +919,6 @@ mod tests {
|
||||
assert_eq!(format!("{line_from_styled_span}"), "Hello, world!");
|
||||
}
|
||||
|
||||
mod widget {
|
||||
use super::*;
|
||||
use crate::assert_buffer_eq;
|
||||
const BLUE: Style = Style::new().fg(Color::Blue);
|
||||
const GREEN: Style = Style::new().fg(Color::Green);
|
||||
const ITALIC: Style = Style::new().add_modifier(Modifier::ITALIC);
|
||||
|
||||
fn hello_world() -> Line<'static> {
|
||||
Line::from(vec![
|
||||
Span::styled("Hello ", BLUE),
|
||||
Span::styled("world!", GREEN),
|
||||
])
|
||||
.style(ITALIC)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
|
||||
hello_world().render(Rect::new(0, 0, 15, 1), &mut buf);
|
||||
let mut expected = Buffer::with_lines(vec!["Hello world! "]);
|
||||
expected.set_style(Rect::new(0, 0, 15, 1), ITALIC);
|
||||
expected.set_style(Rect::new(0, 0, 6, 1), BLUE);
|
||||
expected.set_style(Rect::new(6, 0, 6, 1), GREEN);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_only_styles_line_area() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 1));
|
||||
hello_world().render(Rect::new(0, 0, 15, 1), &mut buf);
|
||||
let mut expected = Buffer::with_lines(vec!["Hello world! "]);
|
||||
expected.set_style(Rect::new(0, 0, 15, 1), ITALIC);
|
||||
expected.set_style(Rect::new(0, 0, 6, 1), BLUE);
|
||||
expected.set_style(Rect::new(6, 0, 6, 1), GREEN);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_truncates() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 1));
|
||||
Line::from("Hello world!").render(Rect::new(0, 0, 5, 1), &mut buf);
|
||||
let expected = Buffer::with_lines(vec!["Hello "]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_centered() {
|
||||
let line = hello_world().alignment(Alignment::Center);
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
|
||||
line.render(Rect::new(0, 0, 15, 1), &mut buf);
|
||||
let mut expected = Buffer::with_lines(vec![" Hello world! "]);
|
||||
expected.set_style(Rect::new(0, 0, 15, 1), ITALIC);
|
||||
expected.set_style(Rect::new(1, 0, 6, 1), BLUE);
|
||||
expected.set_style(Rect::new(7, 0, 6, 1), GREEN);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_right_aligned() {
|
||||
let line = hello_world().alignment(Alignment::Right);
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
|
||||
line.render(Rect::new(0, 0, 15, 1), &mut buf);
|
||||
let mut expected = Buffer::with_lines(vec![" Hello world!"]);
|
||||
expected.set_style(Rect::new(0, 0, 15, 1), ITALIC);
|
||||
expected.set_style(Rect::new(3, 0, 6, 1), BLUE);
|
||||
expected.set_style(Rect::new(9, 0, 6, 1), GREEN);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn left_aligned() {
|
||||
let line = Line::from("Hello, world!").left_aligned();
|
||||
@@ -913,6 +937,342 @@ mod tests {
|
||||
assert_eq!(line.alignment, Some(Alignment::Right));
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn push_span() {
|
||||
let mut line = Line::from("A");
|
||||
line.push_span(Span::raw("B"));
|
||||
line.push_span("C");
|
||||
assert_eq!(
|
||||
line.spans,
|
||||
vec![Span::raw("A"), Span::raw("B"), Span::raw("C")]
|
||||
);
|
||||
}
|
||||
|
||||
mod widget {
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use super::*;
|
||||
use crate::buffer::Cell;
|
||||
|
||||
const BLUE: Style = Style::new().fg(Color::Blue);
|
||||
const GREEN: Style = Style::new().fg(Color::Green);
|
||||
const ITALIC: Style = Style::new().add_modifier(Modifier::ITALIC);
|
||||
|
||||
#[fixture]
|
||||
fn hello_world() -> Line<'static> {
|
||||
Line::from(vec![
|
||||
Span::styled("Hello ", BLUE),
|
||||
Span::styled("world!", GREEN),
|
||||
])
|
||||
.style(ITALIC)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
|
||||
hello_world().render(Rect::new(0, 0, 15, 1), &mut buf);
|
||||
let mut expected = Buffer::with_lines(["Hello world! "]);
|
||||
expected.set_style(Rect::new(0, 0, 15, 1), ITALIC);
|
||||
expected.set_style(Rect::new(0, 0, 6, 1), BLUE);
|
||||
expected.set_style(Rect::new(6, 0, 6, 1), GREEN);
|
||||
assert_eq!(buf, expected);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn render_out_of_bounds(hello_world: Line<'static>, mut small_buf: Buffer) {
|
||||
let out_of_bounds = Rect::new(20, 20, 10, 1);
|
||||
hello_world.render(out_of_bounds, &mut small_buf);
|
||||
assert_eq!(small_buf, Buffer::empty(small_buf.area));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_only_styles_line_area() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 1));
|
||||
hello_world().render(Rect::new(0, 0, 15, 1), &mut buf);
|
||||
let mut expected = Buffer::with_lines(["Hello world! "]);
|
||||
expected.set_style(Rect::new(0, 0, 15, 1), ITALIC);
|
||||
expected.set_style(Rect::new(0, 0, 6, 1), BLUE);
|
||||
expected.set_style(Rect::new(6, 0, 6, 1), GREEN);
|
||||
assert_eq!(buf, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_truncates() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 1));
|
||||
Line::from("Hello world!").render(Rect::new(0, 0, 5, 1), &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["Hello "]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_centered() {
|
||||
let line = hello_world().alignment(Alignment::Center);
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
|
||||
line.render(Rect::new(0, 0, 15, 1), &mut buf);
|
||||
let mut expected = Buffer::with_lines([" Hello world! "]);
|
||||
expected.set_style(Rect::new(0, 0, 15, 1), ITALIC);
|
||||
expected.set_style(Rect::new(1, 0, 6, 1), BLUE);
|
||||
expected.set_style(Rect::new(7, 0, 6, 1), GREEN);
|
||||
assert_eq!(buf, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_right_aligned() {
|
||||
let line = hello_world().alignment(Alignment::Right);
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
|
||||
line.render(Rect::new(0, 0, 15, 1), &mut buf);
|
||||
let mut expected = Buffer::with_lines([" Hello world!"]);
|
||||
expected.set_style(Rect::new(0, 0, 15, 1), ITALIC);
|
||||
expected.set_style(Rect::new(3, 0, 6, 1), BLUE);
|
||||
expected.set_style(Rect::new(9, 0, 6, 1), GREEN);
|
||||
assert_eq!(buf, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_truncates_left() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 5, 1));
|
||||
Line::from("Hello world")
|
||||
.left_aligned()
|
||||
.render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["Hello"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_truncates_right() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 5, 1));
|
||||
Line::from("Hello world")
|
||||
.right_aligned()
|
||||
.render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["world"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_truncates_center() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 5, 1));
|
||||
Line::from("Hello world")
|
||||
.centered()
|
||||
.render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["lo wo"]));
|
||||
}
|
||||
|
||||
/// Part of a regression test for <https://github.com/ratatui-org/ratatui/issues/1032> which
|
||||
/// found panics with truncating lines that contained multi-byte characters.
|
||||
#[test]
|
||||
fn regression_1032() {
|
||||
let line = Line::from(
|
||||
"🦀 RFC8628 OAuth 2.0 Device Authorization GrantでCLIからGithubのaccess tokenを取得する"
|
||||
);
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 83, 1));
|
||||
line.render_ref(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines([
|
||||
"🦀 RFC8628 OAuth 2.0 Device Authorization GrantでCLIからGithubのaccess tokenを取得 "
|
||||
]));
|
||||
}
|
||||
|
||||
/// Documentary test to highlight the crab emoji width / length discrepancy
|
||||
///
|
||||
/// Part of a regression test for <https://github.com/ratatui-org/ratatui/issues/1032> which
|
||||
/// found panics with truncating lines that contained multi-byte characters.
|
||||
#[test]
|
||||
fn crab_emoji_width() {
|
||||
let crab = "🦀";
|
||||
assert_eq!(crab.len(), 4); // bytes
|
||||
assert_eq!(crab.chars().count(), 1);
|
||||
assert_eq!(crab.graphemes(true).count(), 1);
|
||||
assert_eq!(crab.width(), 2); // display width
|
||||
}
|
||||
|
||||
/// Part of a regression test for <https://github.com/ratatui-org/ratatui/issues/1032> which
|
||||
/// found panics with truncating lines that contained multi-byte characters.
|
||||
#[rstest]
|
||||
#[case::left_4(Alignment::Left, 4, "1234")]
|
||||
#[case::left_5(Alignment::Left, 5, "1234 ")]
|
||||
#[case::left_6(Alignment::Left, 6, "1234🦀")]
|
||||
#[case::left_7(Alignment::Left, 7, "1234🦀7")]
|
||||
#[case::right_4(Alignment::Right, 4, "7890")]
|
||||
#[case::right_5(Alignment::Right, 5, " 7890")]
|
||||
#[case::right_6(Alignment::Right, 6, "🦀7890")]
|
||||
#[case::right_7(Alignment::Right, 7, "4🦀7890")]
|
||||
fn render_truncates_emoji(
|
||||
#[case] alignment: Alignment,
|
||||
#[case] buf_width: u16,
|
||||
#[case] expected: &str,
|
||||
) {
|
||||
let line = Line::from("1234🦀7890").alignment(alignment);
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, buf_width, 1));
|
||||
line.render_ref(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines([expected]));
|
||||
}
|
||||
|
||||
/// Part of a regression test for <https://github.com/ratatui-org/ratatui/issues/1032> which
|
||||
/// found panics with truncating lines that contained multi-byte characters.
|
||||
///
|
||||
/// centering is tricky because there's an ambiguity about whether to take one more char
|
||||
/// from the left or the right when the line width is odd. This interacts with the width of
|
||||
/// the crab emoji, which is 2 characters wide by hitting the left or right side of the
|
||||
/// emoji.
|
||||
#[rstest]
|
||||
#[case::center_6_0(6, 0, "")]
|
||||
#[case::center_6_1(6, 1, " ")] // lef side of "🦀"
|
||||
#[case::center_6_2(6, 2, "🦀")]
|
||||
#[case::center_6_3(6, 3, "b🦀")]
|
||||
#[case::center_6_4(6, 4, "b🦀c")]
|
||||
#[case::center_7_0(7, 0, "")]
|
||||
#[case::center_7_1(7, 1, " ")] // right side of "🦀"
|
||||
#[case::center_7_2(7, 2, "🦀")]
|
||||
#[case::center_7_3(7, 3, "🦀c")]
|
||||
#[case::center_7_4(7, 4, "b🦀c")]
|
||||
#[case::center_8_0(8, 0, "")]
|
||||
#[case::center_8_1(8, 1, " ")] // right side of "🦀"
|
||||
#[case::center_8_2(8, 2, " c")] // right side of "🦀c"
|
||||
#[case::center_8_3(8, 3, "🦀c")]
|
||||
#[case::center_8_4(8, 4, "🦀cd")]
|
||||
#[case::center_8_5(8, 5, "b🦀cd")]
|
||||
#[case::center_9_0(9, 0, "")]
|
||||
#[case::center_9_1(9, 1, "c")]
|
||||
#[case::center_9_2(9, 2, " c")] // right side of "🦀c"
|
||||
#[case::center_9_3(9, 3, " cd")]
|
||||
#[case::center_9_4(9, 4, "🦀cd")]
|
||||
#[case::center_9_5(9, 5, "🦀cde")]
|
||||
#[case::center_9_6(9, 6, "b🦀cde")]
|
||||
fn render_truncates_emoji_center(
|
||||
#[case] line_width: u16,
|
||||
#[case] buf_width: u16,
|
||||
#[case] expected: &str,
|
||||
) {
|
||||
// because the crab emoji is 2 characters wide, it will can cause the centering tests
|
||||
// intersect with either the left or right part of the emoji, which causes the emoji to
|
||||
// be not rendered. Checking for four different widths of the line is enough to cover
|
||||
// all the possible cases.
|
||||
let value = match line_width {
|
||||
6 => "ab🦀cd",
|
||||
7 => "ab🦀cde",
|
||||
8 => "ab🦀cdef",
|
||||
9 => "ab🦀cdefg",
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let line = Line::from(value).centered();
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, buf_width, 1));
|
||||
line.render_ref(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines([expected]));
|
||||
}
|
||||
|
||||
/// Ensures the rendering also works away from the 0x0 position.
|
||||
///
|
||||
/// Particularly of note is that an emoji that is truncated will not overwrite the
|
||||
/// characters that are already in the buffer. This is inentional (consider how a line
|
||||
/// that is rendered on a border should not overwrite the border with a partial emoji).
|
||||
#[rstest]
|
||||
#[case::left(Alignment::Left, "XXa🦀bcXXX")]
|
||||
#[case::center(Alignment::Center, "XX🦀bc🦀XX")]
|
||||
#[case::right(Alignment::Right, "XXXbc🦀dXX")]
|
||||
fn render_truncates_away_from_0x0(#[case] alignment: Alignment, #[case] expected: &str) {
|
||||
let line = Line::from(vec![Span::raw("a🦀b"), Span::raw("c🦀d")]).alignment(alignment);
|
||||
// Fill buffer with stuff to ensure the output is indeed padded
|
||||
let mut buf = Buffer::filled(Rect::new(0, 0, 10, 1), Cell::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]));
|
||||
}
|
||||
}
|
||||
|
||||
mod iterators {
|
||||
use super::*;
|
||||
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
fmt::{self, Debug, Display},
|
||||
};
|
||||
use std::{borrow::Cow, fmt};
|
||||
|
||||
use super::Text;
|
||||
|
||||
@@ -19,7 +16,7 @@ use super::Text;
|
||||
/// let password = Masked::new("12345", 'x');
|
||||
///
|
||||
/// Paragraph::new(password).render(buffer.area, &mut buffer);
|
||||
/// assert_eq!(buffer, Buffer::with_lines(vec!["xxxxx"]));
|
||||
/// assert_eq!(buffer, Buffer::with_lines(["xxxxx"]));
|
||||
/// ```
|
||||
#[derive(Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Masked<'a> {
|
||||
@@ -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]
|
||||
|
||||
140
src/text/span.rs
140
src/text/span.rs
@@ -1,4 +1,4 @@
|
||||
use std::{borrow::Cow, fmt::Debug};
|
||||
use std::{borrow::Cow, fmt};
|
||||
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
@@ -282,36 +282,56 @@ impl<'a> Span<'a> {
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let l = "Test Content".green().italic().to_left_aligned_line();
|
||||
/// let line = "Test Content".green().italic().into_left_aligned_line();
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn to_left_aligned_line(self) -> Line<'a> {
|
||||
pub fn into_left_aligned_line(self) -> Line<'a> {
|
||||
Line::from(self).left_aligned()
|
||||
}
|
||||
|
||||
#[allow(clippy::wrong_self_convention)]
|
||||
#[deprecated = "use into_left_aligned_line"]
|
||||
pub fn to_left_aligned_line(self) -> Line<'a> {
|
||||
self.into_left_aligned_line()
|
||||
}
|
||||
|
||||
/// Converts this Span into a center-aligned [`Line`]
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let l = "Test Content".green().italic().to_centered_line();
|
||||
/// let line = "Test Content".green().italic().into_centered_line();
|
||||
/// ```
|
||||
pub fn to_centered_line(self) -> Line<'a> {
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn into_centered_line(self) -> Line<'a> {
|
||||
Line::from(self).centered()
|
||||
}
|
||||
|
||||
#[allow(clippy::wrong_self_convention)]
|
||||
#[deprecated = "use into_centered_line"]
|
||||
pub fn to_centered_line(self) -> Line<'a> {
|
||||
self.into_centered_line()
|
||||
}
|
||||
|
||||
/// Converts this Span into a right-aligned [`Line`]
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let l = "Test Content".green().italic().to_right_aligned_line();
|
||||
/// let line = "Test Content".green().italic().into_right_aligned_line();
|
||||
/// ```
|
||||
pub fn to_right_aligned_line(self) -> Line<'a> {
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn into_right_aligned_line(self) -> Line<'a> {
|
||||
Line::from(self).right_aligned()
|
||||
}
|
||||
|
||||
#[allow(clippy::wrong_self_convention)]
|
||||
#[deprecated = "use into_right_aligned_line"]
|
||||
pub fn to_right_aligned_line(self) -> Line<'a> {
|
||||
self.into_right_aligned_line()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> From<T> for Span<'a>
|
||||
@@ -343,6 +363,7 @@ impl Widget for Span<'_> {
|
||||
|
||||
impl WidgetRef for Span<'_> {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
let area = area.intersection(buf.area);
|
||||
let Rect {
|
||||
x: mut current_x,
|
||||
y,
|
||||
@@ -374,16 +395,23 @@ impl WidgetRef for Span<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Span<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", &self.content)
|
||||
impl fmt::Display for Span<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
fmt::Display::fmt(&self.content, f)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rstest::fixture;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[fixture]
|
||||
fn small_buf() -> Buffer {
|
||||
Buffer::empty(Rect::new(0, 0, 10, 1))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default() {
|
||||
let span = Span::default();
|
||||
@@ -501,20 +529,42 @@ mod tests {
|
||||
#[test]
|
||||
fn display_span() {
|
||||
let span = Span::raw("test content");
|
||||
|
||||
assert_eq!(format!("{span}"), "test content");
|
||||
assert_eq!(format!("{span:.4}"), "test");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_styled_span() {
|
||||
let stylized_span = Span::styled("stylized test content", Style::new().green());
|
||||
|
||||
assert_eq!(format!("{stylized_span}"), "stylized test content");
|
||||
assert_eq!(format!("{stylized_span:.8}"), "stylized");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn left_aligned() {
|
||||
let span = Span::styled("Test Content", Style::new().green().italic());
|
||||
let line = span.into_left_aligned_line();
|
||||
assert_eq!(line.alignment, Some(Alignment::Left));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn centered() {
|
||||
let span = Span::styled("Test Content", Style::new().green().italic());
|
||||
let line = span.into_centered_line();
|
||||
assert_eq!(line.alignment, Some(Alignment::Center));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn right_aligned() {
|
||||
let span = Span::styled("Test Content", Style::new().green().italic());
|
||||
let line = span.into_right_aligned_line();
|
||||
assert_eq!(line.alignment, Some(Alignment::Right));
|
||||
}
|
||||
|
||||
mod widget {
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
use crate::assert_buffer_eq;
|
||||
|
||||
#[test]
|
||||
fn render() {
|
||||
@@ -522,12 +572,18 @@ mod tests {
|
||||
let span = Span::styled("test content", style);
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
|
||||
span.render(buf.area, &mut buf);
|
||||
|
||||
let expected = Buffer::with_lines(vec![Line::from(vec![
|
||||
let expected = Buffer::with_lines([Line::from(vec![
|
||||
"test content".green().on_yellow(),
|
||||
" ".into(),
|
||||
])]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
assert_eq!(buf, expected);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn render_out_of_bounds(mut small_buf: Buffer) {
|
||||
let out_of_bounds = Rect::new(20, 20, 10, 1);
|
||||
Span::raw("Hello, World!").render(out_of_bounds, &mut small_buf);
|
||||
assert_eq!(small_buf, Buffer::empty(small_buf.area));
|
||||
}
|
||||
|
||||
/// When the content of the span is longer than the area passed to render, the content
|
||||
@@ -540,10 +596,11 @@ mod tests {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 1));
|
||||
span.render(Rect::new(0, 0, 5, 1), &mut buf);
|
||||
|
||||
let mut expected = Buffer::with_lines(vec![Line::from("test ")]);
|
||||
expected.set_style(Rect::new(0, 0, 5, 1), (Color::Green, Color::Yellow));
|
||||
|
||||
assert_buffer_eq!(buf, expected);
|
||||
let expected = Buffer::with_lines([Line::from(vec![
|
||||
"test ".green().on_yellow(),
|
||||
" ".into(),
|
||||
])]);
|
||||
assert_eq!(buf, expected);
|
||||
}
|
||||
|
||||
/// When there is already a style set on the buffer, the style of the span should be
|
||||
@@ -555,12 +612,11 @@ mod tests {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
|
||||
buf.set_style(buf.area, Style::new().italic());
|
||||
span.render(buf.area, &mut buf);
|
||||
|
||||
let expected = Buffer::with_lines(vec![Line::from(vec![
|
||||
let expected = Buffer::with_lines([Line::from(vec![
|
||||
"test content".green().on_yellow().italic(),
|
||||
" ".italic(),
|
||||
])]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
assert_eq!(buf, expected);
|
||||
}
|
||||
|
||||
/// When the span contains a multi-width grapheme, the grapheme will ensure that the cells
|
||||
@@ -571,12 +627,11 @@ mod tests {
|
||||
let span = Span::styled("test 😃 content", style);
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
|
||||
span.render(buf.area, &mut buf);
|
||||
|
||||
// The existing code in buffer.set_line() handles multi-width graphemes by clearing the
|
||||
// cells of the hidden characters. This test ensures that the existing behavior is
|
||||
// preserved.
|
||||
let expected = Buffer::with_lines(vec!["test 😃 content".green().on_yellow()]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
let expected = Buffer::with_lines(["test 😃 content".green().on_yellow()]);
|
||||
assert_eq!(buf, expected);
|
||||
}
|
||||
|
||||
/// When the span contains a multi-width grapheme that does not fit in the area passed to
|
||||
@@ -589,11 +644,9 @@ mod tests {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 6, 1));
|
||||
span.render(buf.area, &mut buf);
|
||||
|
||||
let expected = Buffer::with_lines(vec![Line::from(vec![
|
||||
"test ".green().on_yellow(),
|
||||
" ".into(),
|
||||
])]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
let expected =
|
||||
Buffer::with_lines([Line::from(vec!["test ".green().on_yellow(), " ".into()])]);
|
||||
assert_eq!(buf, expected);
|
||||
}
|
||||
|
||||
/// When the area passed to render overflows the buffer, the content should be truncated
|
||||
@@ -605,32 +658,11 @@ mod tests {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
|
||||
span.render(Rect::new(10, 0, 20, 1), &mut buf);
|
||||
|
||||
let expected = Buffer::with_lines(vec![Line::from(vec![
|
||||
let expected = Buffer::with_lines([Line::from(vec![
|
||||
" ".into(),
|
||||
"test ".green().on_yellow(),
|
||||
])]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
assert_eq!(buf, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn left_aligned() {
|
||||
let span = Span::styled("Test Content", Style::new().green().italic());
|
||||
let line = span.to_left_aligned_line();
|
||||
assert_eq!(line.alignment, Some(Alignment::Left));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn centered() {
|
||||
let span = Span::styled("Test Content", Style::new().green().italic());
|
||||
let line = span.to_centered_line();
|
||||
assert_eq!(line.alignment, Some(Alignment::Center));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn right_aligned() {
|
||||
let span = Span::styled("Test Content", Style::new().green().italic());
|
||||
let line = span.to_right_aligned_line();
|
||||
assert_eq!(line.alignment, Some(Alignment::Right));
|
||||
}
|
||||
}
|
||||
|
||||
278
src/text/text.rs
278
src/text/text.rs
@@ -1,5 +1,5 @@
|
||||
#![warn(missing_docs)]
|
||||
use std::borrow::Cow;
|
||||
use std::{borrow::Cow, fmt};
|
||||
|
||||
use itertools::{Itertools, Position};
|
||||
|
||||
@@ -50,6 +50,8 @@ use crate::prelude::*;
|
||||
/// - [`Text::height`] returns the height.
|
||||
/// - [`Text::patch_style`] patches the style of this `Text`, adding modifiers from the given style.
|
||||
/// - [`Text::reset_style`] resets the style of the `Text`.
|
||||
/// - [`Text::push_line`] adds a line to the text.
|
||||
/// - [`Text::push_span`] adds a span to the last line of the text.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
@@ -441,6 +443,46 @@ impl<'a> Text<'a> {
|
||||
pub fn iter_mut(&mut self) -> std::slice::IterMut<Line<'a>> {
|
||||
self.lines.iter_mut()
|
||||
}
|
||||
|
||||
/// Adds a line to the text.
|
||||
///
|
||||
/// `line` can be any type that can be converted into a `Line`. For example, you can pass a
|
||||
/// `&str`, a `String`, a `Span`, or a `Line`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let mut text = Text::from("Hello, world!");
|
||||
/// text.push_line(Line::from("How are you?"));
|
||||
/// text.push_line(Span::from("How are you?"));
|
||||
/// text.push_line("How are you?");
|
||||
/// ```
|
||||
pub fn push_line<T: Into<Line<'a>>>(&mut self, line: T) {
|
||||
self.lines.push(line.into());
|
||||
}
|
||||
|
||||
/// Adds a span to the last line of the text.
|
||||
///
|
||||
/// `span` can be any type that is convertible into a `Span`. For example, you can pass a
|
||||
/// `&str`, a `String`, or a `Span`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let mut text = Text::from("Hello, world!");
|
||||
/// text.push_span(Span::from("How are you?"));
|
||||
/// text.push_span("How are you?");
|
||||
/// ```
|
||||
pub fn push_span<T: Into<Span<'a>>>(&mut self, span: T) {
|
||||
let span = span.into();
|
||||
if let Some(last) = self.lines.last_mut() {
|
||||
last.push_span(span);
|
||||
} else {
|
||||
self.lines.push(Line::from(span));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for Text<'a> {
|
||||
@@ -538,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}")?;
|
||||
@@ -559,6 +601,7 @@ impl Widget for Text<'_> {
|
||||
|
||||
impl WidgetRef for Text<'_> {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
let area = area.intersection(buf.area);
|
||||
buf.set_style(area, self.style);
|
||||
for (line, row) in self.iter().zip(area.rows()) {
|
||||
let line_width = line.width() as u16;
|
||||
@@ -601,6 +644,11 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[fixture]
|
||||
fn small_buf() -> Buffer {
|
||||
Buffer::empty(Rect::new(0, 0, 10, 1))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw() {
|
||||
let text = Text::raw("The first line\nThe second line");
|
||||
@@ -848,103 +896,6 @@ mod tests {
|
||||
assert_eq!(Text::default().italic().style, Modifier::ITALIC.into());
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_right_aligned() {
|
||||
let text = Text::from("foo").alignment(Alignment::Right);
|
||||
|
||||
let area = Rect::new(0, 0, 5, 1);
|
||||
let mut buf = Buffer::empty(area);
|
||||
text.render(area, &mut buf);
|
||||
|
||||
let expected_buf = Buffer::with_lines(vec![" foo"]);
|
||||
|
||||
assert_buffer_eq!(buf, expected_buf);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_centered_odd() {
|
||||
let text = Text::from("foo").alignment(Alignment::Center);
|
||||
|
||||
let area = Rect::new(0, 0, 5, 1);
|
||||
let mut buf = Buffer::empty(area);
|
||||
text.render(area, &mut buf);
|
||||
|
||||
let expected_buf = Buffer::with_lines(vec![" foo "]);
|
||||
|
||||
assert_buffer_eq!(buf, expected_buf);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_centered_even() {
|
||||
let text = Text::from("foo").alignment(Alignment::Center);
|
||||
|
||||
let area = Rect::new(0, 0, 6, 1);
|
||||
let mut buf = Buffer::empty(area);
|
||||
text.render(area, &mut buf);
|
||||
|
||||
let expected_buf = Buffer::with_lines(vec![" foo "]);
|
||||
|
||||
assert_buffer_eq!(buf, expected_buf);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_one_line_right() {
|
||||
let text = Text::from(vec![
|
||||
"foo".into(),
|
||||
Line::from("bar").alignment(Alignment::Center),
|
||||
])
|
||||
.alignment(Alignment::Right);
|
||||
|
||||
let area = Rect::new(0, 0, 5, 2);
|
||||
let mut buf = Buffer::empty(area);
|
||||
text.render(area, &mut buf);
|
||||
|
||||
let expected_buf = Buffer::with_lines(vec![" foo", " bar "]);
|
||||
|
||||
assert_buffer_eq!(buf, expected_buf);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_only_styles_line_area() {
|
||||
let area = Rect::new(0, 0, 5, 1);
|
||||
let mut buf = Buffer::empty(area);
|
||||
Text::from("foo".on_blue()).render(area, &mut buf);
|
||||
|
||||
let mut expected = Buffer::with_lines(vec!["foo "]);
|
||||
expected.set_style(Rect::new(0, 0, 3, 1), Style::new().bg(Color::Blue));
|
||||
|
||||
assert_buffer_eq!(buf, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_truncates() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 6, 1));
|
||||
Text::from("foobar".on_blue()).render(Rect::new(0, 0, 3, 1), &mut buf);
|
||||
|
||||
let mut expected = Buffer::with_lines(vec!["foo "]);
|
||||
expected.set_style(Rect::new(0, 0, 3, 1), Style::new().bg(Color::Blue));
|
||||
|
||||
assert_buffer_eq!(buf, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn left_aligned() {
|
||||
let text = Text::from("Hello, world!").left_aligned();
|
||||
@@ -963,6 +914,133 @@ mod tests {
|
||||
assert_eq!(text.alignment, Some(Alignment::Right));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn push_line() {
|
||||
let mut text = Text::from("A");
|
||||
text.push_line(Line::from("B"));
|
||||
text.push_line(Span::from("C"));
|
||||
text.push_line("D");
|
||||
assert_eq!(
|
||||
text.lines,
|
||||
vec![
|
||||
Line::raw("A"),
|
||||
Line::raw("B"),
|
||||
Line::raw("C"),
|
||||
Line::raw("D")
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn push_line_empty() {
|
||||
let mut text = Text::default();
|
||||
text.push_line(Line::from("Hello, world!"));
|
||||
assert_eq!(text.lines, vec![Line::from("Hello, world!")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn push_span() {
|
||||
let mut text = Text::from("A");
|
||||
text.push_span(Span::raw("B"));
|
||||
text.push_span("C");
|
||||
assert_eq!(
|
||||
text.lines,
|
||||
vec![Line::from(vec![
|
||||
Span::raw("A"),
|
||||
Span::raw("B"),
|
||||
Span::raw("C")
|
||||
])],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn push_span_empty() {
|
||||
let mut text = Text::default();
|
||||
text.push_span(Span::raw("Hello, world!"));
|
||||
assert_eq!(text.lines, vec![Line::from(Span::raw("Hello, world!"))],);
|
||||
}
|
||||
|
||||
mod widget {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn render() {
|
||||
let text = Text::from("foo");
|
||||
let area = Rect::new(0, 0, 5, 1);
|
||||
let mut buf = Buffer::empty(area);
|
||||
text.render(area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["foo "]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn render_out_of_bounds(mut small_buf: Buffer) {
|
||||
let out_of_bounds_area = Rect::new(20, 20, 10, 1);
|
||||
Text::from("Hello, world!").render(out_of_bounds_area, &mut small_buf);
|
||||
assert_eq!(small_buf, Buffer::empty(small_buf.area));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_right_aligned() {
|
||||
let text = Text::from("foo").alignment(Alignment::Right);
|
||||
let area = Rect::new(0, 0, 5, 1);
|
||||
let mut buf = Buffer::empty(area);
|
||||
text.render(area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines([" foo"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_centered_odd() {
|
||||
let text = Text::from("foo").alignment(Alignment::Center);
|
||||
let area = Rect::new(0, 0, 5, 1);
|
||||
let mut buf = Buffer::empty(area);
|
||||
text.render(area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines([" foo "]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_centered_even() {
|
||||
let text = Text::from("foo").alignment(Alignment::Center);
|
||||
let area = Rect::new(0, 0, 6, 1);
|
||||
let mut buf = Buffer::empty(area);
|
||||
text.render(area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines([" foo "]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_one_line_right() {
|
||||
let text = Text::from(vec![
|
||||
"foo".into(),
|
||||
Line::from("bar").alignment(Alignment::Center),
|
||||
])
|
||||
.alignment(Alignment::Right);
|
||||
let area = Rect::new(0, 0, 5, 2);
|
||||
let mut buf = Buffer::empty(area);
|
||||
text.render(area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines([" foo", " bar "]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_only_styles_line_area() {
|
||||
let area = Rect::new(0, 0, 5, 1);
|
||||
let mut buf = Buffer::empty(area);
|
||||
Text::from("foo".on_blue()).render(area, &mut buf);
|
||||
|
||||
let mut expected = Buffer::with_lines(["foo "]);
|
||||
expected.set_style(Rect::new(0, 0, 3, 1), Style::new().bg(Color::Blue));
|
||||
assert_eq!(buf, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_truncates() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 6, 1));
|
||||
Text::from("foobar".on_blue()).render(Rect::new(0, 0, 3, 1), &mut buf);
|
||||
|
||||
let mut expected = Buffer::with_lines(["foo "]);
|
||||
expected.set_style(Rect::new(0, 0, 3, 1), Style::new().bg(Color::Blue));
|
||||
assert_eq!(buf, expected);
|
||||
}
|
||||
}
|
||||
|
||||
mod iterators {
|
||||
use super::*;
|
||||
|
||||
|
||||
428
src/widgets.rs
428
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) {
|
||||
@@ -355,7 +391,7 @@ impl<W: WidgetRef> WidgetRef for Option<W> {
|
||||
/// to a stateful widget and render it later. It also allows you to render boxed stateful widgets.
|
||||
///
|
||||
/// This trait was introduced in Ratatui 0.26.0 and is implemented for all the internal stateful
|
||||
/// widgets. Implemetors should prefer to implement this over the `StatefulWidget` trait and add an
|
||||
/// widgets. Implementors should prefer to implement this over the `StatefulWidget` trait and add an
|
||||
/// implementation of `StatefulWidget` that calls `StatefulWidgetRef::render_ref` where backwards
|
||||
/// compatibility is required.
|
||||
///
|
||||
@@ -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.
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
//!
|
||||
//!
|
||||
//!
|
||||
//! The [`Monthly`] widget will display a calendar for the monh provided in `display_date`. Days are
|
||||
//! styled using the default style unless:
|
||||
//! The [`Monthly`] widget will display a calendar for the month provided in `display_date`. Days
|
||||
//! are styled using the default style unless:
|
||||
//! * `show_surrounding` is set, then days not in the `display_date` month will use that style.
|
||||
//! * a style is returned by the [`DateStyler`] for the day
|
||||
//!
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -330,7 +330,7 @@ impl<'a> Dataset<'a> {
|
||||
|
||||
/// Sets the data points of this dataset
|
||||
///
|
||||
/// Points will then either be rendered as scrattered points or with lines between them
|
||||
/// Points will then either be rendered as scattered points or with lines between them
|
||||
/// depending on [`Dataset::graph_type`].
|
||||
///
|
||||
/// Data consist in an array of `f64` tuples (`(f64, f64)`), the first element being X and the
|
||||
@@ -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);
|
||||
/// ```
|
||||
@@ -493,7 +493,7 @@ pub struct Chart<'a> {
|
||||
style: Style,
|
||||
/// Constraints used to determine whether the legend should be shown or not
|
||||
hidden_legend_constraints: (Constraint, Constraint),
|
||||
/// The position detnermine where the legenth is shown or hide regaurdless of
|
||||
/// The position determine where the length is shown or hide regardless of
|
||||
/// `hidden_legend_constraints`
|
||||
legend_position: Option<LegendPosition>,
|
||||
}
|
||||
@@ -636,7 +636,7 @@ impl<'a> Chart<'a> {
|
||||
/// let chart = Chart::new(vec![]).hidden_legend_constraints(constraints);
|
||||
/// ```
|
||||
///
|
||||
/// Always hide the legend. Note this can be accomplished more exclicitely by passing `None` to
|
||||
/// Always hide the legend. Note this can be accomplished more explicitly by passing `None` to
|
||||
/// [`Chart::legend_position`].
|
||||
///
|
||||
/// ```
|
||||
@@ -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()
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user