Compare commits

..

1 Commits

Author SHA1 Message Date
Josh McKinney
9ea840d87f feat(layout): accept (x, y) tuple for Rect::offset
This allows callers to call `Rect::offset((x, y))`` instead of the more
verbose `Rect::offset(Offset { x, y })`.
2024-03-28 16:22:43 -07:00
122 changed files with 10642 additions and 11450 deletions

16
.cargo-husky/hooks/pre-push Executable file
View File

@@ -0,0 +1,16 @@
#!/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
View File

@@ -5,7 +5,4 @@
# https://git-scm.com/docs/gitignore#_pattern_format
# Maintainers
* @orhun @joshka @kdheepak @Valentin271 @EdJoPaTo
# Past maintainers
# @mindoodoo @sayanarijit
* @orhun @mindoodoo @sayanarijit @joshka @kdheepak @Valentin271 @EdJoPaTo

View File

@@ -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,11 +10,6 @@ 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`
@@ -42,7 +37,7 @@ This is a quick summary of the sections below:
- `Scrollbar`: symbols moved to `symbols` module
- MSRV is now 1.67.0
- [v0.22.0](#v0220)
- `serde` representation of `Borders` and `Modifiers` has changed
- serde representation of `Borders` and `Modifiers` has changed
- [v0.21.0](#v0210)
- MSRV is now 1.65.0
- `terminal::ViewPort` is now an enum
@@ -52,73 +47,16 @@ 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` ([#881])
### `Flex::Start` is the new default flex mode for `Layout`
[#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.
@@ -136,7 +74,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.
@@ -153,7 +91,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
@@ -170,7 +108,7 @@ by removing the call to `.collect()`.
[#751]: https://github.com/ratatui-org/ratatui/pull/751
The default() implementation of Table now sets the column_spacing field to 1 and the segment_size
field to `SegmentSize::None`. This will affect the rendering of a small amount of apps.
field to SegmentSize::None. This will affect the rendering of a small amount of apps.
To use the previous default values, call `table.segment_size(Default::default())` and
`table.column_spacing(0)`.
@@ -284,8 +222,8 @@ widget in the default configuration would not show any indication of the selecte
[#664]: https://github.com/ratatui-org/ratatui/pull/664
Previously `Table`s could be constructed without `widths`. In almost all cases this is an error.
A new `widths` parameter is now mandatory on `Table::new()`. Existing code of the form:
Previously `Table`s could be constructed without widths. In almost all cases this is an error.
A new widths parameter is now mandatory on `Table::new()`. Existing code of the form:
```diff
- Table::new(rows).widths(widths)
@@ -343,7 +281,7 @@ let layout = layout::new(Direction::Vertical, [Constraint::Min(1), Constraint::M
## [v0.24.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.24.0)
### `ScrollbarState` field type changed from `u16` to `usize` ([#456])
### ScrollbarState field type changed from `u16` to `usize` ([#456])
[#456]: https://github.com/ratatui-org/ratatui/pull/456
@@ -447,12 +385,12 @@ The MSRV of ratatui is now 1.67 due to an MSRV update in a dependency (`time`).
## [v0.22.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.22.0)
### `bitflags` updated to 2.3 ([#205])
### bitflags updated to 2.3 ([#205])
[#205]: https://github.com/ratatui-org/ratatui/issues/205
The `serde` representation of `bitflags` has changed. Any existing serialized types that have Borders or
Modifiers will need to be re-serialized. This is documented in the [`bitflags`
The serde representation of bitflags has changed. Any existing serialized types that have Borders or
Modifiers will need to be re-serialized. This is documented in the [bitflags
changelog](https://github.com/bitflags/bitflags/blob/main/CHANGELOG.md#200-rc2)..
## [v0.21.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.21.0)
@@ -484,9 +422,9 @@ let terminal = Terminal::with_options(backend, TerminalOptions {
[#168]: https://github.com/ratatui-org/ratatui/issues/168
A new type `Masked` was introduced that implements `From<Text<'a>>`. This causes any code that
A new type `Masked` was introduced that implements `From<Text<'a>>`. This causes any code that did
previously did not need to use type annotations to fail to compile. To fix this, annotate or call
`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

File diff suppressed because it is too large Load Diff

View File

@@ -56,9 +56,11 @@ documented.
### Run CI tests before pushing a PR
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.
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`.
### Sign your commits

View File

@@ -1,13 +1,11 @@
[package]
name = "ratatui"
version = "0.26.3" # crate version
version = "0.26.1" # 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/"
repository = "https://github.com/ratatui-org/ratatui"
homepage = "https://ratatui.rs"
keywords = ["tui", "terminal", "dashboard"]
categories = ["command-line-interface"]
repository = "https://github.com/ratatui-org/ratatui"
readme = "README.md"
license = "MIT"
exclude = [
@@ -25,46 +23,47 @@ rust-version = "1.74.0"
[badges]
[dependencies]
bitflags = "2.3"
cassowary = "0.3"
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 }
serde = { version = "1", optional = true, features = ["derive"] }
bitflags = "2.3"
cassowary = "0.3"
indoc = "2.0"
itertools = "0.12"
paste = "1.0.2"
strum = { version = "0.26", features = ["derive"] }
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.19.0"
rstest = "0.18.2"
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"
@@ -74,6 +73,7 @@ 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,15 +87,11 @@ 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]
@@ -106,15 +102,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::CrosstermBackend) backend and adds a dependency on [`crossterm`].
## enables the [`CrosstermBackend`] backend and adds a dependency on the [Crossterm crate].
crossterm = ["dep:crossterm"]
## enables the [`TermionBackend`](backend::TermionBackend) backend and adds a dependency on [`termion`].
## enables the [`TermionBackend`] backend and adds a dependency on the [Termion crate].
termion = ["dep:termion"]
## enables the [`TermwizBackend`](backend::TermwizBackend) backend and adds a dependency on [`termwiz`].
## enables the [`TermwizBackend`] backend and adds a dependency on the [Termwiz crate].
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"]
@@ -126,14 +122,12 @@ 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`](widgets::calendar) widget module and adds a dependency on [`time`].
## enables the [`calendar`] widget module and adds a dependency on the [Time crate].
widget-calendar = ["dep:time"]
#! The following optional features are only available for some backends:
#! Underline color is only supported by the [`CrosstermBackend`] backend, and is not supported
#! on Windows 7.
## 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:
@@ -141,13 +135,13 @@ underline-color = ["dep:crossterm"]
## Enable all unstable features.
unstable = ["unstable-rendered-line-info", "unstable-widget-ref"]
## Enables the [`Paragraph::line_count`](widgets::Paragraph::line_count)
## [`Paragraph::line_width`](widgets::Paragraph::line_width) methods
## Enables the [`Paragraph::line_count`](crate::widgets::Paragraph::line_count)
## [`Paragraph::line_width`](crate::widgets::Paragraph::line_width) methods
## which are experimental and may change in the future.
## See [Issue 293](https://github.com/ratatui-org/ratatui/issues/293) for more details.
unstable-rendered-line-info = []
## Enables the [`WidgetRef`](widgets::WidgetRef) and [`StatefulWidgetRef`](widgets::StatefulWidgetRef) traits which are experimental and may change in
## Enables the `WidgetRef` and `StatefulWidgetRef` traits which are experimental and may change in
## the future.
unstable-widget-ref = []
@@ -157,11 +151,6 @@ 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
@@ -170,10 +159,6 @@ harness = false
name = "block"
harness = false
[[bench]]
name = "line"
harness = false
[[bench]]
name = "list"
harness = false
@@ -201,13 +186,13 @@ required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "calendar"
required-features = ["crossterm", "widget-calendar"]
name = "canvas"
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]]
@@ -226,16 +211,6 @@ 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"]
@@ -256,11 +231,6 @@ 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"]
@@ -271,18 +241,23 @@ 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 = "line_gauge"
name = "constraints"
required-features = ["crossterm"]
doc-scrape-examples = false
[[example]]
name = "flex"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "constraint-explorer"
required-features = ["crossterm"]
doc-scrape-examples = true
@@ -291,12 +266,6 @@ 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"]
@@ -348,6 +317,11 @@ 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"]

View File

@@ -7,12 +7,11 @@ 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.
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" }
# 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" } }
[tasks.default]
alias = "ci"

View File

@@ -43,10 +43,10 @@ Ratatui was forked from the [tui-rs] crate in 2023 in order to continue its deve
## Installation
Add `ratatui` as a dependency to your cargo.toml:
Add `ratatui` and `crossterm` as dependencies to your cargo.toml:
```shell
cargo add ratatui
cargo add ratatui crossterm
```
Ratatui uses [Crossterm] by default as it works on most platforms. See the [Installation]
@@ -110,8 +110,7 @@ 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. 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]
using the provided [`render_widget`] method. See the [Widgets] section of the [Ratatui Website]
for more info.
### Handling events
@@ -126,17 +125,12 @@ Website] for more info. For example, if you are using [Crossterm], you can use t
```rust
use std::io::{self, stdout};
use ratatui::{
crossterm::{
event::{self, Event, KeyCode},
terminal::{
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
},
ExecutableCommand,
},
prelude::*,
widgets::*,
use crossterm::{
event::{self, Event, KeyCode},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
};
use ratatui::{prelude::*, widgets::*};
fn main() -> io::Result<()> {
enable_raw_mode()?;
@@ -167,7 +161,8 @@ fn handle_events() -> io::Result<bool> {
fn ui(frame: &mut Frame) {
frame.render_widget(
Paragraph::new("Hello World!").block(Block::bordered().title("Greeting")),
Paragraph::new("Hello World!")
.block(Block::default().title("Greeting").borders(Borders::ALL)),
frame.size(),
);
}
@@ -211,8 +206,14 @@ fn ui(frame: &mut Frame) {
[Constraint::Percentage(50), Constraint::Percentage(50)],
)
.split(main_layout[1]);
frame.render_widget(Block::bordered().title("Left"), inner_layout[0]);
frame.render_widget(Block::bordered().title("Right"), inner_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],
);
}
```
@@ -326,20 +327,24 @@ 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&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
[Crate Badge]: https://img.shields.io/crates/v/ratatui?logo=rust&style=flat-square
[License Badge]: https://img.shields.io/crates/l/ratatui?style=flat-square
[CI Badge]:
https://img.shields.io/github/actions/workflow/status/ratatui-org/ratatui/ci.yml?style=flat-square&logo=github
[CI Workflow]: https://github.com/ratatui-org/ratatui/actions/workflows/ci.yml
[Codecov Badge]: https://img.shields.io/codecov/c/github/ratatui-org/ratatui?logo=codecov&style=flat-square&token=BAQ8SOKEST&color=C43AC3&logoColor=C43AC3
[Codecov Badge]:
https://img.shields.io/codecov/c/github/ratatui-org/ratatui?logo=codecov&style=flat-square&token=BAQ8SOKEST
[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&color=1370D3&logoColor=1370D3
[Discord Badge]:
https://img.shields.io/discord/1070692720437383208?label=discord&logo=discord&style=flat-square
[Discord Server]: https://discord.gg/pMCEU9hNEj
[Docs Badge]: https://img.shields.io/docsrs/ratatui?logo=rust&style=flat-square&logoColor=E05D44
[Matrix Badge]: https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix&color=C43AC3
[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
[Matrix]: https://matrix.to/#/#ratatui:matrix.org
[Sponsors Badge]: https://img.shields.io/github/sponsors/ratatui-org?logo=github&style=flat-square&color=1370D3
[Sponsors Badge]: https://img.shields.io/github/sponsors/ratatui-org?logo=github&style=flat-square
<!-- cargo-rdme end -->

View File

@@ -1,10 +1,11 @@
use criterion::{criterion_group, criterion_main, BatchSize, Bencher, Criterion};
use criterion::{criterion_group, criterion_main, BatchSize, Bencher, BenchmarkId, Criterion};
use ratatui::{
buffer::Buffer,
layout::{Alignment, Rect},
layout::Rect,
prelude::Alignment,
widgets::{
block::{Position, Title},
Block, Padding, Widget,
Block, Borders, Padding, Widget,
},
};
@@ -12,31 +13,32 @@ use ratatui::{
fn block(c: &mut Criterion) {
let mut group = c.benchmark_group("block");
for (width, height) in [
(100, 50), // vertically split screen
(200, 50), // 1080p fullscreen with medium font
(256, 256), // Max sized area
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
] {
let buffer_size = Rect::new(0, 0, width, height);
let buffer_area = buffer_size.area();
// Render an empty block
group.bench_with_input(
format!("render_empty/{width}x{height}"),
BenchmarkId::new("render_empty", buffer_area),
&Block::new(),
|b, block| render(b, block, buffer_size),
);
// Render with all features
group.bench_with_input(
format!("render_all_feature/{width}x{height}"),
&Block::bordered()
.padding(Padding::new(5, 5, 2, 2))
BenchmarkId::new("render_all_feature", buffer_area),
&Block::new()
.borders(Borders::ALL)
.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),
);
}

View File

@@ -1,39 +0,0 @@
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);

View File

@@ -1,9 +1,4 @@
# git-cliff ~ configuration file
# https://git-cliff.org/docs/configuration
[remote.github]
owner = "ratatui-org"
repo = "ratatui"
# configuration for https://github.com/orhun/git-cliff
[changelog]
# changelog header
@@ -26,10 +21,8 @@ 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 | 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 %}\
- [{{ 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 }}
{%- if commit.breaking %} [**breaking**]{% endif %}
{%- if commit.body %}
@@ -56,28 +49,6 @@ 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 %}
"""
@@ -97,7 +68,7 @@ filter_unconventional = true
split_commits = false
# regex for preprocessing the commit messages
commit_preprocessors = [
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "" },
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/ratatui-org/ratatui/issues/${2}))" },
{ 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}" },

View File

@@ -1,17 +0,0 @@
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",
]

View File

@@ -1,20 +1,20 @@
# Examples
This folder might use unreleased code. View the examples for the latest release instead.
This folder contains unreleased code. View the [examples for the latest release
(0.25.0)](https://github.com/ratatui-org/ratatui/tree/v0.25.0/examples) instead.
> [!WARNING]
>
> There may be backwards incompatible changes in these examples, as they are designed to compile
> There are backwards incompatible changes in these examples, as they are designed to compile
> against the `main` branch.
>
> There are a few workaround for this problem:
>
> - View the examples as they were when the latest version was release by selecting the tag that
> matches that version. E.g. <https://github.com/ratatui-org/ratatui/tree/v0.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.
> matches that version. E.g. <https://github.com/ratatui-org/ratatui/tree/v0.25.0/examples>. There
> is a combo box at the top of this page which allows you to select any previous tagged version.
> - To view the code locally, checkout the tag using `git switch --detach v0.25.0`.
> - Use the latest [alpha version of Ratatui]. These are released weekly on Saturdays.
> - Compile your code against the main branch either locally by adding e.g. `path = "../ratatui"` to
> the dependency, or remotely by adding `git = "https://github.com/ratatui-org/ratatui"`
>
@@ -164,18 +164,6 @@ 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
@@ -358,7 +346,6 @@ 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

View File

@@ -19,18 +19,14 @@ 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::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
terminal::{Frame, Terminal},
text::{Line, Span},
widgets::{Bar, BarChart, BarGroup, Block, Paragraph},
prelude::*,
widgets::{Bar, BarChart, BarGroup, Block, Borders, Paragraph},
};
struct Company<'a> {
@@ -163,7 +159,7 @@ fn ui(frame: &mut Frame, app: &App) {
let [left, right] = horizontal.areas(bottom);
let barchart = BarChart::default()
.block(Block::bordered().title("Data1"))
.block(Block::default().title("Data1").borders(Borders::ALL))
.data(&app.data)
.bar_width(9)
.bar_style(Style::default().fg(Color::Yellow))
@@ -221,7 +217,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::bordered().title("Data1"))
.block(Block::default().title("Data1").borders(Borders::ALL))
.bar_width(7)
.group_gap(3);
@@ -250,7 +246,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::bordered().title("Data1"))
.block(Block::default().title("Data1").borders(Borders::ALL))
.bar_width(1)
.group_gap(1)
.bar_gap(0)
@@ -290,13 +286,15 @@ fn draw_legend(f: &mut Frame, area: Rect) {
"- Company B",
Style::default().fg(Color::Yellow),
)),
Line::from(Span::styled(
Line::from(vec![Span::styled(
"- Company C",
Style::default().fg(Color::White),
)),
)]),
];
let block = Block::bordered().style(Style::default().fg(Color::White));
let block = Block::default()
.borders(Borders::ALL)
.style(Style::default().fg(Color::White));
let paragraph = Paragraph::new(text).block(block);
f.render_widget(paragraph, area);
}

View File

@@ -20,18 +20,14 @@ 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::{
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,
prelude::*,
widgets::{
block::{Position, Title},
Block, BorderType, Borders, Padding, Paragraph, Wrap,
@@ -164,20 +160,23 @@ fn render_border_type(
frame: &mut Frame,
area: Rect,
) {
let block = Block::bordered()
let block = Block::new()
.borders(Borders::ALL)
.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::bordered()
let block = Block::new()
.borders(Borders::ALL)
.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::bordered()
let block = Block::new()
.borders(Borders::ALL)
.style(Style::new().blue().on_white().bold().italic())
.title("Styled block");
frame.render_widget(paragraph.clone().block(block), area);
@@ -185,7 +184,8 @@ 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::bordered()
let block = Block::new()
.borders(Borders::ALL)
.title("Styled title")
.title_style(Style::new().blue().on_white().bold().italic());
frame.render_widget(paragraph.clone().block(block), area);
@@ -196,19 +196,21 @@ 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::bordered().title(title);
let block = Block::new().borders(Borders::ALL).title(title);
frame.render_widget(paragraph.clone().block(block), area);
}
fn render_multiple_titles(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
let block = Block::bordered()
let block = Block::new()
.borders(Borders::ALL)
.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::bordered()
let block = Block::new()
.borders(Borders::ALL)
.title(
Title::from("top left")
.position(Position::Top)
@@ -243,15 +245,16 @@ fn render_multiple_title_positions(paragraph: &Paragraph, frame: &mut Frame, are
}
fn render_padding(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
let block = Block::bordered()
.padding(Padding::new(5, 10, 1, 2))
.title("Padding");
let block = Block::new()
.borders(Borders::ALL)
.title("Padding")
.padding(Padding::new(5, 10, 1, 2));
frame.render_widget(paragraph.clone().block(block), area);
}
fn render_nested_blocks(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
let outer_block = Block::bordered().title("Outer block");
let inner_block = Block::bordered().title("Inner block");
let outer_block = Block::new().borders(Borders::ALL).title("Outer block");
let inner_block = Block::new().borders(Borders::ALL).title("Inner block");
let inner = outer_block.inner(area);
frame.render_widget(outer_block, area);
frame.render_widget(paragraph.clone().block(inner_block), inner);

View File

@@ -13,20 +13,16 @@
//! [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 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 crossterm::{
event::{self, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{prelude::*, widgets::calendar::*};
use time::{Date, Month, OffsetDateTime};
fn main() -> Result<(), Box<dyn Error>> {
@@ -56,8 +52,8 @@ fn main() -> Result<(), Box<dyn Error>> {
Ok(())
}
fn draw(frame: &mut Frame) {
let app_area = frame.size();
fn draw(f: &mut Frame) {
let app_area = f.size();
let calarea = Rect {
x: app_area.x + 1,
@@ -84,7 +80,7 @@ fn draw(frame: &mut Frame) {
});
for col in cols {
let cal = cals::get_cal(start.month(), start.year(), &list);
frame.render_widget(cal, col);
f.render_widget(cal, col);
start = start.replace_month(start.month().next()).unwrap();
}
}
@@ -170,7 +166,6 @@ 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> {

View File

@@ -13,26 +13,21 @@
//! [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::{
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,
},
prelude::*,
widgets::{canvas::*, *},
};
fn main() -> io::Result<()> {
@@ -142,7 +137,7 @@ impl App {
fn map_canvas(&self) -> impl Widget + '_ {
Canvas::default()
.block(Block::bordered().title("World"))
.block(Block::default().borders(Borders::ALL).title("World"))
.marker(self.marker)
.paint(|ctx| {
ctx.draw(&Map {
@@ -157,7 +152,7 @@ impl App {
fn pong_canvas(&self) -> impl Widget + '_ {
Canvas::default()
.block(Block::bordered().title("Pong"))
.block(Block::default().borders(Borders::ALL).title("Pong"))
.marker(self.marker)
.paint(|ctx| {
ctx.draw(&self.ball);
@@ -172,7 +167,7 @@ impl App {
let bottom = 0.0;
let top = f64::from(area.height).mul_add(2.0, -4.0);
Canvas::default()
.block(Block::bordered().title("Rects"))
.block(Block::default().borders(Borders::ALL).title("Rects"))
.marker(self.marker)
.x_bounds([left, right])
.y_bounds([bottom, top])

View File

@@ -19,19 +19,14 @@ 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, Rect},
style::{Color, Modifier, Style, Stylize},
symbols::{self, Marker},
terminal::{Frame, Terminal},
text::Span,
widgets::{block::Title, Axis, Block, Chart, Dataset, GraphType, LegendPosition},
prelude::*,
widgets::{block::Title, Axis, Block, Borders, Chart, Dataset, GraphType, LegendPosition},
};
#[derive(Clone)]
@@ -189,7 +184,11 @@ fn render_chart1(f: &mut Frame, area: Rect, app: &App) {
];
let chart = Chart::new(datasets)
.block(Block::bordered().title("Chart 1".cyan().bold()))
.block(
Block::default()
.title("Chart 1".cyan().bold())
.borders(Borders::ALL),
)
.x_axis(
Axis::default()
.title("X Axis")
@@ -218,11 +217,13 @@ fn render_line_chart(f: &mut Frame, area: Rect) {
let chart = Chart::new(datasets)
.block(
Block::bordered().title(
Title::default()
.content("Line chart".cyan().bold())
.alignment(Alignment::Center),
),
Block::default()
.title(
Title::default()
.content("Line chart".cyan().bold())
.alignment(Alignment::Center),
)
.borders(Borders::ALL),
)
.x_axis(
Axis::default()
@@ -268,7 +269,7 @@ fn render_scatter(f: &mut Frame, area: Rect) {
let chart = Chart::new(datasets)
.block(
Block::bordered().title(
Block::new().borders(Borders::all()).title(
Title::default()
.content("Scatter chart".cyan().bold())
.alignment(Alignment::Center),

View File

@@ -23,18 +23,14 @@ 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::{
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,
prelude::*,
widgets::{Block, Borders, Paragraph},
};
@@ -234,12 +230,12 @@ fn render_indexed_colors(frame: &mut Frame, area: Rect) {
}
fn title_block(title: String) -> Block<'static> {
Block::new()
Block::default()
.borders(Borders::TOP)
.title_alignment(Alignment::Center)
.border_style(Style::new().dark_gray())
.title_style(Style::new().reset())
.title(title)
.title_alignment(Alignment::Center)
.title_style(Style::new().reset())
}
fn render_indexed_grayscale(frame: &mut Frame, area: Rect) {

View File

@@ -33,21 +33,13 @@ use std::{
};
use color_eyre::{config::HookBuilder, eyre, Result};
use palette::{convert::FromColorUnclamped, Okhsv, Srgb};
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,
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::*;
#[derive(Debug, Default)]
struct App {
@@ -149,7 +141,8 @@ impl App {
/// to update the colors to render.
impl Widget for &mut App {
fn render(self, area: Rect, buf: &mut Buffer) {
use Constraint::{Length, Min};
#[allow(clippy::enum_glob_use)]
use Constraint::*;
let [top, colors] = Layout::vertical([Length(1), Min(0)]).areas(area);
let [title, fps] = Layout::horizontal([Min(0), Length(8)]).areas(top);
Text::from("colors_rgb example. Press q to quit")

View File

@@ -13,30 +13,23 @@
//! [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::{
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},
layout::{Constraint::*, Flex},
prelude::*,
style::palette::tailwind::*,
symbols::line,
widgets::{Block, Paragraph, Wrap},
};
use strum::{Display, EnumIter, FromRepr};
@@ -130,23 +123,24 @@ impl App {
}
fn handle_events(&mut self) -> Result<()> {
use KeyCode::*;
match event::read()? {
Event::Key(key) if key.kind == KeyEventKind::Press => match key.code {
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(),
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(),
_ => {}
},
_ => {}
@@ -442,7 +436,7 @@ impl ConstraintBlock {
} else {
main_color
};
Block::new()
Block::default()
.fg(Self::TEXT_COLOR)
.bg(selected_color)
.render(area, buf);

View File

@@ -13,30 +13,17 @@
//! [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 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 crossterm::{
event::{self, Event, KeyCode},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
};
use ratatui::{layout::Constraint::*, prelude::*, style::palette::tailwind, widgets::*};
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
const SPACER_HEIGHT: u16 = 0;
@@ -121,17 +108,15 @@ impl App {
fn handle_events(&mut self) -> Result<()> {
if let Event::Key(key) = event::read()? {
if key.kind != KeyEventKind::Press {
return Ok(());
}
use KeyCode::*;
match key.code {
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(),
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(),
_ => (),
}
}
@@ -209,7 +194,7 @@ impl App {
);
Paragraph::new(width_bar.dark_gray())
.centered()
.block(Block::new().padding(Padding {
.block(Block::default().padding(Padding {
left: 0,
right: 0,
top: 1,

View File

@@ -15,23 +15,15 @@
use std::{error::Error, io, ops::ControlFlow, time::Duration};
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},
use crossterm::{
event::{
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, MouseButton, MouseEvent,
MouseEventKind,
},
layout::{Constraint, Layout, Rect},
style::{Color, Style},
terminal::{Frame, Terminal},
text::Line,
widgets::{Paragraph, Widget},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{prelude::*, widgets::Paragraph};
/// A custom widget that renders a button with a label, theme and state.
#[derive(Debug, Clone)]

View File

@@ -4,15 +4,12 @@ use std::{
time::{Duration, Instant},
};
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 crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::prelude::*;
use crate::{app::App, ui};

View File

@@ -1,14 +1,11 @@
use std::{error::Error, io, sync::mpsc, thread, time::Duration};
use ratatui::{
backend::{Backend, TermionBackend},
terminal::Terminal,
termion::{
event::Key,
input::{MouseTerminal, TermRead},
raw::IntoRawMode,
screen::IntoAlternateScreen,
},
use ratatui::prelude::*;
use termion::{
event::Key,
input::{MouseTerminal, TermRead},
raw::IntoRawMode,
screen::IntoAlternateScreen,
};
use crate::{app::App, ui};

View File

@@ -3,13 +3,10 @@ use std::{
time::{Duration, Instant},
};
use ratatui::{
backend::TermwizBackend,
terminal::Terminal,
termwiz::{
input::{InputEvent, KeyCode},
terminal::Terminal as TermwizTerminal,
},
use ratatui::prelude::*;
use termwiz::{
input::{InputEvent, KeyCode},
terminal::Terminal as TermwizTerminal,
};
use crate::{app::App, ui};

View File

@@ -1,14 +1,7 @@
#[allow(clippy::wildcard_imports)]
use ratatui::{
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,
},
prelude::*,
widgets::{canvas::*, *},
};
use crate::app::App;
@@ -21,7 +14,7 @@ pub fn draw(f: &mut Frame, app: &mut App) {
.iter()
.map(|t| text::Line::from(Span::styled(*t, Style::default().fg(Color::Green))))
.collect::<Tabs>()
.block(Block::bordered().title(app.title))
.block(Block::default().borders(Borders::ALL).title(app.title))
.highlight_style(Style::default().fg(Color::Yellow))
.select(app.tabs.index);
f.render_widget(tabs, chunks[0]);
@@ -53,12 +46,12 @@ fn draw_gauges(f: &mut Frame, app: &mut App, area: Rect) {
])
.margin(1)
.split(area);
let block = Block::bordered().title("Graphs");
let block = Block::default().borders(Borders::ALL).title("Graphs");
f.render_widget(block, area);
let label = format!("{:.2}%", app.progress * 100.0);
let gauge = Gauge::default()
.block(Block::new().title("Gauge:"))
.block(Block::default().title("Gauge:"))
.gauge_style(
Style::default()
.fg(Color::Magenta)
@@ -71,7 +64,7 @@ fn draw_gauges(f: &mut Frame, app: &mut App, area: Rect) {
f.render_widget(gauge, chunks[0]);
let sparkline = Sparkline::default()
.block(Block::new().title("Sparkline:"))
.block(Block::default().title("Sparkline:"))
.style(Style::default().fg(Color::Green))
.data(&app.sparkline.points)
.bar_set(if app.enhanced_graphics {
@@ -82,8 +75,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::new().title("LineGauge:"))
.filled_style(Style::default().fg(Color::Magenta))
.block(Block::default().title("LineGauge:"))
.gauge_style(Style::default().fg(Color::Magenta))
.line_set(if app.enhanced_graphics {
symbols::line::THICK
} else {
@@ -117,7 +110,7 @@ fn draw_charts(f: &mut Frame, app: &mut App, area: Rect) {
.map(|i| ListItem::new(vec![text::Line::from(Span::raw(*i))]))
.collect();
let tasks = List::new(tasks)
.block(Block::bordered().title("List"))
.block(Block::default().borders(Borders::ALL).title("List"))
.highlight_style(Style::default().add_modifier(Modifier::BOLD))
.highlight_symbol("> ");
f.render_stateful_widget(tasks, chunks[0], &mut app.tasks.state);
@@ -145,12 +138,12 @@ fn draw_charts(f: &mut Frame, app: &mut App, area: Rect) {
ListItem::new(content)
})
.collect();
let logs = List::new(logs).block(Block::bordered().title("List"));
let logs = List::new(logs).block(Block::default().borders(Borders::ALL).title("List"));
f.render_stateful_widget(logs, chunks[1], &mut app.logs.state);
}
let barchart = BarChart::default()
.block(Block::bordered().title("Bar chart"))
.block(Block::default().borders(Borders::ALL).title("Bar chart"))
.data(&app.barchart)
.bar_width(3)
.bar_gap(2)
@@ -202,12 +195,14 @@ fn draw_charts(f: &mut Frame, app: &mut App, area: Rect) {
];
let chart = Chart::new(datasets)
.block(
Block::bordered().title(Span::styled(
"Chart",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
)),
Block::default()
.title(Span::styled(
"Chart",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
))
.borders(Borders::ALL),
)
.x_axis(
Axis::default()
@@ -259,7 +254,7 @@ fn draw_text(f: &mut Frame, area: Rect) {
"One more thing is that it should display unicode characters: 10€"
),
];
let block = Block::bordered().title(Span::styled(
let block = Block::default().borders(Borders::ALL).title(Span::styled(
"Footer",
Style::default()
.fg(Color::Magenta)
@@ -297,11 +292,11 @@ fn draw_second_tab(f: &mut Frame, app: &mut App, area: Rect) {
.style(Style::default().fg(Color::Yellow))
.bottom_margin(1),
)
.block(Block::bordered().title("Servers"));
.block(Block::default().title("Servers").borders(Borders::ALL));
f.render_widget(table, chunks[0]);
let map = Canvas::default()
.block(Block::bordered().title("World"))
.block(Block::default().title("World").borders(Borders::ALL))
.paint(|ctx| {
ctx.draw(&Map {
color: Color::White,
@@ -395,6 +390,6 @@ fn draw_third_tab(f: &mut Frame, _app: &mut App, area: Rect) {
Constraint::Ratio(1, 3),
],
)
.block(Block::bordered().title("Colors"));
.block(Block::default().title("Colors").borders(Borders::ALL));
f.render_widget(table, chunks[0]);
}

View File

@@ -1,24 +1,12 @@
use std::time::Duration;
use color_eyre::{eyre::Context, Result};
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind};
use itertools::Itertools;
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 ratatui::{prelude::*, widgets::*};
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
use crate::{
destroy,
tabs::{AboutTab, EmailTab, RecipeTab, TracerouteTab, WeatherTab},
term, THEME,
};
use crate::{destroy, tabs::*, term, THEME};
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct App {
@@ -50,10 +38,14 @@ enum Tab {
}
pub fn run(terminal: &mut Terminal<impl Backend>) -> Result<()> {
App::default().run(terminal)
App::new().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() {
@@ -94,13 +86,14 @@ impl App {
}
fn handle_key_press(&mut self, key: KeyEvent) {
use KeyCode::*;
match key.code {
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(),
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(),
_ => {}
};
}

View File

@@ -20,16 +20,7 @@
//!
//! ```rust
//! use anyhow::Result;
//! 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 ratatui::prelude::*;
//! use tui_big_text::{BigTextBuilder, PixelSize};
//!
//! fn render(frame: &mut Frame) -> Result<()> {
@@ -59,13 +50,7 @@ use std::cmp::min;
use derive_builder::Builder;
use font8x8::UnicodeFonts;
use ratatui::{
buffer::Buffer,
layout::Rect,
style::Style,
text::{Line, StyledGrapheme},
widgets::Widget,
};
use ratatui::{prelude::*, text::StyledGrapheme};
#[allow(unused)]
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Default)]
@@ -94,16 +79,7 @@ pub enum PixelSize {
/// # Examples
///
/// ```rust
/// 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 ratatui::prelude::*;
/// use tui_big_text::{BigTextBuilder, PixelSize};
///
/// BigText::builder()
@@ -300,7 +276,7 @@ fn render_glyph(glyph: [u8; 8], area: Rect, buf: &mut Buffer, pixel_size: PixelS
#[cfg(test)]
mod tests {
use ratatui::style::Stylize;
use ratatui::assert_buffer_eq;
use super::*;
@@ -332,7 +308,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([
let expected = Buffer::with_lines(vec![
" ████ ██ ███ ████ ██ ",
"██ ██ ██ ██ ",
"███ ███ █████ ███ ██ ██ ████ ██ ███ █████ ████ ",
@@ -342,7 +318,7 @@ mod tests {
" ████ ████ ██ ██ ██ ████ ████ ███████ ████ ██ ██ ████ ",
" █████ ",
]);
assert_eq!(buf, expected);
assert_buffer_eq!(buf, expected);
Ok(())
}
@@ -353,7 +329,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([
let expected = Buffer::with_lines(vec![
"██████ █ ███",
"█ ██ █ ██ ██",
" ██ ██ ███ ██ ██ █████ ████ ████ █████ ████ ██",
@@ -361,7 +337,7 @@ mod tests {
" ██ ██ ██ ██ ██ ██ ██ ██ █████ ██ ██████ ██ ██",
" ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ █ ██ ██ ██",
]);
assert_eq!(buf, expected);
assert_buffer_eq!(buf, expected);
Ok(())
}
@@ -372,7 +348,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([
let expected = Buffer::with_lines(vec![
"██ ██ ███ █ ██ ",
"███ ███ ██ ██ ",
"███████ ██ ██ ██ █████ ███ ",
@@ -390,7 +366,7 @@ mod tests {
"███████ ████ ██ ██ ████ █████ ",
" ",
]);
assert_eq!(buf, expected);
assert_buffer_eq!(buf, expected);
Ok(())
}
@@ -402,17 +378,18 @@ mod tests {
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 48, 8));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines([
" ████ █ ███ ███ ".bold(),
"██ ██ ██ ██ ██ ".bold(),
"███ █████ ██ ██ ██ ████ ██ ".bold(),
" ███ ██ ██ ██ ██ ██ ██ █████ ".bold(),
" ███ ██ ██ ██ ██ ██████ ██ ██ ".bold(),
"██ ██ ██ █ █████ ██ ██ ██ ██ ".bold(),
" ████ ██ ██ ████ ████ ███ ██ ".bold(),
" █████ ".bold(),
let mut expected = Buffer::with_lines(vec![
" ████ █ ███ ███ ",
"██ ██ ██ ██ ██ ",
"███ █████ ██ ██ ██ ████ ██ ",
" ███ ██ ██ ██ ██ ██ ██ █████ ",
" ███ ██ ██ ██ ██ ██████ ██ ██ ",
"██ ██ ██ █ █████ ██ ██ ██ ██ ",
" ████ ██ ██ ████ ████ ███ ██ ",
" █████ ",
]);
assert_eq!(buf, expected);
expected.set_style(Rect::new(0, 0, 48, 8), Style::new().bold());
assert_buffer_eq!(buf, expected);
Ok(())
}
@@ -427,7 +404,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([
let mut expected = Buffer::with_lines(vec![
"██████ ███ ",
" ██ ██ ██ ",
" ██ ██ ████ ██ ",
@@ -456,7 +433,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_eq!(buf, expected);
assert_buffer_eq!(buf, expected);
Ok(())
}
@@ -468,13 +445,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([
let expected = Buffer::with_lines(vec![
"▄█▀▀█▄ ▀▀ ▀██ ▀██▀ ▀▀ ",
"▀██▄ ▀██ ██▀▀█▄ ▄█▀▀▄█▀ ██ ▄█▀▀█▄ ██ ▀██ ██▀▀█▄ ▄█▀▀█▄ ",
"▄▄ ▀██ ██ ██ ██ ▀█▄▄██ ██ ██▀▀▀▀ ██ ▄█ ██ ██ ██ ██▀▀▀▀ ",
" ▀▀▀▀ ▀▀▀▀ ▀▀ ▀▀ ▄▄▄▄█▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀▀▀▀ ▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀ ",
]);
assert_eq!(buf, expected);
assert_buffer_eq!(buf, expected);
Ok(())
}
@@ -486,12 +463,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([
let expected = Buffer::with_lines(vec![
"█▀██▀█ ▄█ ▀██",
" ██ ▀█▄█▀█▄ ██ ██ ██▀▀█▄ ▄█▀▀█▄ ▀▀▀█▄ ▀██▀▀ ▄█▀▀█▄ ▄▄▄██",
" ██ ██ ▀▀ ██ ██ ██ ██ ██ ▄▄ ▄█▀▀██ ██ ▄ ██▀▀▀▀ ██ ██",
]);
assert_eq!(buf, expected);
assert_buffer_eq!(buf, expected);
Ok(())
}
@@ -503,7 +480,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([
let expected = Buffer::with_lines(vec![
"██▄ ▄██ ▀██ ▄█ ▀▀ ",
"███████ ██ ██ ██ ▀██▀▀ ▀██ ",
"██ ▀ ██ ██ ██ ██ ██ ▄ ██ ",
@@ -513,7 +490,7 @@ mod tests {
" ██ ▄█ ██ ██ ██ ██▀▀▀▀ ▀▀▀█▄ ",
"▀▀▀▀▀▀▀ ▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀ ▀▀▀▀▀ ",
]);
assert_eq!(buf, expected);
assert_buffer_eq!(buf, expected);
Ok(())
}
@@ -526,13 +503,14 @@ mod tests {
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 48, 4));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines([
"▄█▀▀█▄ ▄█ ▀██ ▀██ ".bold(),
"▀██▄ ▀██▀▀ ██ ██ ██ ▄█▀▀█▄ ▄▄▄██ ".bold(),
"▄▄ ▀██ ██ ▄ ▀█▄▄██ ██ ██▀▀▀▀ ██ ██ ".bold(),
" ▀▀▀▀ ▀▀ ▄▄▄▄█▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀ ▀▀ ".bold(),
let mut expected = Buffer::with_lines(vec![
"▄█▀▀█▄ ▄█ ▀██ ▀██ ",
"▀██▄ ▀██▀▀ ██ ██ ██ ▄█▀▀█▄ ▄▄▄██ ",
"▄▄ ▀██ ██ ▄ ▀█▄▄██ ██ ██▀▀▀▀ ██ ██ ",
" ▀▀▀▀ ▀▀ ▄▄▄▄█▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀ ▀▀ ",
]);
assert_eq!(buf, expected);
expected.set_style(Rect::new(0, 0, 48, 4), Style::new().bold());
assert_buffer_eq!(buf, expected);
Ok(())
}
@@ -548,7 +526,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([
let mut expected = Buffer::with_lines(vec![
"▀██▀▀█▄ ▀██ ",
" ██▄▄█▀ ▄█▀▀█▄ ▄▄▄██ ",
" ██ ▀█▄ ██▀▀▀▀ ██ ██ ",
@@ -565,7 +543,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_eq!(buf, expected);
assert_buffer_eq!(buf, expected);
Ok(())
}
@@ -577,7 +555,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([
let expected = Buffer::with_lines(vec![
"▐█▌ █ ▐█ ██ █ ",
"█ █ █ ▐▌ ",
"█▌ ▐█ ██▌ ▐█▐▌ █ ▐█▌ ▐▌ ▐█ ██▌ ▐█▌ ",
@@ -587,7 +565,7 @@ mod tests {
"▐█▌ ▐█▌ █ █ █ ▐█▌ ▐█▌ ███▌▐█▌ █ █ ▐█▌ ",
" ██▌ ",
]);
assert_eq!(buf, expected);
assert_buffer_eq!(buf, expected);
Ok(())
}
@@ -599,7 +577,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([
let expected = Buffer::with_lines(vec![
"███ ▐ ▐█",
"▌█▐ █ █",
" █ █▐█ █ █ ██▌ ▐█▌ ▐█▌ ▐██ ▐█▌ █",
@@ -607,7 +585,7 @@ mod tests {
" █ ▐▌▐▌█ █ █ █ █ ▐██ █ ███ █ █",
" █ ▐▌ █ █ █ █ █ █ █ █ █▐ █ █ █",
]);
assert_eq!(buf, expected);
assert_buffer_eq!(buf, expected);
Ok(())
}
@@ -619,7 +597,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([
let expected = Buffer::with_lines(vec![
"█ ▐▌ ▐█ ▐ █ ",
"█▌█▌ █ █ ",
"███▌█ █ █ ▐██ ▐█ ",
@@ -637,7 +615,7 @@ mod tests {
"███▌▐█▌ █ █ ▐█▌ ██▌ ",
" ",
]);
assert_eq!(buf, expected);
assert_buffer_eq!(buf, expected);
Ok(())
}
@@ -650,17 +628,18 @@ mod tests {
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 24, 8));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines([
"▐█▌ ▐ ▐█ ▐█ ".bold(),
"█ █ █ █ █ ".bold(),
"█▌ ▐██ █ █ █ ▐█▌ █ ".bold(),
"▐█ █ █ █ █ █ █ ▐██ ".bold(),
" ▐█ █ █ █ █ ███ █ █ ".bold(),
"█ █ █▐ ▐██ █ █ █ █ ".bold(),
"▐█▌ ▐▌ █ ▐█▌ ▐█▌ ▐█▐▌".bold(),
" ██▌ ".bold(),
let mut expected = Buffer::with_lines(vec![
"▐█▌ ▐ ▐█ ▐█ ",
"█ █ █ █ █ ",
"█▌ ▐██ █ █ █ ▐█▌ █ ",
"▐█ █ █ █ █ █ █ ▐██ ",
" ▐█ █ █ █ █ ███ █ █ ",
"█ █ █▐ ▐██ █ █ █ █ ",
"▐█▌ ▐▌ █ ▐█▌ ▐█▌ ▐█▐▌",
" ██▌ ",
]);
assert_eq!(buf, expected);
expected.set_style(Rect::new(0, 0, 24, 8), Style::new().bold());
assert_buffer_eq!(buf, expected);
Ok(())
}
@@ -676,7 +655,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([
let mut expected = Buffer::with_lines(vec![
"███ ▐█ ",
"▐▌▐▌ █ ",
"▐▌▐▌▐█▌ █ ",
@@ -705,7 +684,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_eq!(buf, expected);
assert_buffer_eq!(buf, expected);
Ok(())
}
@@ -738,13 +717,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([
let expected = Buffer::with_lines(vec![
"▟▀▙ ▀ ▝█ ▜▛ ▀ ",
"▜▙ ▝█ █▀▙ ▟▀▟▘ █ ▟▀▙ ▐▌ ▝█ █▀▙ ▟▀▙ ",
"▄▝█ █ █ █ ▜▄█ █ █▀▀ ▐▌▗▌ █ █ █ █▀▀ ",
"▝▀▘ ▝▀▘ ▀ ▀ ▄▄▛ ▝▀▘ ▝▀▘ ▀▀▀▘▝▀▘ ▀ ▀ ▝▀▘ ",
]);
assert_eq!(buf, expected);
assert_buffer_eq!(buf, expected);
Ok(())
}
@@ -756,12 +735,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([
let expected = Buffer::with_lines(vec![
"▛█▜ ▟ ▝█",
" █ ▜▟▜▖█ █ █▀▙ ▟▀▙ ▝▀▙ ▝█▀ ▟▀▙ ▗▄█",
" █ ▐▌▝▘█ █ █ █ █ ▄ ▟▀█ █▗ █▀▀ █ █",
]);
assert_eq!(buf, expected);
assert_buffer_eq!(buf, expected);
Ok(())
}
@@ -773,7 +752,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([
let expected = Buffer::with_lines(vec![
"█▖▟▌ ▝█ ▟ ▀ ",
"███▌█ █ █ ▝█▀ ▝█ ",
"█▝▐▌█ █ █ █▗ █ ",
@@ -783,7 +762,7 @@ mod tests {
"▐▌▗▌ █ █ █ █▀▀ ▝▀▙ ",
"▀▀▀▘▝▀▘ ▀ ▀ ▝▀▘ ▀▀▘ ",
]);
assert_eq!(buf, expected);
assert_buffer_eq!(buf, expected);
Ok(())
}
@@ -796,13 +775,14 @@ mod tests {
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 24, 4));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines([
"▟▀▙ ▟ ▝█ ▝█ ".bold(),
"▜▙ ▝█▀ █ █ █ ▟▀▙ ▗▄█ ".bold(),
"▄▝█ █▗ ▜▄█ █ █▀▀ █ █ ".bold(),
"▝▀▘ ▝▘ ▄▄▛ ▝▀▘ ▝▀▘ ▝▀▝▘".bold(),
let mut expected = Buffer::with_lines(vec![
"▟▀▙ ▟ ▝█ ▝█ ",
"▜▙ ▝█▀ █ █ █ ▟▀▙ ▗▄█ ",
"▄▝█ █▗ ▜▄█ █ █▀▀ █ █ ",
"▝▀▘ ▝▘ ▄▄▛ ▝▀▘ ▝▀▘ ▝▀▝▘",
]);
assert_eq!(buf, expected);
expected.set_style(Rect::new(0, 0, 24, 4), Style::new().bold());
assert_buffer_eq!(buf, expected);
Ok(())
}
@@ -818,7 +798,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([
let mut expected = Buffer::with_lines(vec![
"▜▛▜▖ ▝█ ",
"▐▙▟▘▟▀▙ ▗▄█ ",
"▐▌▜▖█▀▀ █ █ ",
@@ -835,7 +815,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_eq!(buf, expected);
assert_buffer_eq!(buf, expected);
Ok(())
}
}

View File

@@ -1,5 +1,5 @@
use palette::{IntoColor, Okhsv, Srgb};
use ratatui::{buffer::Buffer, layout::Rect, style::Color, widgets::Widget};
use ratatui::prelude::*;
/// A widget that renders a color swatch of RGB colors.
///

View File

@@ -1,12 +1,6 @@
use rand::Rng;
use rand_chacha::rand_core::SeedableRng;
use ratatui::{
buffer::Buffer,
layout::{Flex, Layout, Rect},
style::{Color, Style},
terminal::Frame,
widgets::Widget,
};
use ratatui::{buffer::Cell, layout::Flex, prelude::*};
use unicode_width::UnicodeWidthStr;
use crate::big_text::{BigTextBuilder, PixelSize};
@@ -65,7 +59,7 @@ fn drip(frame_count: usize, area: Rect, buf: &mut Buffer) {
if rng.gen_ratio(1, 10) {
*dest = src;
} else {
dest.reset();
*dest = Cell::default();
}
} else {
// move the pixel down one row

View File

@@ -14,9 +14,11 @@
//! [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::must_use_candidate,
clippy::wildcard_imports
)]
mod app;
@@ -28,17 +30,16 @@ mod tabs;
mod term;
mod theme;
pub use app::*;
use color_eyre::Result;
pub use self::{
colors::{color_from_oklab, RgbSwatch},
theme::THEME,
};
pub use colors::*;
pub use term::*;
pub use theme::*;
fn main() -> Result<()> {
errors::init_hooks()?;
let terminal = &mut term::init()?;
app::run(terminal)?;
App::new().run(terminal)?;
term::restore()?;
Ok(())
}

View File

@@ -1,9 +1,5 @@
use itertools::Itertools;
use ratatui::{
buffer::Buffer,
layout::{Alignment, Constraint, Layout, Margin, Rect},
widgets::{Block, Borders, Clear, Padding, Paragraph, Widget, Wrap},
};
use ratatui::{prelude::*, widgets::*};
use crate::{RgbSwatch, THEME};
@@ -68,16 +64,20 @@ 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,
});

View File

@@ -1,14 +1,5 @@
use itertools::Itertools;
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 ratatui::{prelude::*, widgets::*};
use unicode_width::UnicodeWidthStr;
use crate::{RgbSwatch, THEME};
@@ -68,7 +59,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,
});

View File

@@ -1,14 +1,5 @@
use itertools::Itertools;
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 ratatui::{prelude::*, widgets::*};
use crate::{RgbSwatch, THEME};
@@ -114,7 +105,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,
});
@@ -133,7 +124,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,
});

View File

@@ -1,14 +1,7 @@
use itertools::Itertools;
use ratatui::{
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,
},
prelude::*,
widgets::{canvas::*, *},
};
use crate::{RgbSwatch, THEME};
@@ -33,7 +26,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,
});
@@ -56,10 +49,10 @@ fn render_hops(selected_row: usize, area: Rect, buf: &mut Buffer) {
.iter()
.map(|hop| Row::new(vec![hop.host, hop.address]))
.collect_vec();
let block = Block::new()
.padding(Padding::new(1, 1, 1, 1))
let block = Block::default()
.title("Traceroute bad.horse".bold().white())
.title_alignment(Alignment::Center)
.title("Traceroute bad.horse".bold().white());
.padding(Padding::new(1, 1, 1, 1));
StatefulWidget::render(
Table::new(rows, [Constraint::Max(100), Constraint::Length(15)])
.header(Row::new(vec!["Host", "Address"]).set_style(THEME.traceroute.header))
@@ -111,7 +104,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: MapResolution::High,
resolution: canvas::MapResolution::High,
color: theme.color,
};
Canvas::default()

View File

@@ -1,14 +1,8 @@
use itertools::Itertools;
use palette::Okhsv;
use ratatui::{
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,
},
prelude::*,
widgets::{calendar::CalendarEventStore, *},
};
use time::OffsetDateTime;
@@ -34,14 +28,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,
});
@@ -65,7 +59,7 @@ impl Widget for WeatherTab {
fn render_calendar(area: Rect, buf: &mut Buffer) {
let date = OffsetDateTime::now_utc().date();
Monthly::new(date, CalendarEventStore::today(Style::new().red().bold()))
calendar::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())
@@ -146,8 +140,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 filled_color = color_from_oklab(hue, Okhsv::max_saturation(), value);
let unfilled_color = color_from_oklab(hue, Okhsv::max_saturation(), value * 0.5);
let fg = color_from_oklab(hue, Okhsv::max_saturation(), value);
let bg = color_from_oklab(hue, Okhsv::max_saturation(), value * 0.5);
let label = if percent < 100.0 {
format!("Downloading: {percent}%")
} else {
@@ -157,8 +151,7 @@ fn render_line_gauge(percent: f64, area: Rect, buf: &mut Buffer) {
.ratio(percent / 100.0)
.label(label)
.style(Style::new().light_blue())
.filled_style(Style::new().fg(filled_color))
.unfilled_style(Style::new().fg(unfilled_color))
.gauge_style(Style::new().fg(fg).bg(bg))
.line_set(symbols::line::THICK)
.render(area, buf);
}

View File

@@ -4,16 +4,12 @@ use std::{
};
use color_eyre::{eyre::WrapErr, Result};
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 crossterm::{
event::{self, Event},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
};
use ratatui::prelude::*;
pub fn init() -> Result<Terminal<impl Backend>> {
// this size is to match the size of the terminal when running the demo

View File

@@ -1,4 +1,4 @@
use ratatui::style::{Color, Modifier, Style};
use ratatui::prelude::*;
pub struct Theme {
pub root: Style,

View File

@@ -15,17 +15,12 @@
use std::io::{self, stdout};
use crossterm::{
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
};
use ratatui::{
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},
prelude::*,
widgets::{Block, Borders, Paragraph},
};
@@ -56,11 +51,13 @@ fn main() -> io::Result<()> {
fn hello_world(frame: &mut Frame) {
frame.render_widget(
Paragraph::new("Hello World!").block(Block::bordered().title("Greeting")),
Paragraph::new("Hello World!")
.block(Block::default().title("Greeting").borders(Borders::ALL)),
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()? {
@@ -90,8 +87,8 @@ fn layout(frame: &mut Frame) {
Block::new().borders(Borders::TOP).title("Status Bar"),
status_bar,
);
frame.render_widget(Block::bordered().title("Left"), left);
frame.render_widget(Block::bordered().title("Right"), right);
frame.render_widget(Block::default().borders(Borders::ALL).title("Left"), left);
frame.render_widget(Block::default().borders(Borders::ALL).title("Right"), right);
}
fn styling(frame: &mut Frame) {

View File

@@ -13,30 +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, 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::{
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,
},
layout::{Constraint::*, Flex},
prelude::*,
style::palette::tailwind,
symbols::line,
widgets::{block::Title, *},
};
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
@@ -185,17 +177,18 @@ impl App {
}
fn handle_events(&mut self) -> Result<()> {
use KeyCode::*;
match event::read()? {
Event::Key(key) if key.kind == KeyEventKind::Press => match key.code {
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(),
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(),
_ => (),
},
_ => {}
@@ -371,7 +364,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::{INDIGO, ORANGE, SKY};
use tailwind::*;
let text = value.to_string();
let color = match value {
Self::Legacy => ORANGE.c400,
@@ -516,7 +509,7 @@ impl Example {
}
const fn color_for_constraint(constraint: Constraint) -> Color {
use tailwind::{BLUE, SLATE};
use tailwind::*;
match constraint {
Constraint::Min(_) => BLUE.c900,
Constraint::Max(_) => BLUE.c800,

View File

@@ -13,22 +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::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::{
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},
prelude::*,
style::palette::tailwind,
widgets::{block::Title, Block, Borders, Gauge, Padding, Paragraph},
};
const GAUGE1_COLOR: Color = tailwind::RED.c800;
@@ -101,9 +99,10 @@ impl App {
if event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
use KeyCode::*;
match key.code {
KeyCode::Char(' ') | KeyCode::Enter => self.start(),
KeyCode::Char('q') | KeyCode::Esc => self.quit(),
Char(' ') | Enter => self.start(),
Char('q') | Esc => self.quit(),
_ => {}
}
}
@@ -124,7 +123,7 @@ impl App {
impl Widget for &App {
#[allow(clippy::similar_names)]
fn render(self, area: Rect, buf: &mut Buffer) {
use Constraint::{Length, Min, Ratio};
use Constraint::*;
let layout = Layout::vertical([Length(2), Min(0), Length(1)]);
let [header_area, gauge_area, footer_area] = layout.areas(area);
@@ -207,11 +206,11 @@ impl App {
fn title_block(title: &str) -> Block {
let title = Title::from(title).alignment(Alignment::Center);
Block::new()
.borders(Borders::NONE)
.padding(Padding::vertical(1))
Block::default()
.title(title)
.borders(Borders::NONE)
.fg(CUSTOM_LABEL_COLOR)
.padding(Padding::vertical(1))
}
fn init_error_hooks() -> color_eyre::Result<()> {

View File

@@ -19,16 +19,12 @@ use std::{
};
use anyhow::{Context, Result};
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 crossterm::{
event::{self, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
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

View File

@@ -13,6 +13,8 @@
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
#![allow(clippy::wildcard_imports)]
use std::{
collections::{BTreeMap, VecDeque},
error::Error,
@@ -23,16 +25,7 @@ use std::{
};
use rand::distributions::{Distribution, Uniform};
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,
};
use ratatui::{prelude::*, widgets::*};
const NUM_DOWNLOADS: usize = 10;
@@ -243,7 +236,7 @@ fn run_app<B: Backend>(
fn ui(f: &mut Frame, downloads: &Downloads) {
let area = f.size();
let block = Block::new().title(block::Title::from("Progress").alignment(Alignment::Center));
let block = Block::default().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);
@@ -255,7 +248,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()
.filled_style(Style::default().fg(Color::Blue))
.gauge_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);

View File

@@ -13,25 +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::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::{
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},
layout::Constraint::*,
prelude::*,
widgets::{Block, Borders, Paragraph},
};
fn main() -> Result<(), Box<dyn Error>> {
@@ -195,9 +190,10 @@ fn render_example_combination(
title: &str,
constraints: Vec<(Constraint, Constraint)>,
) {
let block = Block::bordered()
let block = Block::default()
.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);

View File

@@ -1,218 +0,0 @@
//! # [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(())
}

View File

@@ -13,26 +13,17 @@
//! [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 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 crossterm::{
event::{self, Event, KeyCode, KeyEventKind},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
};
use ratatui::{prelude::*, style::palette::tailwind, widgets::*};
const TODO_HEADER_BG: Color = tailwind::BLUE.c950;
const NORMAL_ROW_COLOR: Color = tailwind::SLATE.c950;
@@ -47,25 +38,15 @@ enum Status {
Completed,
}
struct TodoItem {
todo: String,
info: String,
struct TodoItem<'a> {
todo: &'a str,
info: &'a str,
status: Status,
}
impl TodoItem {
fn new(todo: &str, info: &str, status: Status) -> Self {
Self {
todo: todo.to_string(),
info: info.to_string(),
status,
}
}
}
struct TodoList {
struct StatefulList<'a> {
state: ListState,
items: Vec<TodoItem>,
items: Vec<TodoItem<'a>>,
last_selected: Option<usize>,
}
@@ -75,8 +56,8 @@ struct TodoList {
///
/// 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 {
items: TodoList,
struct App<'a> {
items: StatefulList<'a>,
}
fn main() -> Result<(), Box<dyn Error>> {
@@ -121,10 +102,10 @@ fn restore_terminal() -> color_eyre::Result<()> {
Ok(())
}
impl App {
impl<'a> App<'a> {
fn new() -> Self {
Self {
items: TodoList::with_items(&[
items: StatefulList::with_items([
("Rewrite everything with Rust!", "I can't hold my inner voice. He tells me to rewrite the complete universe with Rust", Status::Todo),
("Rewrite all of your tui apps with Ratatui", "Yes, you heard that right. Go and replace your tui with Ratatui.", Status::Completed),
("Pet your cat", "Minnak loves to be pet by you! Don't forget to pet and give some treats!", Status::Todo),
@@ -154,23 +135,22 @@ impl App {
}
}
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 {
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(),
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(),
_ => {}
}
}
@@ -184,7 +164,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([
@@ -206,16 +186,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::new()
let outer_block = Block::default()
.borders(Borders::NONE)
.title_alignment(Alignment::Center)
.title("TODO List")
.fg(TEXT_COLOR)
.bg(TODO_HEADER_BG);
let inner_block = Block::new()
.bg(TODO_HEADER_BG)
.title("TODO List")
.title_alignment(Alignment::Center);
let inner_block = Block::default()
.borders(Borders::NONE)
.fg(TEXT_COLOR)
.bg(NORMAL_ROW_COLOR);
@@ -258,24 +238,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 => format!("✓ DONE: {}", self.items.items[i].info),
Status::Todo => format!("TODO: {}", self.items.items[i].info),
Status::Completed => "✓ DONE: ".to_string() + self.items.items[i].info,
Status::Todo => "TODO: ".to_string() + 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::new()
let outer_info_block = Block::default()
.borders(Borders::NONE)
.title_alignment(Alignment::Center)
.title("TODO Info")
.fg(TEXT_COLOR)
.bg(TODO_HEADER_BG);
let inner_info_block = Block::new()
.bg(TODO_HEADER_BG)
.title("TODO Info")
.title_alignment(Alignment::Center);
let inner_info_block = Block::default()
.borders(Borders::NONE)
.padding(Padding::horizontal(1))
.bg(NORMAL_ROW_COLOR);
.bg(NORMAL_ROW_COLOR)
.padding(Padding::horizontal(1));
// 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.
@@ -308,14 +288,11 @@ fn render_footer(area: Rect, buf: &mut Buffer) {
.render(area, buf);
}
impl TodoList {
fn with_items(items: &[(&str, &str, Status)]) -> Self {
Self {
impl StatefulList<'_> {
fn with_items<'a>(items: [(&'a str, &'a str, Status); 6]) -> StatefulList<'a> {
StatefulList {
state: ListState::default(),
items: items
.iter()
.map(|(todo, info, status)| TodoItem::new(todo, info, *status))
.collect(),
items: items.iter().map(TodoItem::from).collect(),
last_selected: None,
}
}
@@ -356,7 +333,7 @@ impl TodoList {
}
}
impl TodoItem {
impl TodoItem<'_> {
fn to_list_item(&self, index: usize) -> ListItem {
let bg_color = match index % 2 {
0 => NORMAL_ROW_COLOR,
@@ -373,3 +350,13 @@ 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,
}
}
}

View File

@@ -1,48 +0,0 @@
//! # [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(())
}

View File

@@ -25,20 +25,13 @@ use std::{
time::Duration,
};
use itertools::Itertools;
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,
use crossterm::{
event::{self, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use itertools::Itertools;
use ratatui::{prelude::*, widgets::Paragraph};
type Result<T> = result::Result<T, Box<dyn Error>>;

View File

@@ -31,15 +31,13 @@
use std::{error::Error, io};
use crossterm::{
event::{self, Event, KeyCode},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
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},
prelude::*,
widgets::{Block, Borders, Paragraph},
};
type Result<T> = std::result::Result<T, Box<dyn Error>>;
@@ -144,9 +142,11 @@ fn ui(f: &mut Frame, app: &App) {
Line::from("try first without the panic handler to see the difference"),
];
let paragraph = Paragraph::new(text)
.block(Block::bordered().title("Panic Handler Demo"))
.centered();
let b = Block::default()
.title("Panic Handler Demo")
.borders(Borders::ALL);
f.render_widget(paragraph, f.size());
let p = Paragraph::new(text).block(b).centered();
f.render_widget(p, f.size());
}

View File

@@ -19,18 +19,14 @@ 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::{Constraint, Layout},
style::{Color, Modifier, Style, Stylize},
terminal::{Frame, Terminal},
text::{Line, Masked, Span},
widgets::{Block, Paragraph, Wrap},
prelude::*,
widgets::{Block, Borders, Paragraph, Wrap},
};
struct App {
@@ -109,7 +105,7 @@ fn ui(f: &mut Frame, app: &App) {
let mut long_line = s.repeat(usize::from(size.width) / s.len() + 4);
long_line.push('\n');
let block = Block::new().black();
let block = Block::default().black();
f.render_widget(block, size);
let layout = Layout::vertical([Constraint::Ratio(1, 4); 4]).split(size);
@@ -131,7 +127,8 @@ fn ui(f: &mut Frame, app: &App) {
];
let create_block = |title| {
Block::bordered()
Block::default()
.borders(Borders::ALL)
.style(Style::default().fg(Color::Gray))
.title(Span::styled(
title,

View File

@@ -18,17 +18,14 @@
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::{
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},
prelude::*,
widgets::{Block, Borders, Clear, Paragraph, Wrap},
};
struct App {
@@ -101,11 +98,14 @@ fn ui(f: &mut Frame, app: &App) {
.wrap(Wrap { trim: true });
f.render_widget(paragraph, instructions);
let block = Block::bordered().title("Content").on_blue();
let block = Block::default()
.title("Content")
.borders(Borders::ALL)
.on_blue();
f.render_widget(block, content);
if app.show_popup {
let block = Block::bordered().title("Popup");
let block = Block::default().title("Popup").borders(Borders::ALL);
let area = centered_rect(60, 20, area);
f.render_widget(Clear, area); //this clears out the background
f.render_widget(block, area);

View File

@@ -19,15 +19,10 @@ use std::{
time::Duration,
};
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use indoc::indoc;
use itertools::izip;
use ratatui::{
backend::{Backend, CrosstermBackend},
crossterm::terminal::{disable_raw_mode, enable_raw_mode},
terminal::{Terminal, Viewport},
widgets::Paragraph,
TerminalOptions,
};
use ratatui::{prelude::*, widgets::Paragraph};
/// A fun example of using half block characters to draw a logo
#[allow(clippy::many_single_char_names)]

View File

@@ -14,6 +14,7 @@
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
#![warn(clippy::pedantic)]
#![allow(clippy::wildcard_imports)]
use std::{
error::Error,
@@ -21,20 +22,12 @@ use std::{
time::{Duration, Instant},
};
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 crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{prelude::*, symbols::scrollbar, widgets::*};
#[derive(Default)]
struct App {
@@ -193,7 +186,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,
}),
@@ -211,7 +204,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,
}),
@@ -229,7 +222,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,
}),

View File

@@ -19,20 +19,17 @@ 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::{
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},
prelude::*,
widgets::{Block, Borders, Sparkline},
};
@@ -154,18 +151,18 @@ fn ui(f: &mut Frame, app: &App) {
.split(f.size());
let sparkline = Sparkline::default()
.block(
Block::new()
.borders(Borders::LEFT | Borders::RIGHT)
.title("Data1"),
Block::default()
.title("Data1")
.borders(Borders::LEFT | Borders::RIGHT),
)
.data(&app.data1)
.style(Style::default().fg(Color::Yellow));
f.render_widget(sparkline, chunks[0]);
let sparkline = Sparkline::default()
.block(
Block::new()
.borders(Borders::LEFT | Borders::RIGHT)
.title("Data2"),
Block::default()
.title("Data2")
.borders(Borders::LEFT | Borders::RIGHT),
)
.data(&app.data2)
.style(Style::default().bg(Color::Green));
@@ -173,9 +170,9 @@ fn ui(f: &mut Frame, app: &App) {
// Multiline
let sparkline = Sparkline::default()
.block(
Block::new()
.borders(Borders::LEFT | Borders::RIGHT)
.title("Data3"),
Block::default()
.title("Data3")
.borders(Borders::LEFT | Borders::RIGHT),
)
.data(&app.data3)
.style(Style::default().fg(Color::Red));

View File

@@ -13,25 +13,17 @@
//! [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 itertools::Itertools;
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 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 style::palette::tailwind;
use unicode_width::UnicodeWidthStr;
@@ -220,12 +212,13 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
use KeyCode::*;
match key.code {
KeyCode::Char('q') | 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(),
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(),
_ => {}
}
}
@@ -325,7 +318,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,
}),
@@ -338,9 +331,10 @@ 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::bordered()
.border_type(BorderType::Double)
.border_style(Style::new().fg(app.colors.footer_border_color)),
Block::default()
.borders(Borders::ALL)
.border_style(Style::new().fg(app.colors.footer_border_color))
.border_type(BorderType::Double),
);
f.render_widget(info_footer, area);
}

View File

@@ -13,24 +13,17 @@
//! [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 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 crossterm::{
event::{self, Event, KeyCode, KeyEventKind},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
};
use ratatui::{prelude::*, style::palette::tailwind, widgets::*};
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
#[derive(Default)]
@@ -84,10 +77,11 @@ 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 {
KeyCode::Char('l') | KeyCode::Right => self.next_tab(),
KeyCode::Char('h') | KeyCode::Left => self.previous_tab(),
KeyCode::Char('q') | KeyCode::Esc => self.quit(),
Char('l') | Right => self.next_tab(),
Char('h') | Left => self.previous_tab(),
Char('q') | Esc => self.quit(),
_ => {}
}
}
@@ -126,7 +120,7 @@ impl SelectedTab {
impl Widget for &App {
fn render(self, area: Rect, buf: &mut Buffer) {
use Constraint::{Length, Min};
use Constraint::*;
let vertical = Layout::vertical([Length(1), Min(0), Length(1)]);
let [header_area, inner_area, footer_area] = vertical.areas(area);
@@ -211,7 +205,8 @@ impl SelectedTab {
/// A block surrounding the tab's content
fn block(self) -> Block<'static> {
Block::bordered()
Block::default()
.borders(Borders::ALL)
.border_set(symbols::border::PROPORTIONAL_TALL)
.padding(Padding::horizontal(1))
.border_style(self.palette().c700)

View File

@@ -29,18 +29,14 @@
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::{
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},
prelude::*,
widgets::{Block, Borders, List, ListItem, Paragraph},
};
enum InputMode {
@@ -53,7 +49,7 @@ struct App {
/// Current value of the input box
input: String,
/// Position of cursor in the editor area.
character_index: usize,
cursor_position: usize,
/// Current input mode
input_mode: InputMode,
/// History of recorded messages
@@ -66,46 +62,34 @@ impl App {
input: String::new(),
input_mode: InputMode::Normal,
messages: Vec::new(),
character_index: 0,
cursor_position: 0,
}
}
fn move_cursor_left(&mut self) {
let cursor_moved_left = self.character_index.saturating_sub(1);
self.character_index = self.clamp_cursor(cursor_moved_left);
let cursor_moved_left = self.cursor_position.saturating_sub(1);
self.cursor_position = self.clamp_cursor(cursor_moved_left);
}
fn move_cursor_right(&mut self) {
let cursor_moved_right = self.character_index.saturating_add(1);
self.character_index = self.clamp_cursor(cursor_moved_right);
let cursor_moved_right = self.cursor_position.saturating_add(1);
self.cursor_position = self.clamp_cursor(cursor_moved_right);
}
fn enter_char(&mut self, new_char: char) {
let index = self.byte_index();
self.input.insert(index, new_char);
self.input.insert(self.cursor_position, 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.character_index != 0;
let is_not_cursor_leftmost = self.cursor_position != 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.character_index;
let current_index = self.cursor_position;
let from_left_to_current_index = current_index - 1;
// Getting all characters before the selected character.
@@ -121,11 +105,11 @@ impl App {
}
fn clamp_cursor(&self, new_cursor_pos: usize) -> usize {
new_cursor_pos.clamp(0, self.input.chars().count())
new_cursor_pos.clamp(0, self.input.len())
}
fn reset_cursor(&mut self) {
self.character_index = 0;
self.cursor_position = 0;
}
fn submit_message(&mut self) {
@@ -242,7 +226,7 @@ fn ui(f: &mut Frame, app: &App) {
InputMode::Normal => Style::default(),
InputMode::Editing => Style::default().fg(Color::Yellow),
})
.block(Block::bordered().title("Input"));
.block(Block::default().borders(Borders::ALL).title("Input"));
f.render_widget(input, input_area);
match app.input_mode {
InputMode::Normal =>
@@ -256,7 +240,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.character_index as u16 + 1,
input_area.x + app.cursor_position as u16 + 1,
// Move one line down, from the border to the input line
input_area.y + 1,
);
@@ -272,6 +256,7 @@ fn ui(f: &mut Frame, app: &App) {
ListItem::new(content)
})
.collect();
let messages = List::new(messages).block(Block::bordered().title("Messages"));
let messages =
List::new(messages).block(Block::default().borders(Borders::ALL).title("Messages"));
f.render_widget(messages, messages_area);
}

View File

@@ -1,14 +0,0 @@
# 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

View File

@@ -1,9 +1,6 @@
# configuration for https://rust-lang.github.io/rustfmt/
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
comment_width = 100
format_code_in_doc_comments = true

View File

@@ -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,15 +45,11 @@ use crate::{
/// ```rust,no_run
/// use std::io::{stderr, stdout};
///
/// use ratatui::{
/// crossterm::{
/// terminal::{
/// disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
/// },
/// ExecutableCommand,
/// },
/// prelude::*,
/// use crossterm::{
/// terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
/// ExecutableCommand,
/// };
/// use ratatui::prelude::*;
///
/// let mut backend = CrosstermBackend::new(stdout());
/// // or
@@ -104,27 +100,6 @@ 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>
@@ -170,12 +145,14 @@ where
diff.queue(&mut self.writer)?;
modifier = cell.modifier;
}
if cell.fg != fg || cell.bg != bg {
queue!(
self.writer,
SetColors(Colors::new(cell.fg.into(), cell.bg.into()))
)?;
if cell.fg != fg {
let color = CColor::from(cell.fg);
queue!(self.writer, SetForegroundColor(color))?;
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")]
@@ -251,7 +228,7 @@ where
Ok(Rect::new(0, 0, width, height))
}
fn window_size(&mut self) -> io::Result<WindowSize> {
fn window_size(&mut self) -> Result<WindowSize, io::Error> {
let crossterm::terminal::WindowSize {
columns,
rows,

View File

@@ -9,12 +9,13 @@ use std::{
io::{self, Write},
};
use termion::{color as tcolor, style as tstyle};
use crate::{
backend::{Backend, ClearType, WindowSize},
buffer::Cell,
prelude::Rect,
style::{Color, Modifier, Style},
termion::{self, color as tcolor, color::Color as _, style as tstyle},
};
/// A [`Backend`] implementation that uses [Termion] to render to the terminal.
@@ -39,10 +40,8 @@ use crate::{
/// ```rust,no_run
/// use std::io::{stderr, stdout};
///
/// use ratatui::{
/// prelude::*,
/// termion::{raw::IntoRawMode, screen::IntoAlternateScreen},
/// };
/// use ratatui::prelude::*;
/// use termion::{raw::IntoRawMode, screen::IntoAlternateScreen};
///
/// let writer = stdout().into_raw_mode()?.into_alternate_screen()?;
/// let mut backend = TermionBackend::new(writer);
@@ -86,26 +85,6 @@ 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>
@@ -219,7 +198,7 @@ where
Ok(Rect::new(0, 0, terminal.0, terminal.1))
}
fn window_size(&mut self) -> io::Result<WindowSize> {
fn window_size(&mut self) -> Result<WindowSize, io::Error> {
Ok(WindowSize {
columns_rows: termion::terminal_size()?.into(),
pixels: termion::terminal_size_pixels()?.into(),
@@ -244,6 +223,7 @@ 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),
@@ -269,6 +249,7 @@ 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),
@@ -294,7 +275,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
@@ -435,7 +416,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

View File

@@ -7,19 +7,20 @@
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.
@@ -110,7 +111,7 @@ impl TermwizBackend {
}
impl Backend for TermwizBackend {
fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error>
where
I: Iterator<Item = (u16, u16, &'a Cell)>,
{
@@ -180,13 +181,13 @@ impl Backend for TermwizBackend {
Ok(())
}
fn hide_cursor(&mut self) -> io::Result<()> {
fn hide_cursor(&mut self) -> Result<(), io::Error> {
self.buffered_terminal
.add_change(Change::CursorVisibility(CursorVisibility::Hidden));
Ok(())
}
fn show_cursor(&mut self) -> io::Result<()> {
fn show_cursor(&mut self) -> Result<(), io::Error> {
self.buffered_terminal
.add_change(Change::CursorVisibility(CursorVisibility::Visible));
Ok(())
@@ -206,18 +207,18 @@ impl Backend for TermwizBackend {
Ok(())
}
fn clear(&mut self) -> io::Result<()> {
fn clear(&mut self) -> Result<(), io::Error> {
self.buffered_terminal
.add_change(Change::ClearScreen(termwiz::color::ColorAttribute::Default));
Ok(())
}
fn size(&self) -> io::Result<Rect> {
fn size(&self) -> Result<Rect, io::Error> {
let (cols, rows) = self.buffered_terminal.dimensions();
Ok(Rect::new(0, 0, u16_max(cols), u16_max(rows)))
}
fn window_size(&mut self) -> io::Result<WindowSize> {
fn window_size(&mut self) -> Result<WindowSize, io::Error> {
let ScreenSize {
cols,
rows,
@@ -240,7 +241,7 @@ impl Backend for TermwizBackend {
})
}
fn flush(&mut self) -> io::Result<()> {
fn flush(&mut self) -> Result<(), io::Error> {
self.buffered_terminal
.flush()
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
@@ -270,7 +271,7 @@ impl From<CellAttributes> for Style {
style.fg = Some(value.foreground().into());
style.bg = Some(value.background().into());
#[cfg(feature = "underline-color")]
#[cfg(feature = "underline_color")]
{
style.underline_color = Some(value.underline_color().into());
}
@@ -406,6 +407,7 @@ fn u16_max(i: usize) -> u16 {
#[cfg(test)]
mod tests {
use super::*;
use crate::style::Stylize;
mod into_color {
use Color as C;
@@ -574,19 +576,11 @@ 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);
assert_eq!(
Style::from(CellAttributes::default()),
Style::new().fg(Color::Reset).bg(Color::Reset)
);
// foreground color
assert_eq!(
Style::from(
@@ -594,7 +588,7 @@ mod tests {
.set_foreground(ColorAttribute::PaletteIndex(31))
.to_owned()
),
STYLE.fg(Color::Indexed(31))
Style::new().fg(Color::Indexed(31)).bg(Color::Reset)
);
// background color
assert_eq!(
@@ -603,7 +597,21 @@ mod tests {
.set_background(ColorAttribute::PaletteIndex(31))
.to_owned()
),
STYLE.bg(Color::Indexed(31))
Style::new().fg(Color::Reset).bg(Color::Indexed(31))
);
// underline color
#[cfg(feature = "underline_color")]
assert_eq!(
Style::from(
CellAttributes::default()
.set_underline_color(AnsiColor::Red)
.set
.to_owned()
),
Style::new()
.fg(Color::Reset)
.bg(Color::Reset)
.underline_color(Color::Red)
);
// underlined
assert_eq!(
@@ -612,12 +620,12 @@ mod tests {
.set_underline(Underline::Single)
.to_owned()
),
STYLE.underlined()
Style::new().fg(Color::Reset).bg(Color::Reset).underlined()
);
// blink
assert_eq!(
Style::from(CellAttributes::default().set_blink(Blink::Slow).to_owned()),
STYLE.slow_blink()
Style::new().fg(Color::Reset).bg(Color::Reset).slow_blink()
);
// intensity
assert_eq!(
@@ -626,38 +634,27 @@ mod tests {
.set_intensity(Intensity::Bold)
.to_owned()
),
STYLE.bold()
Style::new().fg(Color::Reset).bg(Color::Reset).bold()
);
// italic
assert_eq!(
Style::from(CellAttributes::default().set_italic(true).to_owned()),
STYLE.italic()
Style::new().fg(Color::Reset).bg(Color::Reset).italic()
);
// reversed
assert_eq!(
Style::from(CellAttributes::default().set_reverse(true).to_owned()),
STYLE.reversed()
Style::new().fg(Color::Reset).bg(Color::Reset).reversed()
);
// strikethrough
assert_eq!(
Style::from(CellAttributes::default().set_strikethrough(true).to_owned()),
STYLE.crossed_out()
Style::new().fg(Color::Reset).bg(Color::Reset).crossed_out()
);
// hidden
assert_eq!(
Style::from(CellAttributes::default().set_invisible(true).to_owned()),
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))
Style::new().fg(Color::Reset).bg(Color::Reset).hidden()
);
}
}

View File

@@ -2,19 +2,21 @@
//! It is used in the integration tests to verify the correctness of the library.
use std::{
fmt::{self, Write},
fmt::{Display, 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 renders to an memory buffer.
/// A [`Backend`] implementation used for integration testing that that renders to an in 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
@@ -28,7 +30,7 @@ use crate::{
///
/// let mut backend = TestBackend::new(10, 2);
/// backend.clear()?;
/// backend.assert_buffer_lines([" "; 2]);
/// backend.assert_buffer(&Buffer::with_lines(vec![" "; 2]));
/// # std::io::Result::Ok(())
/// ```
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
@@ -95,46 +97,24 @@ impl TestBackend {
}
/// Asserts that the `TestBackend`'s buffer is equal to the expected buffer.
///
/// This is a shortcut for `assert_eq!(self.buffer(), &expected)`.
///
/// # Panics
/// When they are not equal, a panic occurs with a detailed error message showing the
/// differences between the expected and actual buffers.
#[allow(deprecated)]
/// If the buffers 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(&self, expected: &Buffer) {
// 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));
assert_buffer_eq!(&self.buffer, expected);
}
}
impl fmt::Display for TestBackend {
impl Display for TestBackend {
/// Formats the `TestBackend` for display by calling the `buffer_view` function
/// on its internal buffer.
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", buffer_view(&self.buffer))
}
}
impl Backend for TestBackend {
fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error>
where
I: Iterator<Item = (u16, u16, &'a Cell)>,
{
@@ -145,54 +125,51 @@ impl Backend for TestBackend {
Ok(())
}
fn hide_cursor(&mut self) -> io::Result<()> {
fn hide_cursor(&mut self) -> Result<(), io::Error> {
self.cursor = false;
Ok(())
}
fn show_cursor(&mut self) -> io::Result<()> {
fn show_cursor(&mut self) -> Result<(), io::Error> {
self.cursor = true;
Ok(())
}
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
fn get_cursor(&mut self) -> Result<(u16, u16), io::Error> {
Ok(self.pos)
}
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
fn set_cursor(&mut self, x: u16, y: u16) -> Result<(), io::Error> {
self.pos = (x, y);
Ok(())
}
fn clear(&mut self) -> io::Result<()> {
fn clear(&mut self) -> Result<(), io::Error> {
self.buffer.reset();
Ok(())
}
fn clear_region(&mut self, clear_type: super::ClearType) -> io::Result<()> {
let region = match clear_type {
ClearType::All => return self.clear(),
match clear_type {
ClearType::All => self.clear()?,
ClearType::AfterCursor => {
let index = self.buffer.index_of(self.pos.0, self.pos.1) + 1;
&mut self.buffer.content[index..]
self.buffer.content[index..].fill(Cell::default());
}
ClearType::BeforeCursor => {
let index = self.buffer.index_of(self.pos.0, self.pos.1);
&mut self.buffer.content[..index]
self.buffer.content[..index].fill(Cell::default());
}
ClearType::CurrentLine => {
let line_start_index = self.buffer.index_of(0, self.pos.1);
let line_end_index = self.buffer.index_of(self.width - 1, self.pos.1);
&mut self.buffer.content[line_start_index..=line_end_index]
self.buffer.content[line_start_index..=line_end_index].fill(Cell::default());
}
ClearType::UntilNewLine => {
let index = self.buffer.index_of(self.pos.0, self.pos.1);
let line_end_index = self.buffer.index_of(self.width - 1, self.pos.1);
&mut self.buffer.content[index..=line_end_index]
self.buffer.content[index..=line_end_index].fill(Cell::default());
}
};
for cell in region {
cell.reset();
}
Ok(())
}
@@ -237,11 +214,11 @@ impl Backend for TestBackend {
Ok(())
}
fn size(&self) -> io::Result<Rect> {
fn size(&self) -> Result<Rect, io::Error> {
Ok(Rect::new(0, 0, self.width, self.height))
}
fn window_size(&mut self) -> io::Result<WindowSize> {
fn window_size(&mut self) -> Result<WindowSize, io::Error> {
// Some arbitrary window pixel size, probably doesn't need much testing.
static WINDOW_PIXEL_SIZE: Size = Size {
width: 640,
@@ -253,7 +230,7 @@ impl Backend for TestBackend {
})
}
fn flush(&mut self) -> io::Result<()> {
fn flush(&mut self) -> Result<(), io::Error> {
Ok(())
}
}
@@ -269,7 +246,7 @@ mod tests {
TestBackend {
width: 10,
height: 2,
buffer: Buffer::with_lines([" "; 2]),
buffer: Buffer::with_lines(vec![" "; 2]),
cursor: false,
pos: (0, 0),
}
@@ -277,14 +254,14 @@ mod tests {
}
#[test]
fn test_buffer_view() {
let buffer = Buffer::with_lines(["aaaa"; 2]);
let buffer = Buffer::with_lines(vec!["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([multi_byte_char]);
let buffer = Buffer::with_lines(vec![multi_byte_char]);
assert_eq!(
buffer_view(&buffer),
format!(
@@ -297,27 +274,29 @@ mod tests {
#[test]
fn buffer() {
let backend = TestBackend::new(10, 2);
backend.assert_buffer_lines([" "; 2]);
assert_eq!(backend.buffer(), &Buffer::with_lines(vec![" "; 2]));
}
#[test]
fn resize() {
let mut backend = TestBackend::new(10, 2);
backend.resize(5, 5);
backend.assert_buffer_lines([" "; 5]);
assert_eq!(backend.buffer(), &Buffer::with_lines(vec![" "; 5]));
}
#[test]
fn assert_buffer() {
let backend = TestBackend::new(10, 2);
backend.assert_buffer_lines([" "; 2]);
let buffer = Buffer::with_lines(vec![" "; 2]);
backend.assert_buffer(&buffer);
}
#[test]
#[should_panic = "buffer contents not equal"]
fn assert_buffer_panics() {
let backend = TestBackend::new(10, 2);
backend.assert_buffer_lines(["aaaaaaaaaa"; 2]);
let buffer = Buffer::with_lines(vec!["aaaaaaaaaa"; 2]);
backend.assert_buffer(&buffer);
}
#[test]
@@ -329,10 +308,11 @@ mod tests {
#[test]
fn draw() {
let mut backend = TestBackend::new(10, 2);
let cell = Cell::new("a");
let mut cell = Cell::default();
cell.set_symbol("a");
backend.draw([(0, 0, &cell)].into_iter()).unwrap();
backend.draw([(0, 1, &cell)].into_iter()).unwrap();
backend.assert_buffer_lines(["a "; 2]);
backend.assert_buffer(&Buffer::with_lines(vec!["a "; 2]));
}
#[test]
@@ -364,18 +344,24 @@ mod tests {
#[test]
fn clear() {
let mut backend = TestBackend::new(4, 2);
let cell = Cell::new("a");
let mut backend = TestBackend::new(10, 4);
let mut cell = Cell::default();
cell.set_symbol("a");
backend.draw([(0, 0, &cell)].into_iter()).unwrap();
backend.draw([(0, 1, &cell)].into_iter()).unwrap();
backend.clear().unwrap();
backend.assert_buffer_lines([" ", " "]);
backend.assert_buffer(&Buffer::with_lines(vec![
" ",
" ",
" ",
" ",
]));
}
#[test]
fn clear_region_all() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines([
backend.buffer = Buffer::with_lines(vec![
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
@@ -384,19 +370,19 @@ mod tests {
]);
backend.clear_region(ClearType::All).unwrap();
backend.assert_buffer_lines([
backend.assert_buffer(&Buffer::with_lines(vec![
" ",
" ",
" ",
" ",
" ",
]);
]));
}
#[test]
fn clear_region_after_cursor() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines([
backend.buffer = Buffer::with_lines(vec![
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
@@ -406,19 +392,19 @@ mod tests {
backend.set_cursor(3, 2).unwrap();
backend.clear_region(ClearType::AfterCursor).unwrap();
backend.assert_buffer_lines([
backend.assert_buffer(&Buffer::with_lines(vec![
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaa ",
" ",
" ",
]);
]));
}
#[test]
fn clear_region_before_cursor() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines([
backend.buffer = Buffer::with_lines(vec![
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
@@ -428,19 +414,19 @@ mod tests {
backend.set_cursor(5, 3).unwrap();
backend.clear_region(ClearType::BeforeCursor).unwrap();
backend.assert_buffer_lines([
backend.assert_buffer(&Buffer::with_lines(vec![
" ",
" ",
" ",
" aaaaa",
"aaaaaaaaaa",
]);
]));
}
#[test]
fn clear_region_current_line() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines([
backend.buffer = Buffer::with_lines(vec![
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
@@ -450,19 +436,19 @@ mod tests {
backend.set_cursor(3, 1).unwrap();
backend.clear_region(ClearType::CurrentLine).unwrap();
backend.assert_buffer_lines([
backend.assert_buffer(&Buffer::with_lines(vec![
"aaaaaaaaaa",
" ",
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
]);
]));
}
#[test]
fn clear_region_until_new_line() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines([
backend.buffer = Buffer::with_lines(vec![
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
@@ -472,19 +458,19 @@ mod tests {
backend.set_cursor(3, 0).unwrap();
backend.clear_region(ClearType::UntilNewLine).unwrap();
backend.assert_buffer_lines([
backend.assert_buffer(&Buffer::with_lines(vec![
"aaa ",
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
]);
]));
}
#[test]
fn append_lines_not_at_last_line() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines([
backend.buffer = Buffer::with_lines(vec![
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
@@ -510,19 +496,19 @@ mod tests {
assert_eq!(backend.get_cursor().unwrap(), (4, 4));
// As such the buffer should remain unchanged
backend.assert_buffer_lines([
backend.assert_buffer(&Buffer::with_lines(vec![
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
"dddddddddd",
"eeeeeeeeee",
]);
]));
}
#[test]
fn append_lines_at_last_line() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines([
backend.buffer = Buffer::with_lines(vec![
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
@@ -536,7 +522,7 @@ mod tests {
backend.append_lines(1).unwrap();
backend.buffer = Buffer::with_lines([
backend.buffer = Buffer::with_lines(vec![
"bbbbbbbbbb",
"cccccccccc",
"dddddddddd",
@@ -552,7 +538,7 @@ mod tests {
#[test]
fn append_multiple_lines_not_at_last_line() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines([
backend.buffer = Buffer::with_lines(vec![
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
@@ -569,19 +555,19 @@ mod tests {
assert_eq!(backend.get_cursor().unwrap(), (1, 4));
// As such the buffer should remain unchanged
backend.assert_buffer_lines([
backend.assert_buffer(&Buffer::with_lines(vec![
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
"dddddddddd",
"eeeeeeeeee",
]);
]));
}
#[test]
fn append_multiple_lines_past_last_line() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines([
backend.buffer = Buffer::with_lines(vec![
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
@@ -594,19 +580,19 @@ mod tests {
backend.append_lines(3).unwrap();
assert_eq!(backend.get_cursor().unwrap(), (1, 4));
backend.assert_buffer_lines([
backend.assert_buffer(&Buffer::with_lines(vec![
"cccccccccc",
"dddddddddd",
"eeeeeeeeee",
" ",
" ",
]);
]));
}
#[test]
fn append_multiple_lines_where_cursor_at_end_appends_height_lines() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines([
backend.buffer = Buffer::with_lines(vec![
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
@@ -619,19 +605,19 @@ mod tests {
backend.append_lines(5).unwrap();
assert_eq!(backend.get_cursor().unwrap(), (1, 4));
backend.assert_buffer_lines([
backend.assert_buffer(&Buffer::with_lines(vec![
" ",
" ",
" ",
" ",
" ",
]);
]));
}
#[test]
fn append_multiple_lines_where_cursor_appends_height_lines() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines([
backend.buffer = Buffer::with_lines(vec![
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
@@ -644,13 +630,13 @@ mod tests {
backend.append_lines(5).unwrap();
assert_eq!(backend.get_cursor().unwrap(), (1, 4));
backend.assert_buffer_lines([
backend.assert_buffer(&Buffer::with_lines(vec![
"bbbbbbbbbb",
"cccccccccc",
"dddddddddd",
"eeeeeeeeee",
" ",
]);
]));
}
#[test]

View File

@@ -1,44 +1,57 @@
/// Assert that two buffers are equal by comparing their areas and content.
///
/// # Panics
/// When the buffers differ this method panics and displays the differences similar to
/// `assert_eq!()`.
#[deprecated = "use assert_eq!(&actual, &expected)"]
/// On panic, displays the areas or the content and a diff of the contents.
#[macro_export]
macro_rules! assert_buffer_eq {
($actual_expr:expr, $expected_expr:expr) => {
match (&$actual_expr, &$expected_expr) {
(actual, expected) => {
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}",
);
if actual.area != expected.area {
panic!(
indoc::indoc!(
"
buffer areas not equal
expected: {:?}
actual: {:?}"
),
expected, actual
);
}
let diff = expected.diff(&actual);
if !diff.is_empty() {
let nice_diff = diff
.iter()
.enumerate()
.map(|(i, (x, y, cell))| {
let expected_cell = expected.get(*x, *y);
indoc::formatdoc! {"
{i}: at ({x}, {y})
expected: {expected_cell:?}
actual: {cell:?}
"}
})
.collect::<Vec<String>>()
.join("\n");
panic!(
indoc::indoc!(
"
buffer contents not equal
expected: {:?}
actual: {:?}
diff:
{}"
),
expected, actual, nice_diff
);
}
// shouldn't get here, but this guards against future behavior
// that changes equality but not area or content
assert_eq!(
actual, expected,
"buffers are not equal in an unexpected way. Please open an issue about this."
);
assert_eq!(actual, expected, "buffers not equal");
}
}
};
}
#[allow(deprecated)]
#[cfg(test)]
mod tests {
use crate::prelude::*;

View File

@@ -1,4 +1,7 @@
use std::fmt;
use std::{
cmp::min,
fmt::{Debug, Formatter, Result},
};
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
@@ -52,21 +55,22 @@ pub struct Buffer {
impl Buffer {
/// Returns a Buffer with all cells set to the default one
#[must_use]
pub fn empty(area: Rect) -> Self {
Self::filled(area, Cell::EMPTY)
let cell = Cell::default();
Self::filled(area, &cell)
}
/// Returns a Buffer with all cells initialized with the attributes of the given Cell
#[must_use]
pub fn filled(area: Rect, cell: Cell) -> Self {
pub fn filled(area: Rect, cell: &Cell) -> Self {
let size = area.area() as usize;
let content = vec![cell; size];
let mut content = Vec::with_capacity(size);
for _ in 0..size {
content.push(cell.clone());
}
Self { area, content }
}
/// Returns a Buffer containing the given lines
#[must_use]
pub fn with_lines<'a, Iter>(lines: Iter) -> Self
where
Iter: IntoIterator,
@@ -93,14 +97,12 @@ 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]
@@ -132,7 +134,6 @@ 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()
@@ -183,56 +184,65 @@ 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);
self.set_stringn(x, y, string, usize::MAX, style.into());
}
/// Print at most the first n characters of a string if enough space is available
/// until the end of the line.
/// until the end of the line
///
/// Use [`Buffer::set_string`] when the maximum amount of characters can be printed.
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
pub fn set_stringn<T, S>(
&mut self,
mut x: u16,
x: u16,
y: u16,
string: T,
max_width: usize,
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();
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),
while x < next_symbol {
self.get_mut(x, y).reset();
x += 1;
let mut index = self.index_of(x, y);
let mut x_offset = x as usize;
let graphemes = UnicodeSegmentation::graphemes(string.as_ref(), true);
let max_offset = min(self.area.right() as usize, width.saturating_add(x as usize));
for s in graphemes {
let width = s.width();
if width == 0 {
continue;
}
// `x_offset + width > max_offset` could be integer overflow on 32-bit machines if we
// change dimensions to usize or u32 and someone resizes the terminal to 1x2^32.
if width > max_offset.saturating_sub(x_offset) {
break;
}
self.content[index].set_symbol(s);
self.content[index].set_style(style);
// Reset following cells if multi-width (they would be hidden by the grapheme),
for i in index + 1..index + width {
self.content[i].reset();
}
index += width;
x_offset += width;
}
(x, y)
(x_offset as u16, y)
}
/// Print a line, starting at the position (x, y)
pub fn set_line(&mut self, x: u16, y: u16, line: &Line<'_>, max_width: u16) -> (u16, u16) {
let mut remaining_width = max_width;
pub fn set_line(&mut self, x: u16, y: u16, line: &Line<'_>, width: u16) -> (u16, u16) {
let mut remaining_width = width;
let mut x = x;
for span in line {
if remaining_width == 0 {
@@ -253,8 +263,8 @@ impl Buffer {
}
/// Print a span, starting at the position (x, y)
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)
pub fn set_span(&mut self, x: u16, y: u16, span: &Span<'_>, width: u16) -> (u16, u16) {
self.set_stringn(x, y, span.content.as_ref(), width as usize, span.style)
}
/// Set the style of all cells in the given area.
@@ -278,22 +288,23 @@ impl Buffer {
if self.content.len() > length {
self.content.truncate(length);
} else {
self.content.resize(length, Cell::EMPTY);
self.content.resize(length, Cell::default());
}
self.area = area;
}
/// Reset all cells in the buffer
pub fn reset(&mut self) {
for cell in &mut self.content {
cell.reset();
for c in &mut self.content {
c.reset();
}
}
/// Merge an other buffer into this one
pub fn merge(&mut self, other: &Self) {
let area = self.area.union(other.area);
self.content.resize(area.area() as usize, Cell::EMPTY);
let cell = Cell::default();
self.content.resize(area.area() as usize, cell.clone());
// Move original content to the appropriate space
let size = self.area.area() as usize;
@@ -303,7 +314,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].reset();
self.content[i] = cell.clone();
}
}
@@ -372,7 +383,7 @@ impl Buffer {
}
}
impl fmt::Debug for Buffer {
impl Debug for Buffer {
/// Writes a debug representation of the buffer to the given formatter.
///
/// The format is like a pretty printed struct, with the following fields:
@@ -380,14 +391,11 @@ impl fmt::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 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")?;
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
f.write_fmt(format_args!(
"Buffer {{\n area: {:?},\n content: [\n",
&self.area
))?;
let mut last_style = None;
let mut styles = vec![];
for (y, line) in self.content.chunks(self.area.width as usize).enumerate() {
@@ -418,13 +426,12 @@ impl fmt::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 {
@@ -452,42 +459,19 @@ mod tests {
use rstest::{fixture, rstest};
use super::*;
use crate::assert_buffer_eq;
#[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);
fn cell(s: &str) -> Cell {
let mut cell = Cell::default();
cell.set_symbol(s);
cell
}
#[test]
fn debug_some_example() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 12, 2));
buffer.set_string(0, 0, "Hello World!", Style::default());
buffer.set_string(
fn debug() {
let mut buf = Buffer::empty(Rect::new(0, 0, 12, 2));
buf.set_string(0, 0, "Hello World!", Style::default());
buf.set_string(
0,
1,
"G'day World!",
@@ -496,40 +480,42 @@ mod tests {
.bg(Color::Yellow)
.add_modifier(Modifier::BOLD),
);
let result = format!("{buffer:?}");
println!("{result}");
#[cfg(feature = "underline-color")]
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,
]
}"#
assert_eq!(
format!("{buf:?}"),
indoc::indoc!(
"
Buffer {
area: Rect { x: 0, y: 0, width: 12, height: 2 },
content: [
\"Hello World!\",
\"G'day World!\",
],
styles: [
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
x: 0, y: 1, fg: Green, bg: Yellow, underline: Reset, modifier: BOLD,
]
}"
)
);
#[cfg(not(feature = "underline-color"))]
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!(
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,
]
}"
)
);
assert_eq!(result, expected);
}
#[test]
@@ -573,27 +559,27 @@ mod tests {
// Zero-width
buffer.set_stringn(0, 0, "aaa", 0, Style::default());
assert_eq!(buffer, Buffer::with_lines([" "]));
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" "]));
buffer.set_string(0, 0, "aaa", Style::default());
assert_eq!(buffer, Buffer::with_lines(["aaa "]));
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["aaa "]));
// Width limit:
buffer.set_stringn(0, 0, "bbbbbbbbbbbbbb", 4, Style::default());
assert_eq!(buffer, Buffer::with_lines(["bbbb "]));
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["bbbb "]));
buffer.set_string(0, 0, "12345", Style::default());
assert_eq!(buffer, Buffer::with_lines(["12345"]));
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["12345"]));
// Width truncation:
buffer.set_string(0, 0, "123456", Style::default());
assert_eq!(buffer, Buffer::with_lines(["12345"]));
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["12345"]));
// multi-line
buffer = Buffer::empty(Rect::new(0, 0, 5, 2));
buffer.set_string(0, 0, "12345", Style::default());
buffer.set_string(0, 1, "67890", Style::default());
assert_eq!(buffer, Buffer::with_lines(["12345", "67890"]));
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["12345", "67890"]));
}
#[test]
@@ -604,7 +590,7 @@ mod tests {
// multi-width overwrite
buffer.set_string(0, 0, "aaaaa", Style::default());
buffer.set_string(0, 0, "称号", Style::default());
assert_eq!(buffer, Buffer::with_lines(["称号a"]));
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["称号a"]));
}
#[test]
@@ -615,12 +601,12 @@ mod tests {
// Leading grapheme with zero width
let s = "\u{1}a";
buffer.set_stringn(0, 0, s, 1, Style::default());
assert_eq!(buffer, Buffer::with_lines(["a"]));
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["a"]));
// Trailing grapheme with zero with
let s = "a\u{1}";
buffer.set_stringn(0, 0, s, 1, Style::default());
assert_eq!(buffer, Buffer::with_lines(["a"]));
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["a"]));
}
#[test]
@@ -628,11 +614,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_eq!(buffer, Buffer::with_lines(["コン "]));
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["コン "]));
// Only 1 space left.
buffer.set_string(0, 0, "コンピ", Style::default());
assert_eq!(buffer, Buffer::with_lines(["コン "]));
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["コン "]));
}
#[fixture]
@@ -657,7 +643,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_eq!(small_one_line_buffer, expected_buffer);
assert_buffer_eq!(small_one_line_buffer, expected_buffer);
}
#[rstest]
@@ -698,39 +684,28 @@ mod tests {
#[test]
fn set_style() {
let mut buffer = Buffer::with_lines(["aaaaa", "bbbbb", "ccccc"]);
let mut buffer = Buffer::with_lines(vec!["aaaaa", "bbbbb", "ccccc"]);
buffer.set_style(Rect::new(0, 1, 5, 1), Style::new().red());
#[rustfmt::skip]
let expected = Buffer::with_lines([
"aaaaa".into(),
"bbbbb".red(),
"ccccc".into(),
]);
assert_eq!(buffer, expected);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec!["aaaaa".into(), "bbbbb".red(), "ccccc".into(),])
);
}
#[test]
fn set_style_does_not_panic_when_out_of_area() {
let mut buffer = Buffer::with_lines(["aaaaa", "bbbbb", "ccccc"]);
let mut buffer = Buffer::with_lines(vec!["aaaaa", "bbbbb", "ccccc"]);
buffer.set_style(Rect::new(0, 1, 10, 3), Style::new().red());
#[rustfmt::skip]
let expected = Buffer::with_lines([
"aaaaa".into(),
"bbbbb".red(),
"ccccc".red(),
]);
assert_eq!(buffer, expected);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec!["aaaaa".into(), "bbbbb".red(), "ccccc".red(),])
);
}
#[test]
fn with_lines() {
#[rustfmt::skip]
let buffer = Buffer::with_lines([
"┌────────┐",
"│コンピュ│",
"│ーa 上で│",
"└────────┘",
]);
let buffer =
Buffer::with_lines(vec!["┌────────┐", "│コンピュ│", "│ーa 上で│", "└────────┘"]);
assert_eq!(buffer.area.x, 0);
assert_eq!(buffer.area.y, 0);
assert_eq!(buffer.area.width, 10);
@@ -743,14 +718,14 @@ mod tests {
let prev = Buffer::empty(area);
let next = Buffer::empty(area);
let diff = prev.diff(&next);
assert_eq!(diff, []);
assert_eq!(diff, vec![]);
}
#[test]
fn diff_empty_filled() {
let area = Rect::new(0, 0, 40, 40);
let prev = Buffer::empty(area);
let next = Buffer::filled(area, Cell::new("a"));
let next = Buffer::filled(area, Cell::default().set_symbol("a"));
let diff = prev.diff(&next);
assert_eq!(diff.len(), 40 * 40);
}
@@ -758,22 +733,22 @@ mod tests {
#[test]
fn diff_filled_filled() {
let area = Rect::new(0, 0, 40, 40);
let prev = Buffer::filled(area, Cell::new("a"));
let next = Buffer::filled(area, Cell::new("a"));
let prev = Buffer::filled(area, Cell::default().set_symbol("a"));
let next = Buffer::filled(area, Cell::default().set_symbol("a"));
let diff = prev.diff(&next);
assert_eq!(diff, []);
assert_eq!(diff, vec![]);
}
#[test]
fn diff_single_width() {
let prev = Buffer::with_lines([
let prev = Buffer::with_lines(vec![
" ",
"┌Title─┐ ",
"│ │ ",
"│ │ ",
"└──────┘ ",
]);
let next = Buffer::with_lines([
let next = Buffer::with_lines(vec![
" ",
"┌TITLE─┐ ",
"│ │ ",
@@ -783,84 +758,116 @@ mod tests {
let diff = prev.diff(&next);
assert_eq!(
diff,
[
(2, 1, &Cell::new("I")),
(3, 1, &Cell::new("T")),
(4, 1, &Cell::new("L")),
(5, 1, &Cell::new("E")),
vec![
(2, 1, &cell("I")),
(3, 1, &cell("T")),
(4, 1, &cell("L")),
(5, 1, &cell("E")),
]
);
}
#[test]
#[rustfmt::skip]
fn diff_multi_width() {
#[rustfmt::skip]
let prev = Buffer::with_lines([
let prev = Buffer::with_lines(vec![
"┌Title─┐ ",
"└──────┘ ",
]);
#[rustfmt::skip]
let next = Buffer::with_lines([
let next = Buffer::with_lines(vec![
"┌称号──┐ ",
"└──────┘ ",
]);
let diff = prev.diff(&next);
assert_eq!(
diff,
[
(1, 0, &Cell::new("")),
vec![
(1, 0, &cell("")),
// Skipped "i"
(3, 0, &Cell::new("")),
(3, 0, &cell("")),
// Skipped "l"
(5, 0, &Cell::new("")),
(5, 0, &cell("")),
]
);
}
#[test]
fn diff_multi_width_offset() {
let prev = Buffer::with_lines(["┌称号──┐"]);
let next = Buffer::with_lines(["┌─称号─┐"]);
let prev = Buffer::with_lines(vec!["┌称号──┐"]);
let next = Buffer::with_lines(vec!["┌─称号─┐"]);
let diff = prev.diff(&next);
assert_eq!(
diff,
[
(1, 0, &Cell::new("")),
(2, 0, &Cell::new("")),
(4, 0, &Cell::new("")),
]
vec![(1, 0, &cell("")), (2, 0, &cell("")), (4, 0, &cell("")),]
);
}
#[test]
fn diff_skip() {
let prev = Buffer::with_lines(["123"]);
let mut next = Buffer::with_lines(["456"]);
let prev = Buffer::with_lines(vec!["123"]);
let mut next = Buffer::with_lines(vec!["456"]);
for i in 1..3 {
next.content[i].set_skip(true);
}
let diff = prev.diff(&next);
assert_eq!(diff, [(0, 0, &Cell::new("4"))],);
}
#[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_eq!(one, Buffer::with_lines(expected));
assert_eq!(diff, vec![(0, 0, &cell("4"))],);
}
#[test]
fn merge_with_offset() {
fn merge() {
let mut one = Buffer::filled(
Rect {
x: 0,
y: 0,
width: 2,
height: 2,
},
Cell::default().set_symbol("1"),
);
let two = Buffer::filled(
Rect {
x: 0,
y: 2,
width: 2,
height: 2,
},
Cell::default().set_symbol("2"),
);
one.merge(&two);
assert_buffer_eq!(one, Buffer::with_lines(vec!["11", "11", "22", "22"]));
}
#[test]
fn merge2() {
let mut one = Buffer::filled(
Rect {
x: 2,
y: 2,
width: 2,
height: 2,
},
Cell::default().set_symbol("1"),
);
let two = Buffer::filled(
Rect {
x: 0,
y: 0,
width: 2,
height: 2,
},
Cell::default().set_symbol("2"),
);
one.merge(&two);
assert_buffer_eq!(
one,
Buffer::with_lines(vec!["22 ", "22 ", " 11", " 11"])
);
}
#[test]
fn merge3() {
let mut one = Buffer::filled(
Rect {
x: 3,
@@ -868,7 +875,7 @@ mod tests {
width: 2,
height: 2,
},
Cell::new("1"),
Cell::default().set_symbol("1"),
);
let two = Buffer::filled(
Rect {
@@ -877,48 +884,67 @@ mod tests {
width: 3,
height: 4,
},
Cell::new("2"),
Cell::default().set_symbol("2"),
);
one.merge(&two);
let mut expected = Buffer::with_lines(["222 ", "222 ", "2221", "2221"]);
expected.area = Rect {
let mut merged = Buffer::with_lines(vec!["222 ", "222 ", "2221", "2221"]);
merged.area = Rect {
x: 1,
y: 1,
width: 4,
height: 4,
};
assert_eq!(one, expected);
assert_buffer_eq!(one, merged);
}
#[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 {
#[test]
fn merge_skip() {
let mut one = Buffer::filled(
Rect {
x: 0,
y: 0,
width: 2,
height: 2,
};
let mut cell = Cell::new("1");
cell.skip = skip_one;
Buffer::filled(area, cell)
};
let two = {
let area = Rect {
},
Cell::default().set_symbol("1"),
);
let two = Buffer::filled(
Rect {
x: 0,
y: 1,
width: 2,
height: 2,
};
let mut cell = Cell::new("2");
cell.skip = skip_two;
Buffer::filled(area, cell)
};
},
Cell::default().set_symbol("2").set_skip(true),
);
one.merge(&two);
let skipped = one.content().iter().map(|c| c.skip).collect::<Vec<_>>();
assert_eq!(skipped, expected);
let skipped: Vec<bool> = one.content().iter().map(|c| c.skip).collect();
assert_eq!(skipped, vec![false, false, true, true, true, true]);
}
#[test]
fn merge_skip2() {
let mut one = Buffer::filled(
Rect {
x: 0,
y: 0,
width: 2,
height: 2,
},
Cell::default().set_symbol("1").set_skip(true),
);
let two = Buffer::filled(
Rect {
x: 0,
y: 1,
width: 2,
height: 2,
},
Cell::default().set_symbol("2"),
);
one.merge(&two);
let skipped: Vec<bool> = one.content().iter().map(|c| c.skip).collect();
assert_eq!(skipped, vec![true, true, false, false, false, false]);
}
#[test]
@@ -927,6 +953,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(["foo".red(), "bar".blue()]));
assert_eq!(buf, Buffer::with_lines(vec!["foo".red(), "bar".blue()]));
}
}

View File

@@ -1,3 +1,5 @@
use std::fmt::Debug;
use compact_str::CompactString;
use crate::prelude::*;
@@ -34,29 +36,7 @@ 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()
}
@@ -108,16 +88,19 @@ impl Cell {
}
/// Returns the style of the cell.
#[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(),
}
pub fn style(&self) -> Style {
#[cfg(feature = "underline-color")]
return Style::default()
.fg(self.fg)
.bg(self.bg)
.underline_color(self.underline_color)
.add_modifier(self.modifier);
#[cfg(not(feature = "underline-color"))]
return Style::default()
.fg(self.fg)
.bg(self.bg)
.add_modifier(self.modifier);
}
/// Sets the cell to be skipped when copying (diffing) the buffer to the screen.
@@ -129,7 +112,7 @@ impl Cell {
self
}
/// Resets the cell to the empty state.
/// Resets the cell to the default state.
pub fn reset(&mut self) {
self.symbol = CompactString::new_inline(" ");
self.fg = Color::Reset;
@@ -145,7 +128,15 @@ impl Cell {
impl Default for Cell {
fn default() -> Self {
Self::EMPTY
Self {
symbol: CompactString::new_inline(" "),
fg: Color::Reset,
bg: Color::Reset,
#[cfg(feature = "underline-color")]
underline_color: Color::Reset,
modifier: Modifier::empty(),
skip: false,
}
}
}
@@ -155,15 +146,11 @@ mod tests {
#[test]
fn symbol_field() {
let mut cell = Cell::EMPTY;
let mut cell = Cell::default();
assert_eq!(cell.symbol(), " ");
cell.set_symbol(""); // Multi-byte character
assert_eq!(cell.symbol(), "");
cell.set_symbol("👨‍👩‍👧‍👦"); // Multiple code units combined with ZWJ
assert_eq!(cell.symbol(), "👨‍👩‍👧‍👦");
// 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(), " ");
}
}

View File

@@ -2,6 +2,7 @@
mod alignment;
mod constraint;
mod corner;
mod direction;
mod flex;
#[allow(clippy::module_inception)]
@@ -13,6 +14,7 @@ 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;

View File

@@ -1,4 +1,4 @@
use std::fmt;
use std::fmt::{self, Display};
use itertools::Itertools;
use strum::EnumIs;
@@ -362,7 +362,7 @@ impl Default for Constraint {
}
}
impl fmt::Display for Constraint {
impl Display for Constraint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Percentage(p) => write!(f, "Percentage({p})"),

33
src/layout/corner.rs Normal file
View File

@@ -0,0 +1,33 @@
use strum::{Display, EnumString};
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
pub enum Corner {
#[default]
TopLeft,
TopRight,
BottomRight,
BottomLeft,
}
#[cfg(test)]
mod tests {
use strum::ParseError;
use super::*;
#[test]
fn corner_to_string() {
assert_eq!(Corner::BottomLeft.to_string(), "BottomLeft");
assert_eq!(Corner::BottomRight.to_string(), "BottomRight");
assert_eq!(Corner::TopLeft.to_string(), "TopLeft");
assert_eq!(Corner::TopRight.to_string(), "TopRight");
}
#[test]
fn corner_from_str() {
assert_eq!("BottomLeft".parse::<Corner>(), Ok(Corner::BottomLeft));
assert_eq!("BottomRight".parse::<Corner>(), Ok(Corner::BottomRight));
assert_eq!("TopLeft".parse::<Corner>(), Ok(Corner::TopLeft));
assert_eq!("TopRight".parse::<Corner>(), Ok(Corner::TopRight));
assert_eq!("".parse::<Corner>(), Err(ParseError::VariantNotFound));
}
}

View File

@@ -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.as_ref().try_into().expect("invalid number of rects")
areas.to_vec().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
.as_ref()
.to_vec()
.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,6 +1334,7 @@ mod tests {
use rstest::rstest;
use crate::{
assert_buffer_eq,
layout::flex::Flex,
prelude::{Constraint::*, *},
widgets::Paragraph,
@@ -1360,7 +1361,8 @@ mod tests {
let s = c.to_string().repeat(area.width as usize);
Paragraph::new(s).render(layout[i], &mut buffer);
}
assert_eq!(buffer, Buffer::with_lines([expected]));
let expected = Buffer::with_lines(vec![expected]);
assert_buffer_eq!(buffer, expected);
}
#[rstest]

View File

@@ -1,4 +1,4 @@
use std::fmt;
use std::fmt::{self, Display};
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
pub struct Margin {
@@ -15,7 +15,7 @@ impl Margin {
}
}
impl fmt::Display for Margin {
impl Display for Margin {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}x{}", self.horizontal, self.vertical)
}

View File

@@ -40,6 +40,16 @@ pub struct Offset {
pub y: i32,
}
impl<X: Into<i32>, Y: Into<i32>> From<(X, Y)> for Offset {
/// Creates a new `Offset` from a tuple of (x, y).
fn from((x, y): (X, Y)) -> Self {
Self {
x: x.into(),
y: y.into(),
}
}
}
impl fmt::Display for Rect {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}x{}+{}+{}", self.width, self.height, self.x, self.y)
@@ -47,14 +57,6 @@ 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 {
@@ -120,12 +122,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 const fn inner(self, margin: Margin) -> Self {
pub 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::ZERO
Self::default()
} else {
Self {
x: self.x.saturating_add(margin.horizontal),
@@ -143,9 +145,20 @@ impl Rect {
/// - Positive `x` moves the whole `Rect` to the right, negative to the left.
/// - Positive `y` moves the whole `Rect` to the bottom, negative to the top.
///
/// See [`Offset`] for details.
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, layout::Offset};
/// let rect = Rect::new(1, 2, 3, 4);
/// let rect = rect.offset(Offset { x: 10, y: 20 });
/// assert_eq!(rect, Rect::new(11, 22, 3, 4));
///
/// // offset can also be called with a tuple of (x, y)
/// let rect = rect.offset((10, 20));
/// ```
#[must_use = "method returns the modified value"]
pub fn offset(self, offset: Offset) -> Self {
pub fn offset<T: Into<Offset>>(self, offset: T) -> Self {
let offset = offset.into();
Self {
x: i32::from(self.x)
.saturating_add(offset.x)
@@ -321,18 +334,6 @@ 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 {
@@ -405,7 +406,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)
);
}
@@ -443,6 +444,11 @@ mod tests {
);
}
#[test]
fn offset_from_tuple() {
assert_eq!(Rect::new(1, 2, 3, 4).offset((5, 6)), Rect::new(6, 8, 3, 4));
}
#[test]
fn union() {
assert_eq!(

View File

@@ -20,10 +20,10 @@
//!
//! ## Installation
//!
//! Add `ratatui` as a dependency to your cargo.toml:
//! Add `ratatui` and `crossterm` as dependencies to your cargo.toml:
//!
//! ```shell
//! cargo add ratatui
//! cargo add ratatui crossterm
//! ```
//!
//! Ratatui uses [Crossterm] by default as it works on most platforms. See the [Installation]
@@ -103,17 +103,12 @@
//! ```rust,no_run
//! use std::io::{self, stdout};
//!
//! use ratatui::{
//! crossterm::{
//! event::{self, Event, KeyCode},
//! terminal::{
//! disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
//! },
//! ExecutableCommand,
//! },
//! prelude::*,
//! widgets::*,
//! use crossterm::{
//! event::{self, Event, KeyCode},
//! terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
//! ExecutableCommand,
//! };
//! use ratatui::{prelude::*, widgets::*};
//!
//! fn main() -> io::Result<()> {
//! enable_raw_mode()?;
@@ -144,7 +139,8 @@
//!
//! fn ui(frame: &mut Frame) {
//! frame.render_widget(
//! Paragraph::new("Hello World!").block(Block::bordered().title("Greeting")),
//! Paragraph::new("Hello World!")
//! .block(Block::default().title("Greeting").borders(Borders::ALL)),
//! frame.size(),
//! );
//! }
@@ -188,8 +184,14 @@
//! [Constraint::Percentage(50), Constraint::Percentage(50)],
//! )
//! .split(main_layout[1]);
//! frame.render_widget(Block::bordered().title("Left"), inner_layout[0]);
//! frame.render_widget(Block::bordered().title("Right"), inner_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],
//! );
//! }
//! ```
//!
@@ -260,6 +262,22 @@
//! ![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/
@@ -305,20 +323,24 @@
//! [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&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
//! [Crate Badge]: https://img.shields.io/crates/v/ratatui?logo=rust&style=flat-square
//! [License Badge]: https://img.shields.io/crates/l/ratatui?style=flat-square
//! [CI Badge]:
//! https://img.shields.io/github/actions/workflow/status/ratatui-org/ratatui/ci.yml?style=flat-square&logo=github
//! [CI Workflow]: https://github.com/ratatui-org/ratatui/actions/workflows/ci.yml
//! [Codecov Badge]: https://img.shields.io/codecov/c/github/ratatui-org/ratatui?logo=codecov&style=flat-square&token=BAQ8SOKEST&color=C43AC3&logoColor=C43AC3
//! [Codecov Badge]:
//! https://img.shields.io/codecov/c/github/ratatui-org/ratatui?logo=codecov&style=flat-square&token=BAQ8SOKEST
//! [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&color=1370D3&logoColor=1370D3
//! [Discord Badge]:
//! https://img.shields.io/discord/1070692720437383208?label=discord&logo=discord&style=flat-square
//! [Discord Server]: https://discord.gg/pMCEU9hNEj
//! [Docs Badge]: https://img.shields.io/docsrs/ratatui?logo=rust&style=flat-square&logoColor=E05D44
//! [Matrix Badge]: https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix&color=C43AC3
//! [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
//! [Matrix]: https://matrix.to/#/#ratatui:matrix.org
//! [Sponsors Badge]: https://img.shields.io/github/sponsors/ratatui-org?logo=github&style=flat-square&color=1370D3
//! [Sponsors Badge]: https://img.shields.io/github/sponsors/ratatui-org?logo=github&style=flat-square
// show the feature flags in the generated documentation
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
@@ -340,13 +362,3 @@ 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;

View File

@@ -27,7 +27,7 @@ pub(crate) use crate::widgets::{StatefulWidgetRef, WidgetRef};
pub use crate::{
backend::{self, Backend},
buffer::{self, Buffer},
layout::{self, Alignment, Constraint, Direction, Layout, Margin, Rect},
layout::{self, Alignment, Constraint, Corner, Direction, Layout, Margin, Rect},
style::{self, Color, Modifier, Style, Styled, Stylize},
symbols::{self, Marker},
terminal::{CompletedFrame, Frame, Terminal, TerminalOptions, Viewport},

View File

@@ -68,14 +68,14 @@
//! [`prelude`]: crate::prelude
//! [`Span`]: crate::text::Span
use std::fmt;
use std::fmt::{self, Debug};
use bitflags::bitflags;
mod color;
mod stylize;
pub use color::{Color, ParseColorError};
pub use color::Color;
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");
}
write!(f, "{}", self.0)
fmt::Debug::fmt(&self.0, f)
}
}
@@ -222,7 +222,7 @@ impl fmt::Debug for Modifier {
/// buffer.get(0, 0).style(),
/// );
/// ```
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Style {
pub fg: Option<Color>,
@@ -233,6 +233,12 @@ pub struct Style {
pub sub_modifier: Modifier,
}
impl Default for Style {
fn default() -> Self {
Self::new()
}
}
impl Styled for Style {
type Item = Self;
@@ -544,30 +550,34 @@ impl From<(Color, Color, Modifier, Modifier)> for Style {
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
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),
]
}
#[test]
fn combined_patch_gives_same_result_as_individual_patch() {
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),
];
let styles = styles();
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::new().patch(a).patch(b).patch(c).patch(d),
Style::new().patch(a.patch(b.patch(c.patch(d))))
Style::default().patch(a).patch(b).patch(c).patch(d),
Style::default().patch(combined)
);
}
}
@@ -579,7 +589,7 @@ mod tests {
fn combine_individual_modifiers() {
use crate::{buffer::Buffer, layout::Rect};
let mods = [
let mods = vec![
Modifier::BOLD,
Modifier::DIM,
Modifier::ITALIC,
@@ -593,30 +603,37 @@ 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::new().add_modifier(m));
buffer
.get_mut(0, 0)
.set_style(Style::default().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));
}
}
#[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]
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"
);
}
#[test]
@@ -646,80 +663,151 @@ mod tests {
);
}
#[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));
}
#[allow(clippy::too_many_lines)]
#[test]
fn reset_can_be_stylized() {
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
assert_eq!(Style::new().reset(), Style::reset());
}

View File

@@ -1,6 +1,9 @@
#![allow(clippy::unreadable_literal)]
use std::{fmt, str::FromStr};
use std::{
fmt::{self, Debug, Display},
str::FromStr,
};
/// ANSI Color
///
@@ -63,6 +66,7 @@ use std::{fmt, str::FromStr};
///
/// [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]
@@ -137,102 +141,14 @@ 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>,
{
/// 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)),
},
}
let s = String::deserialize(deserializer)?;
FromStr::from_str(&s).map_err(serde::de::Error::custom)
}
}
@@ -240,8 +156,8 @@ impl<'de> serde::Deserialize<'de> for Color {
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub struct ParseColorError;
impl fmt::Display for ParseColorError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
impl std::fmt::Display for ParseColorError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Failed to parse Colors")
}
}
@@ -313,7 +229,16 @@ impl FromStr for Color {
_ => {
if let Ok(index) = s.parse::<u8>() {
Self::Indexed(index)
} else if let Some((r, g, b)) = parse_hex_color(s) {
} 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),
)
} {
Self::Rgb(r, g, b)
} else {
return Err(ParseColorError);
@@ -324,17 +249,7 @@ impl FromStr 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 {
impl Display for Color {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Reset => write!(f, "Reset"),
@@ -588,7 +503,6 @@ 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
];
@@ -665,42 +579,4 @@ 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(())
}
}

View File

@@ -133,13 +133,17 @@ macro_rules! modifier {
/// "world".green().on_yellow().not_bold(),
/// ]);
/// let paragraph = Paragraph::new(line).italic().underlined();
/// let block = Block::bordered().title("Title").on_white().bold();
/// let block = Block::default()
/// .title("Title")
/// .borders(Borders::ALL)
/// .on_white()
/// .bold();
/// ```
pub trait Stylize<'a, T>: Sized {
#[must_use = "`bg` returns the modified style without modifying the original"]
fn bg<C: Into<Color>>(self, color: C) -> T;
fn bg(self, color: Color) -> T;
#[must_use = "`fg` returns the modified style without modifying the original"]
fn fg<C: Into<Color>>(self, color: C) -> T;
fn fg<S: Into<Color>>(self, color: S) -> T;
#[must_use = "`reset` returns the modified style without modifying the original"]
fn reset(self) -> T;
#[must_use = "`add_modifier` returns the modified style without modifying the original"]
@@ -179,12 +183,12 @@ impl<'a, T, U> Stylize<'a, T> for U
where
U: Styled<Item = T>,
{
fn bg<C: Into<Color>>(self, color: C) -> T {
let style = self.style().bg(color.into());
fn bg(self, color: Color) -> T {
let style = self.style().bg(color);
self.set_style(style)
}
fn fg<C: Into<Color>>(self, color: C) -> T {
fn fg<S: Into<Color>>(self, color: S) -> T {
let style = self.style().fg(color.into());
self.set_style(style)
}

View File

@@ -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::new();
/// let block = Block::default();
/// let area = Rect::new(0, 0, 5, 5);
/// frame.render_widget(block, area);
/// ```
@@ -83,15 +83,13 @@ 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::new();
/// let block = Block::default();
/// 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")]
@@ -140,7 +138,6 @@ 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();
@@ -149,7 +146,6 @@ 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")]

View File

@@ -25,20 +25,21 @@
//! // Converted to Line(vec![
//! // Span { content: Cow::Borrowed("My title"), style: Style { .. } }
//! // ])
//! let block = Block::new().title("My title");
//! let block = Block::default().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::new().title(Span::styled("My title", Style::default().fg(Color::Yellow)));
//! let block =
//! Block::default().title(Span::styled("My title", Style::default().fg(Color::Yellow)));
//!
//! // A string with multiple styles.
//! // Converted to Line(vec![
//! // Span { content: Cow::Borrowed("My"), style: Style { fg: Some(Color::Yellow), .. } },
//! // Span { content: Cow::Borrowed(" title"), .. }
//! // ])
//! let block = Block::new().title(vec![
//! let block = Block::default().title(vec![
//! Span::styled("My", Style::default().fg(Color::Yellow)),
//! Span::raw(" title"),
//! ]);

View File

@@ -1,8 +1,5 @@
#![deny(missing_docs)]
#![warn(clippy::pedantic, clippy::nursery, clippy::arithmetic_side_effects)]
use std::{borrow::Cow, fmt};
use unicode_truncate::UnicodeTruncateStr;
use std::borrow::Cow;
use super::StyledGrapheme;
use crate::prelude::*;
@@ -555,100 +552,32 @@ impl Widget for Line<'_> {
impl WidgetRef for Line<'_> {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
let area = area.intersection(buf.area);
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);
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,
};
}
}
/// 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;
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;
}
}
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 {
impl std::fmt::Display for Line<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for span in &self.spans {
write!(f, "{span}")?;
}
@@ -949,12 +878,8 @@ mod tests {
}
mod widget {
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use super::*;
use crate::buffer::Cell;
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);
@@ -972,36 +897,37 @@ mod tests {
fn render() {
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
hello_world().render(Rect::new(0, 0, 15, 1), &mut buf);
let mut expected = Buffer::with_lines(["Hello world! "]);
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_eq!(buf, expected);
assert_buffer_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));
assert_buffer_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! "]);
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_eq!(buf, expected);
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);
assert_eq!(buf, Buffer::with_lines(["Hello "]));
let expected = Buffer::with_lines(vec!["Hello "]);
assert_buffer_eq!(buf, expected);
}
#[test]
@@ -1009,11 +935,11 @@ mod tests {
let line = hello_world().alignment(Alignment::Center);
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
line.render(Rect::new(0, 0, 15, 1), &mut buf);
let mut expected = Buffer::with_lines([" Hello world! "]);
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_eq!(buf, expected);
assert_buffer_eq!(buf, expected);
}
#[test]
@@ -1021,255 +947,11 @@ mod tests {
let line = hello_world().alignment(Alignment::Right);
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
line.render(Rect::new(0, 0, 15, 1), &mut buf);
let mut expected = Buffer::with_lines([" Hello world!"]);
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_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]));
assert_buffer_eq!(buf, expected);
}
}

View File

@@ -1,4 +1,7 @@
use std::{borrow::Cow, fmt};
use std::{
borrow::Cow,
fmt::{self, Debug, Display},
};
use super::Text;
@@ -16,7 +19,7 @@ use super::Text;
/// let password = Masked::new("12345", 'x');
///
/// Paragraph::new(password).render(buffer.area, &mut buffer);
/// assert_eq!(buffer, Buffer::with_lines(["xxxxx"]));
/// assert_eq!(buffer, Buffer::with_lines(vec!["xxxxx"]));
/// ```
#[derive(Default, Clone, Eq, PartialEq, Hash)]
pub struct Masked<'a> {
@@ -43,18 +46,17 @@ impl<'a> Masked<'a> {
}
}
impl fmt::Debug for Masked<'_> {
impl Debug for Masked<'_> {
/// Debug representation of a masked string is the underlying string
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// note that calling display instead of Debug here is intentional
fmt::Display::fmt(&self.inner, f)
Display::fmt(&self.inner, f)
}
}
impl fmt::Display for Masked<'_> {
impl Display for Masked<'_> {
/// Display representation of a masked string is the masked string
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.value(), f)
Display::fmt(&self.value(), f)
}
}
@@ -110,14 +112,12 @@ 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]

View File

@@ -1,4 +1,4 @@
use std::{borrow::Cow, fmt};
use std::{borrow::Cow, fmt::Debug};
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
@@ -395,9 +395,9 @@ impl WidgetRef for Span<'_> {
}
}
impl fmt::Display for Span<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.content, f)
impl std::fmt::Display for Span<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", &self.content)
}
}
@@ -529,15 +529,15 @@ mod tests {
#[test]
fn display_span() {
let span = Span::raw("test content");
assert_eq!(format!("{span}"), "test content");
assert_eq!(format!("{span:.4}"), "test");
}
#[test]
fn display_styled_span() {
let stylized_span = Span::styled("stylized test content", Style::new().green());
assert_eq!(format!("{stylized_span}"), "stylized test content");
assert_eq!(format!("{stylized_span:.8}"), "stylized");
}
#[test]
@@ -565,6 +565,7 @@ mod tests {
use rstest::rstest;
use super::*;
use crate::assert_buffer_eq;
#[test]
fn render() {
@@ -572,11 +573,12 @@ 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([Line::from(vec![
let expected = Buffer::with_lines(vec![Line::from(vec![
"test content".green().on_yellow(),
" ".into(),
])]);
assert_eq!(buf, expected);
assert_buffer_eq!(buf, expected);
}
#[rstest]
@@ -596,11 +598,10 @@ mod tests {
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 1));
span.render(Rect::new(0, 0, 5, 1), &mut buf);
let expected = Buffer::with_lines([Line::from(vec![
"test ".green().on_yellow(),
" ".into(),
])]);
assert_eq!(buf, expected);
let mut expected = Buffer::with_lines(vec![Line::from("test ")]);
expected.set_style(Rect::new(0, 0, 5, 1), (Color::Green, Color::Yellow));
assert_buffer_eq!(buf, expected);
}
/// When there is already a style set on the buffer, the style of the span should be
@@ -612,11 +613,12 @@ 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([Line::from(vec![
let expected = Buffer::with_lines(vec![Line::from(vec![
"test content".green().on_yellow().italic(),
" ".italic(),
])]);
assert_eq!(buf, expected);
assert_buffer_eq!(buf, expected);
}
/// When the span contains a multi-width grapheme, the grapheme will ensure that the cells
@@ -627,11 +629,12 @@ 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(["test 😃 content".green().on_yellow()]);
assert_eq!(buf, expected);
let expected = Buffer::with_lines(vec!["test 😃 content".green().on_yellow()]);
assert_buffer_eq!(buf, expected);
}
/// When the span contains a multi-width grapheme that does not fit in the area passed to
@@ -644,9 +647,11 @@ mod tests {
let mut buf = Buffer::empty(Rect::new(0, 0, 6, 1));
span.render(buf.area, &mut buf);
let expected =
Buffer::with_lines([Line::from(vec!["test ".green().on_yellow(), " ".into()])]);
assert_eq!(buf, expected);
let expected = Buffer::with_lines(vec![Line::from(vec![
"test ".green().on_yellow(),
" ".into(),
])]);
assert_buffer_eq!(buf, expected);
}
/// When the area passed to render overflows the buffer, the content should be truncated
@@ -658,11 +663,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([Line::from(vec![
let expected = Buffer::with_lines(vec![Line::from(vec![
" ".into(),
"test ".green().on_yellow(),
])]);
assert_eq!(buf, expected);
assert_buffer_eq!(buf, expected);
}
}
}

View File

@@ -1,5 +1,5 @@
#![warn(missing_docs)]
use std::{borrow::Cow, fmt};
use std::borrow::Cow;
use itertools::{Itertools, Position};
@@ -580,8 +580,8 @@ where
}
}
impl fmt::Display for Text<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
impl std::fmt::Display for Text<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for (position, line) in self.iter().with_position() {
if position == Position::Last {
write!(f, "{line}")?;
@@ -962,14 +962,19 @@ mod tests {
mod widget {
use super::*;
use crate::assert_buffer_eq;
#[test]
fn render() {
let text = Text::from("foo");
let area = Rect::new(0, 0, 5, 1);
let mut buf = Buffer::empty(area);
text.render(area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["foo "]));
let expected_buf = Buffer::with_lines(vec!["foo "]);
assert_buffer_eq!(buf, expected_buf);
}
#[rstest]
@@ -982,28 +987,40 @@ mod tests {
#[test]
fn render_right_aligned() {
let text = Text::from("foo").alignment(Alignment::Right);
let area = Rect::new(0, 0, 5, 1);
let mut buf = Buffer::empty(area);
text.render(area, &mut buf);
assert_eq!(buf, Buffer::with_lines([" foo"]));
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);
assert_eq!(buf, Buffer::with_lines([" foo "]));
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);
assert_eq!(buf, Buffer::with_lines([" foo "]));
let expected_buf = Buffer::with_lines(vec![" foo "]);
assert_buffer_eq!(buf, expected_buf);
}
#[test]
@@ -1013,10 +1030,14 @@ mod tests {
Line::from("bar").alignment(Alignment::Center),
])
.alignment(Alignment::Right);
let area = Rect::new(0, 0, 5, 2);
let mut buf = Buffer::empty(area);
text.render(area, &mut buf);
assert_eq!(buf, Buffer::with_lines([" foo", " bar "]));
let expected_buf = Buffer::with_lines(vec![" foo", " bar "]);
assert_buffer_eq!(buf, expected_buf);
}
#[test]
@@ -1025,9 +1046,10 @@ mod tests {
let mut buf = Buffer::empty(area);
Text::from("foo".on_blue()).render(area, &mut buf);
let mut expected = Buffer::with_lines(["foo "]);
let mut expected = Buffer::with_lines(vec!["foo "]);
expected.set_style(Rect::new(0, 0, 3, 1), Style::new().bg(Color::Blue));
assert_eq!(buf, expected);
assert_buffer_eq!(buf, expected);
}
#[test]
@@ -1035,9 +1057,10 @@ mod tests {
let mut buf = Buffer::empty(Rect::new(0, 0, 6, 1));
Text::from("foobar".on_blue()).render(Rect::new(0, 0, 3, 1), &mut buf);
let mut expected = Buffer::with_lines(["foo "]);
let mut expected = Buffer::with_lines(vec!["foo "]);
expected.set_style(Rect::new(0, 0, 3, 1), Style::new().bg(Color::Blue));
assert_eq!(buf, expected);
assert_buffer_eq!(buf, expected);
}
}

View File

@@ -245,15 +245,9 @@ 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;
@@ -300,7 +294,6 @@ pub trait StatefulWidget {
/// widget.render_ref(area, buf);
/// }
/// # }
/// # }
/// ```
#[stability::unstable(feature = "widget-ref")]
pub trait WidgetRef {
@@ -309,40 +302,13 @@ 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
@@ -355,7 +321,6 @@ where
/// # Examples
///
/// ```rust
/// # #[cfg(feature = "unstable-widget-ref")] {
/// use ratatui::{prelude::*, widgets::*};
///
/// struct Parent {
@@ -375,7 +340,6 @@ where
/// self.child.render_ref(area, buf);
/// }
/// }
/// # }
/// ```
impl<W: WidgetRef> WidgetRef for Option<W> {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
@@ -404,7 +368,6 @@ impl<W: WidgetRef> WidgetRef for Option<W> {
/// # Examples
///
/// ```rust
/// # #[cfg(feature = "unstable-widget-ref")] {
/// use ratatui::{prelude::*, widgets::*};
///
/// struct PersonalGreeting;
@@ -423,11 +386,10 @@ 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")]
@@ -508,79 +470,90 @@ 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))
}
mod widget {
use super::*;
struct Greeting;
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 "]));
}
#[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_ref {
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;
struct Farewell;
/// 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 "]));
}
impl WidgetRef for Greeting {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
Line::from("Hello").render(area, buf);
}
}
#[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 "]));
}
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;
#[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);
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]
@@ -588,181 +561,98 @@ mod tests {
"world".to_string()
}
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_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_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 "]));
}
#[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 option_widget_ref {
use super::*;
// 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 "]));
// }
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 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 "]));
}
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_some(mut buf: Buffer) {
let widget = Some(Greeting);
widget.render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["Hello "]));
}
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 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 function_widget {
use super::*;
#[rstest]
fn str_render(mut buf: Buffer) {
"hello world".render(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_render_ref(mut buf: Buffer) {
"hello world".render_ref(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(mut buf: Buffer) {
Some("hello world").render(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 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_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(mut buf: Buffer) {
String::from("hello world").render(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 "]));
}
#[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 "]),);
}
}

View File

@@ -45,7 +45,7 @@ pub use bar_group::BarGroup;
/// use ratatui::{prelude::*, widgets::*};
///
/// BarChart::default()
/// .block(Block::bordered().title("BarChart"))
/// .block(Block::default().title("BarChart").borders(Borders::ALL))
/// .bar_width(3)
/// .bar_gap(1)
/// .group_gap(3)
@@ -615,140 +615,151 @@ mod tests {
use itertools::iproduct;
use super::*;
use crate::widgets::BorderType;
use crate::{
assert_buffer_eq,
widgets::{BorderType, Borders},
};
#[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_eq!(buffer, Buffer::with_lines([" "; 3]));
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" "; 3]));
}
#[test]
fn data() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
let widget = BarChart::default().data(&[("foo", 1), ("bar", 2)]);
widget.render(buffer.area, &mut buffer);
#[rustfmt::skip]
let expected = Buffer::with_lines([
"",
"1 2 ",
"f b ",
]);
assert_eq!(buffer, expected);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
" ",
"1 2 ",
"f b ",
])
);
}
#[test]
fn block() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 5));
let block = Block::bordered()
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 5));
let block = Block::default()
.title("Block")
.border_type(BorderType::Double)
.title("Block");
.borders(Borders::ALL);
let widget = BarChart::default()
.data(&[("foo", 1), ("bar", 2)])
.block(block);
widget.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines([
"╔Block═══╗",
"║ █ ║",
"║1 2 ║",
"f b",
"╚════════╝",
]);
assert_eq!(buffer, expected);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
"╔Block════════╗",
"",
"║1 2 ║",
"║f b ║",
"╚═════════════╝",
])
);
}
#[test]
fn max() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
let without_max = BarChart::default().data(&[("foo", 1), ("bar", 2), ("baz", 100)]);
without_max.render(buffer.area, &mut buffer);
#[rustfmt::skip]
let expected = Buffer::with_lines([
"",
"",
"f b b ",
]);
assert_eq!(buffer, expected);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
" ",
" ",
"f b b ",
])
);
let with_max = BarChart::default()
.data(&[("foo", 1), ("bar", 2), ("baz", 100)])
.max(2);
with_max.render(buffer.area, &mut buffer);
#[rustfmt::skip]
let expected = Buffer::with_lines([
" █ █ ",
"1 2 █ ",
"f b b ",
]);
assert_eq!(buffer, expected);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
" █ █ ",
"1 2 █ ",
"f b b ",
])
);
}
#[test]
fn bar_style() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
let widget = BarChart::default()
.data(&[("foo", 1), ("bar", 2)])
.bar_style(Style::new().red());
widget.render(buffer.area, &mut buffer);
#[rustfmt::skip]
let mut expected = Buffer::with_lines([
" ",
"1 2 ",
"f b ",
let mut expected = Buffer::with_lines(vec![
"",
"1 2 ",
"f b ",
]);
for (x, y) in iproduct!([0, 2], [0, 1]) {
expected.get_mut(x, y).set_fg(Color::Red);
}
assert_eq!(buffer, expected);
assert_buffer_eq!(buffer, expected);
}
#[test]
fn bar_width() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
let widget = BarChart::default()
.data(&[("foo", 1), ("bar", 2)])
.bar_width(3);
widget.render(buffer.area, &mut buffer);
#[rustfmt::skip]
let expected = Buffer::with_lines([
" ███ ",
"█1█ █2█ ",
"foo bar ",
]);
assert_eq!(buffer, expected);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
" ███ ",
"█1█ █2█ ",
"foo bar ",
])
);
}
#[test]
fn bar_gap() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
let widget = BarChart::default()
.data(&[("foo", 1), ("bar", 2)])
.bar_gap(2);
widget.render(buffer.area, &mut buffer);
#[rustfmt::skip]
let expected = Buffer::with_lines([
"",
"1 2 ",
"f b ",
]);
assert_eq!(buffer, expected);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
" ",
"1 2 ",
"f b ",
])
);
}
#[test]
fn bar_set() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
let widget = BarChart::default()
.data(&[("foo", 0), ("bar", 1), ("baz", 3)])
.bar_set(symbols::bar::THREE_LEVELS);
widget.render(buffer.area, &mut buffer);
#[rustfmt::skip]
let expected = Buffer::with_lines([
"",
" ▄ 3 ",
"f b b ",
]);
assert_eq!(buffer, expected);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
" ",
" ▄ 3 ",
"f b b ",
])
);
}
#[test]
@@ -768,68 +779,67 @@ mod tests {
])
.bar_set(symbols::bar::NINE_LEVELS);
widget.render(Rect::new(0, 1, 18, 2), &mut buffer);
let expected = Buffer::with_lines([
" ",
" ▁ ▂ ▃ ▄ ▅ ▆ ▇ 8 ",
"a b c d e f g h i ",
]);
assert_eq!(buffer, expected);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
" ",
" ▁ ▂ ▃ ▄ ▅ ▆ ▇ 8 ",
"a b c d e f g h i ",
])
);
}
#[test]
fn value_style() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
let widget = BarChart::default()
.data(&[("foo", 1), ("bar", 2)])
.bar_width(3)
.value_style(Style::new().red());
widget.render(buffer.area, &mut buffer);
#[rustfmt::skip]
let mut expected = Buffer::with_lines([
" ███ ",
"█1█ █2█ ",
"foo bar ",
let mut expected = Buffer::with_lines(vec![
" ███ ",
"█1█ █2█ ",
"foo bar ",
]);
expected.get_mut(1, 1).set_fg(Color::Red);
expected.get_mut(5, 1).set_fg(Color::Red);
assert_eq!(buffer, expected);
assert_buffer_eq!(buffer, expected);
}
#[test]
fn label_style() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
let widget = BarChart::default()
.data(&[("foo", 1), ("bar", 2)])
.label_style(Style::new().red());
widget.render(buffer.area, &mut buffer);
#[rustfmt::skip]
let mut expected = Buffer::with_lines([
" ",
"1 2 ",
"f b ",
let mut expected = Buffer::with_lines(vec![
"",
"1 2 ",
"f b ",
]);
expected.get_mut(0, 2).set_fg(Color::Red);
expected.get_mut(2, 2).set_fg(Color::Red);
assert_eq!(buffer, expected);
assert_buffer_eq!(buffer, expected);
}
#[test]
fn style() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
let widget = BarChart::default()
.data(&[("foo", 1), ("bar", 2)])
.style(Style::new().red());
widget.render(buffer.area, &mut buffer);
#[rustfmt::skip]
let mut expected = Buffer::with_lines([
" ",
"1 2 ",
"f b ",
let mut expected = Buffer::with_lines(vec![
"",
"1 2 ",
"f b ",
]);
for (x, y) in iproduct!(0..10, 0..3) {
for (x, y) in iproduct!(0..15, 0..3) {
expected.get_mut(x, y).set_fg(Color::Red);
}
assert_eq!(buffer, expected);
assert_buffer_eq!(buffer, expected);
}
#[test]
@@ -855,13 +865,8 @@ mod tests {
let mut buffer = Buffer::empty(Rect::new(0, 0, 3, 3));
chart.render(buffer.area, &mut buffer);
#[rustfmt::skip]
let expected = Buffer::with_lines([
"",
"1 2",
"G ",
]);
assert_eq!(buffer, expected);
let expected = Buffer::with_lines(vec!["", "1 2", "G "]);
assert_buffer_eq!(buffer, expected);
}
fn build_test_barchart<'a>() -> BarChart<'a> {
@@ -887,7 +892,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([
let expected = Buffer::with_lines(vec![
"2█ ",
"3██ ",
"4███ ",
@@ -897,7 +902,8 @@ mod tests {
"5████",
"G2 ",
]);
assert_eq!(buffer, expected);
assert_buffer_eq!(buffer, expected);
}
#[test]
@@ -906,7 +912,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([
let expected = Buffer::with_lines(vec![
"2█ ",
"3██ ",
"4███ ",
@@ -915,7 +921,8 @@ mod tests {
"4███ ",
"5████",
]);
assert_eq!(buffer, expected);
assert_buffer_eq!(buffer, expected);
}
#[test]
@@ -924,15 +931,9 @@ mod tests {
let mut buffer = Buffer::empty(Rect::new(0, 0, 5, 5));
chart.render(buffer.area, &mut buffer);
#[rustfmt::skip]
let expected = Buffer::with_lines([
"2█ ",
"3██ ",
"4███ ",
"G1 ",
"3██ ",
]);
assert_eq!(buffer, expected);
let expected = Buffer::with_lines(vec!["2█ ", "3██ ", "4███ ", "G1 ", "3██ "]);
assert_buffer_eq!(buffer, expected);
}
fn test_horizontal_bars_label_width_greater_than_bar(bar_color: Option<Color>) {
@@ -955,7 +956,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(["label", "5████"]);
let mut expected = Buffer::with_lines(vec!["label", "5████"]);
// first line has a yellow foreground. first cell contains italic "5"
expected.get_mut(0, 1).modifier.insert(Modifier::ITALIC);
@@ -963,7 +964,11 @@ mod tests {
expected.get_mut(x, 1).set_fg(Color::Yellow);
}
let expected_color = bar_color.unwrap_or(Color::Yellow);
let expected_color = if let Some(color) = bar_color {
color
} else {
Color::Yellow
};
// second line contains the word "label". Since the bar value is 2,
// then the first 2 characters of "label" are italic red.
@@ -976,7 +981,7 @@ mod tests {
expected.get_mut(3, 0).set_fg(expected_color);
expected.get_mut(4, 0).set_fg(expected_color);
assert_eq!(buffer, expected);
assert_buffer_eq!(buffer, expected);
}
#[test]
@@ -999,13 +1004,9 @@ mod tests {
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
chart.render(buffer.area, &mut buffer);
#[rustfmt::skip]
let expected = Buffer::with_lines([
"Jan 10█ ",
"Feb 20████",
"Mar 5 ",
]);
assert_eq!(buffer, expected);
let expected = Buffer::with_lines(vec!["Jan 10█ ", "Feb 20████", "Mar 5 "]);
assert_buffer_eq!(buffer, expected);
}
#[test]
@@ -1026,13 +1027,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(["2████", "G1 "]);
let mut expected = Buffer::with_lines(vec!["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_eq!(buffer, expected);
assert_buffer_eq!(buffer, expected);
}
#[test]
@@ -1049,14 +1050,17 @@ mod tests {
let mut buffer = Buffer::empty(Rect::new(0, 0, 13, 5));
chart.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines([
" ▂ █ ▂",
" ▄ █ █ ▄ █",
"▆ 2 3 4 ▆ 2 3",
"a b c c a b c",
" G1 G2 ",
]);
assert_eq!(buffer, expected);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
" ▂ █ ▂",
" ▄ █ █ ▄ █",
"▆ 2 3 4 ▆ 2 3",
"a b c c a b c",
" G1 G2 ",
])
);
}
#[test]
@@ -1069,13 +1073,9 @@ mod tests {
let mut buffer = Buffer::empty(Rect::new(0, 0, 3, 3));
chart.render(buffer.area, &mut buffer);
#[rustfmt::skip]
let expected = Buffer::with_lines([
"",
"▆ 5",
" G",
]);
assert_eq!(buffer, expected);
let expected = Buffer::with_lines(vec!["", "▆ 5", " G"]);
assert_buffer_eq!(buffer, expected);
}
#[test]
@@ -1098,14 +1098,15 @@ mod tests {
let mut buffer = Buffer::empty(Rect::new(0, 0, 11, 5));
chart.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines([
let expected = Buffer::with_lines(vec![
" ▆▆▆ ███",
" ███ ███",
"▃▃▃ ███ ███",
"写█ 写█ 写█",
"B1 B2 B2 ",
]);
assert_eq!(buffer, expected);
assert_buffer_eq!(buffer, expected);
}
#[test]
@@ -1117,7 +1118,7 @@ mod tests {
.bar_gap(0);
let mut buffer = Buffer::empty(Rect::new(0, 0, 0, 10));
chart.render(buffer.area, &mut buffer);
assert_eq!(buffer, Buffer::empty(Rect::new(0, 0, 0, 10)));
assert_buffer_eq!(buffer, Buffer::empty(Rect::new(0, 0, 0, 10)));
}
#[test]
@@ -1142,7 +1143,8 @@ mod tests {
let mut buffer = Buffer::empty(Rect::new(0, 0, 17, 1));
chart.render(buffer.area, &mut buffer);
assert_eq!(buffer, Buffer::with_lines([" ▁ ▂ ▃ ▄ ▅ ▆ ▇ 8"]));
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" ▁ ▂ ▃ ▄ ▅ ▆ ▇ 8"]));
}
#[test]
@@ -1167,12 +1169,15 @@ 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);
let expected = Buffer::with_lines([
" ",
" ▁ ▂ ▃ ▄ ▅ ▆ ▇ 8",
"a b c d e f g h i",
]);
assert_eq!(buffer, expected);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
" ",
" ▁ ▂ ▃ ▄ ▅ ▆ ▇ 8",
"a b c d e f g h i",
])
);
}
#[test]
@@ -1197,12 +1202,15 @@ mod tests {
let mut buffer = Buffer::empty(Rect::new(0, 0, 17, 3));
chart.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines([
" ▁ ▂ ▃ ▄ ▅ ▆ ▇ 8",
"a b c d e f g h i",
" Group ",
]);
assert_eq!(buffer, expected);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
" ▁ ▂ ▃ ▄ ▅ ▆ ▇ 8",
"a b c d e f g h i",
" Group ",
])
);
}
#[test]
@@ -1227,12 +1235,15 @@ mod tests {
let mut buffer = Buffer::empty(Rect::new(0, 0, 26, 3));
chart.render(buffer.area, &mut buffer);
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);
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 ",
])
);
}
#[test]
@@ -1257,13 +1268,16 @@ mod tests {
let mut buffer = Buffer::empty(Rect::new(0, 0, 17, 4));
chart.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines([
" ▂ ▄ ▆ █",
" ▂ ▄ ▆ 4 5 6 7 8",
"a b c d e f g h i",
" Group ",
]);
assert_eq!(buffer, expected);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
" ▂ ▄ ▆ █",
" ▂ ▄ ▆ 4 5 6 7 8",
"a b c d e f g h i",
" Group ",
])
);
}
#[test]
@@ -1286,12 +1300,15 @@ 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);
let expected = Buffer::with_lines([
" ",
" ▁ ▂ ▃ ▄ ▅ ▆ ▇ 8",
" Group ",
]);
assert_eq!(buffer, expected);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
" ",
" ▁ ▂ ▃ ▄ ▅ ▆ ▇ 8",
" Group ",
])
);
}
#[test]
@@ -1302,9 +1319,13 @@ mod tests {
let mut buffer = Buffer::empty(Rect::new(0, 0, 59, 1));
chart.render(buffer.area, &mut buffer);
let expected =
Buffer::with_lines([" ▁ ▁ ▁ ▁ ▂ ▂ ▂ ▃ ▃ ▃ ▃ ▄ ▄ ▄ ▄ ▅ ▅ ▅ ▆ ▆ ▆ ▆ ▇ ▇ ▇ █"]);
assert_eq!(buffer, expected);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
" ▁ ▁ ▁ ▁ ▂ ▂ ▂ ▃ ▃ ▃ ▃ ▄ ▄ ▄ ▄ ▅ ▅ ▅ ▆ ▆ ▆ ▆ ▇ ▇ ▇ █",
])
);
}
#[test]
@@ -1316,14 +1337,17 @@ mod tests {
let mut buffer = Buffer::empty(Rect::new(0, 0, 7, 6));
chart.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines([
" ██ ",
" ██ ",
"▄▄ ██ ",
"██ ██ ",
"1█ 2",
"a b ",
]);
assert_eq!(buffer, expected);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
" ██ ",
"",
"▄▄ ██ ",
"██ ██ ",
"1█ 2█ ",
"a b ",
])
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -35,14 +35,6 @@ 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.
@@ -56,9 +48,13 @@ impl Padding {
}
/// Creates a `Padding` with all fields set to `0`.
#[deprecated = "use Padding::ZERO"]
pub const fn zero() -> Self {
Self::ZERO
Self {
left: 0,
right: 0,
top: 0,
bottom: 0,
}
}
/// Creates a `Padding` with the same value for `left` and `right`.
@@ -177,6 +173,7 @@ 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));
@@ -192,6 +189,7 @@ 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);

View File

@@ -1,4 +1,4 @@
use std::fmt;
use std::fmt::{self, Debug};
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 fmt::Debug for Borders {
impl Debug for Borders {
/// Display the Borders bitflags as a list of names. For example, `Borders::NONE` will be
/// displayed as `NONE` and `Borders::ALL` will be displayed as `ALL`. If multiple flags are
/// set, they will be displayed separated by a pipe character.

View File

@@ -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 const fn new(display_date: Date, events: DS) -> Self {
pub fn new(display_date: Date, events: DS) -> Self {
Self {
display_date,
events,
show_surrounding: None,
show_weekday: None,
show_month: None,
default_style: Style::new(),
default_style: Style::default(),
block: None,
}
}
@@ -90,10 +90,10 @@ impl<'a, DS: DateStyler> Monthly<'a, DS> {
}
/// Return a style with only the background from the default style
const fn default_bg(&self) -> Style {
fn default_bg(&self) -> Style {
match self.default_style.bg {
None => Style::new(),
Some(c) => Style::new().bg(c),
None => Style::default(),
Some(c) => Style::default().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() != self.display_date.month().next() {
while curr_day.month() as u8 != self.display_date.month().next() as u8 {
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_else(|_| OffsetDateTime::now_utc())
.unwrap_or(OffsetDateTime::now_utc())
.date(),
style.into(),
);

View File

@@ -19,7 +19,7 @@ mod points;
mod rectangle;
mod world;
use std::{fmt, iter::zip};
use std::{fmt::Debug, 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: fmt::Debug {
trait Grid: 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::bordered().title("Canvas"))
/// .block(Block::default().title("Canvas").borders(Borders::ALL))
/// .x_bounds([-180.0, 180.0])
/// .y_bounds([-90.0, 90.0])
/// .paint(|ctx| {
@@ -817,7 +817,9 @@ mod tests {
// results in the expected output
fn test_marker(marker: Marker, expected: &str) {
let area = Rect::new(0, 0, 5, 5);
let mut buf = Buffer::filled(area, Cell::new("x"));
let mut cell = Cell::default();
cell.set_char('x');
let mut buf = Buffer::filled(area, &cell);
let horizontal_line = Line {
x1: 0.0,
y1: 0.0,

View File

@@ -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([
let expected = Buffer::with_lines(vec![
" ⢀⣠⢤⣀ ",
" ⢰⠋ ⠈⣇",
" ⠘⣆⡀ ⣠⠇",

View File

@@ -112,111 +112,154 @@ fn draw_line_high(painter: &mut Painter, x1: usize, y1: usize, x2: usize, y2: us
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::Line;
use crate::{assert_buffer_eq, prelude::*, widgets::canvas::Canvas};
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>>,
{
#[allow(clippy::needless_pass_by_value)]
#[track_caller]
fn test(line: Line, expected_lines: Vec<&str>) {
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);
let mut expected = Buffer::with_lines(expected_lines);
for cell in &mut expected.content {
if cell.symbol() == "" {
cell.set_style(Style::new().red());
}
}
assert_eq!(buffer, expected);
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![
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
],
);
}
}

View File

@@ -27,7 +27,7 @@ pub enum MapResolution {
}
impl MapResolution {
const fn data(self) -> &'static [(f64, f64)] {
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::{prelude::*, widgets::canvas::Canvas};
use crate::{assert_buffer_eq, 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([
let expected = Buffer::with_lines(vec![
" ",
" ••••••• •• •• •• • ",
" •••••••••••••• ••• •••• ••• •• •••• ",
@@ -143,7 +143,7 @@ mod tests {
"",
" ",
]);
assert_eq!(buffer, expected);
assert_buffer_eq!(buffer, expected);
}
#[test]
@@ -160,7 +160,7 @@ mod tests {
});
});
canvas.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines([
let expected = Buffer::with_lines(vec![
" ",
" ⢀⣠⠤⠤⠤⠔⢤⣤⡄⠤⡠⣄⠢⠂⢢⠰⣠⡄⣀⡀ ⣀ ",
" ⢀⣀⡤⣦⠲⢶⣿⣮⣿⡉⣰⢶⢏⡂ ⢀⣟⠁ ⢺⣻⢿⠏ ⠈⠉⠁ ⢀⣀ ⠈⠓⢳⣢⣂⡀ ",
@@ -202,6 +202,6 @@ mod tests {
"⠶⠔⠲⠤⠠⠜⢗⠤⠄ ⠘⠉ ⠁ ⠈⠉⠒⠔⠤",
" ",
]);
assert_eq!(buffer, expected);
assert_buffer_eq!(buffer, expected);
}
}

View File

@@ -66,7 +66,7 @@ impl Shape for Rectangle {
#[cfg(test)]
mod tests {
use super::*;
use crate::{prelude::*, widgets::canvas::Canvas};
use crate::{assert_buffer_eq, 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([
let mut expected = Buffer::with_lines(vec![
"██████████",
"█ █",
"█ █",
@@ -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_eq!(buffer, expected);
expected.set_style(buffer.area.inner(&Margin::new(1, 1)), Style::reset());
assert_buffer_eq!(buffer, expected);
}
#[test]
@@ -119,7 +119,7 @@ mod tests {
});
});
canvas.render(buffer.area, &mut buffer);
let mut expected = Buffer::with_lines([
let mut expected = Buffer::with_lines(vec![
"█▀▀▀▀▀▀▀▀█",
"█ █",
"█ █",
@@ -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_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_buffer_eq!(buffer, expected);
}
#[test]
@@ -163,7 +163,7 @@ mod tests {
});
});
canvas.render(buffer.area, &mut buffer);
let mut expected = Buffer::with_lines([
let mut expected = Buffer::with_lines(vec![
"⡏⠉⠉⠉⠉⠉⠉⠉⠉⢹",
"⡇⢠⠤⠤⠤⠤⠤⠤⡄⢸",
"⡇⢸ ⡇⢸",
@@ -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_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_buffer_eq!(buffer, expected);
}
}

View File

@@ -1,5 +1,5 @@
/// [Source data](http://www.gnuplotting.org/plotting-the-world-revisited)
pub const WORLD_HIGH_RESOLUTION: [(f64, f64); 5125] = [
pub static WORLD_HIGH_RESOLUTION: [(f64, f64); 5125] = [
(-163.7128, -78.5956),
(-163.1058, -78.2233),
(-161.2451, -78.3801),
@@ -5127,7 +5127,7 @@ pub const WORLD_HIGH_RESOLUTION: [(f64, f64); 5125] = [
(180.0, -84.71338),
];
pub const WORLD_LOW_RESOLUTION: [(f64, f64); 1166] = [
pub static WORLD_LOW_RESOLUTION: [(f64, f64); 1166] = [
(-92.32, 48.24),
(-88.13, 48.92),
(-83.11, 46.27),

View File

@@ -8,7 +8,7 @@ use crate::{
prelude::*,
widgets::{
canvas::{Canvas, Line as CanvasLine, Points},
Block,
Block, Borders,
},
};
@@ -475,7 +475,7 @@ struct ChartLayout {
///
/// // Create the chart and link all the parts together
/// let chart = Chart::new(datasets)
/// .block(Block::new().title("Chart"))
/// .block(Block::default().title("Chart"))
/// .x_axis(x_axis)
/// .y_axis(y_axis);
/// ```
@@ -1050,7 +1050,9 @@ impl WidgetRef for Chart<'_> {
if let Some(legend_area) = layout.legend_area {
buf.set_style(legend_area, original_style);
Block::bordered().render(legend_area, buf);
Block::default()
.borders(Borders::ALL)
.render(legend_area, buf);
for (i, (dataset_name, dataset_style)) in self
.datasets
@@ -1111,10 +1113,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,
@@ -1209,6 +1211,7 @@ 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]));
}
@@ -1243,25 +1246,30 @@ 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([
let expected = Buffer::with_lines(vec![
" ┌──────────────┐",
" │Very long name│",
" │ Short name│",
" └──────────────┘",
" ",
]);
assert_eq!(buffer, expected);
assert_buffer_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([
let expected = Buffer::with_lines(vec![
"┌───┐ ",
"│Ds1│ ",
"└───┘ ",
@@ -1283,6 +1291,7 @@ mod tests {
" ",
" ",
]);
assert_eq!(buffer, expected);
}
@@ -1290,10 +1299,13 @@ 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([
let expected = Buffer::with_lines(vec![
"The title overlap a legend. ",
" ┌───┐",
" │Ds1│",
@@ -1315,6 +1327,7 @@ mod tests {
" ",
" ",
]);
assert_eq!(buffer, expected);
}
@@ -1322,10 +1335,13 @@ 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([
let expected = Buffer::with_lines(vec![
" ",
" ",
" ",
@@ -1337,6 +1353,7 @@ mod tests {
" ",
" ",
]);
assert_eq!(buffer, expected);
}
@@ -1345,8 +1362,12 @@ 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,
@@ -1360,103 +1381,171 @@ 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);
}
}
#[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>>,
{
#[allow(clippy::too_many_lines)]
#[test]
fn test_legend_of_chart_have_odd_margin_size() {
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 = Chart::new(vec![Dataset::default().name(name)])
.legend_position(legend_position)
.hidden_legend_constraints((Constraint::Percentage(100), Constraint::Percentage(100)));
let chart = base_chart
.clone()
.legend_position(Some(LegendPosition::TopLeft));
buffer.reset();
chart.render(buffer.area, &mut buffer);
assert_eq!(buffer, Buffer::with_lines(expected));
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![
" ",
" ",
" ",
" ",
" ",
" ",
])
);
}
}

View File

@@ -11,7 +11,7 @@ use crate::prelude::*;
/// use ratatui::{prelude::*, widgets::*};
///
/// fn draw_on_clear(f: &mut Frame, area: Rect) {
/// let block = Block::bordered().title("Block");
/// let block = Block::default().title("Block").borders(Borders::ALL);
/// f.render_widget(Clear, area); // <- this will clear/reset the area first
/// f.render_widget(block, area); // now render the block widget
/// }
@@ -43,21 +43,24 @@ impl WidgetRef for Clear {
#[cfg(test)]
mod tests {
use super::*;
use crate::assert_buffer_eq;
#[test]
fn render() {
let mut buffer = Buffer::with_lines(["xxxxxxxxxxxxxxx"; 7]);
let mut buf = Buffer::with_lines(vec!["xxxxxxxxxxxxxxx"; 7]);
let clear = Clear;
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);
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",
])
);
}
}

View File

@@ -5,10 +5,8 @@ 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.
@@ -19,7 +17,7 @@ use crate::{prelude::*, widgets::Block};
/// use ratatui::{prelude::*, widgets::*};
///
/// Gauge::default()
/// .block(Block::bordered().title("Progress"))
/// .block(Block::default().borders(Borders::ALL).title("Progress"))
/// .gauge_style(
/// Style::default()
/// .fg(Color::White)
@@ -32,8 +30,7 @@ use crate::{prelude::*, widgets::Block};
/// # See also
///
/// - [`LineGauge`] for a thin progress bar
#[allow(clippy::struct_field_names)] // gauge_style needs to be differentiated to style
#[derive(Debug, Default, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq)]
pub struct Gauge<'a> {
block: Option<Block<'a>>,
ratio: f64,
@@ -43,6 +40,19 @@ 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`].
///
@@ -154,12 +164,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_gauge(inner, buf);
self.render_gague(inner, buf);
}
}
impl Gauge<'_> {
fn render_gauge(&self, gauge_area: Rect, buf: &mut Buffer) {
fn render_gague(&self, gauge_area: Rect, buf: &mut Buffer) {
if gauge_area.is_empty() {
return;
}
@@ -224,20 +234,14 @@ 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`].
///
/// 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.
/// This can be useful to indicate the progression of a task, like a download.
///
/// # Examples:
///
@@ -245,8 +249,8 @@ fn get_unicode_block<'a>(frac: f64) -> &'a str {
/// use ratatui::{prelude::*, widgets::*};
///
/// LineGauge::default()
/// .block(Block::bordered().title("Progress"))
/// .filled_style(
/// .block(Block::default().borders(Borders::ALL).title("Progress"))
/// .gauge_style(
/// Style::default()
/// .fg(Color::White)
/// .bg(Color::Black)
@@ -266,8 +270,7 @@ pub struct LineGauge<'a> {
label: Option<Line<'a>>,
line_set: symbols::line::Set,
style: Style,
filled_style: Style,
unfilled_style: Style,
gauge_style: Style,
}
impl<'a> LineGauge<'a> {
@@ -339,40 +342,9 @@ 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 {
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.gauge_style = style.into();
self
}
}
@@ -406,12 +378,26 @@ impl WidgetRef for LineGauge<'_> {
for col in start..end {
buf.get_mut(col, row)
.set_symbol(self.line_set.horizontal)
.set_style(self.filled_style);
.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,
});
}
for col in end..gauge_area.right() {
buf.get_mut(col, row)
.set_symbol(self.line_set.horizontal)
.set_style(self.unfilled_style);
.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,
});
}
}
}
@@ -491,36 +477,24 @@ 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!(
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()
}
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."
);
}
}

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More