Compare commits

..

13 Commits

Author SHA1 Message Date
Dheepak Krishnamurthy
ccf9d92f10 test: Add more tests for selection and marks behavior 2024-05-13 23:26:47 -04:00
Dheepak Krishnamurthy
b712034644 feat: Show highlight_column only if there are marks 2024-05-13 23:23:57 -04:00
Dheepak Krishnamurthy
38fca62fa9 chore: Use assert_eq instead of assert_buffer_eq 2024-05-13 22:51:31 -04:00
Dheepak Krishnamurthy
3a51a027a6 Merge branch 'main' into kd/multi-select-table 2024-05-13 22:49:22 -04:00
Dheepak Krishnamurthy
5b30f2275c docs: Update docstring for field 2024-05-13 22:45:35 -04:00
Dheepak Krishnamurthy
b4c27c744c chore: Move pretty assert to top of file 2024-05-13 20:05:20 -04:00
Dheepak Krishnamurthy
977a4899c8 chore: Update serialized data representation 2024-05-13 20:03:59 -04:00
Dheepak Krishnamurthy
cd27b4829a docs: use pretty assertion 2024-05-13 20:00:56 -04:00
Dheepak Krishnamurthy
feee871519 docs: fix broken test 2024-05-13 19:57:56 -04:00
Dheepak Krishnamurthy
0051bb2037 build(test): Add more tests 2024-05-13 03:59:28 -04:00
Dheepak Krishnamurthy
477217c77a feat: Better spacing 2024-05-13 03:45:22 -04:00
Dheepak Krishnamurthy
31de3586f7 feat: Add mark, unmark and mark_highlight symbols 2024-05-13 03:27:22 -04:00
Dheepak Krishnamurthy
f702025b75 feat: Add multi-selection for table 2024-05-13 01:20:12 -04:00
90 changed files with 6882 additions and 7250 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

View File

@@ -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`
@@ -52,63 +47,6 @@ 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])
@@ -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

10551
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,6 +1,6 @@
[package]
name = "ratatui"
version = "0.26.3" # crate version
version = "0.26.2" # crate version
authors = ["Florian Dehau <work@fdehau.com>", "The Ratatui Developers"]
description = "A library that's all about cooking up terminal user interfaces"
documentation = "https://docs.rs/ratatui/latest/ratatui/"
@@ -47,6 +47,9 @@ unicode-width = "0.1"
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"
@@ -74,6 +77,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 +91,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 +106,15 @@ use_self = "warn"
## which allows you to set the underline color of text.
default = ["crossterm", "underline-color"]
#! Generally an application will only use one backend, so you should only enable one of the following features:
## enables the [`CrosstermBackend`](backend::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 +126,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 +139,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 +155,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
@@ -201,13 +194,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 +219,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 +239,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 +249,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 +274,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 +325,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

@@ -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::bordered().title("Greeting")),
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::bordered().title("Left"),
inner_layout[0],
);
frame.render_widget(
Block::bordered().title("Right"),
inner_layout[1],
);
}
```
@@ -330,14 +331,17 @@ Running this example produces the following output:
[License Badge]: https://img.shields.io/crates/l/ratatui?style=flat-square&color=1370D3
[CI Badge]: https://img.shields.io/github/actions/workflow/status/ratatui-org/ratatui/ci.yml?style=flat-square&logo=github
[CI Workflow]: https://github.com/ratatui-org/ratatui/actions/workflows/ci.yml
[Codecov Badge]: https://img.shields.io/codecov/c/github/ratatui-org/ratatui?logo=codecov&style=flat-square&token=BAQ8SOKEST&color=C43AC3&logoColor=C43AC3
[Codecov Badge]:
https://img.shields.io/codecov/c/github/ratatui-org/ratatui?logo=codecov&style=flat-square&token=BAQ8SOKEST&color=C43AC3&logoColor=C43AC3
[Codecov]: https://app.codecov.io/gh/ratatui-org/ratatui
[Deps.rs Badge]: https://deps.rs/repo/github/ratatui-org/ratatui/status.svg?style=flat-square
[Deps.rs]: https://deps.rs/repo/github/ratatui-org/ratatui
[Discord Badge]: https://img.shields.io/discord/1070692720437383208?label=discord&logo=discord&style=flat-square&color=1370D3&logoColor=1370D3
[Discord Badge]:
https://img.shields.io/discord/1070692720437383208?label=discord&logo=discord&style=flat-square&color=1370D3&logoColor=1370D3
[Discord Server]: https://discord.gg/pMCEU9hNEj
[Docs Badge]: https://img.shields.io/docsrs/ratatui?logo=rust&style=flat-square&logoColor=E05D44
[Matrix Badge]: https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix&color=C43AC3
[Matrix Badge]:
https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix&color=C43AC3
[Matrix]: https://matrix.to/#/#ratatui:matrix.org
[Sponsors Badge]: https://img.shields.io/github/sponsors/ratatui-org?logo=github&style=flat-square&color=1370D3

View File

@@ -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,17 +19,13 @@ 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},
prelude::*,
widgets::{Bar, BarChart, BarGroup, Block, Paragraph},
};

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,

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<()> {

View File

@@ -19,18 +19,13 @@ 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,
prelude::*,
widgets::{block::Title, Axis, Block, Chart, Dataset, GraphType, LegendPosition},
};

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},
};

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(),
_ => {}
},
_ => {}

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, KeyEventKind},
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,18 @@ impl App {
fn handle_events(&mut self) -> Result<()> {
if let Event::Key(key) = event::read()? {
use KeyCode::*;
if key.kind != KeyEventKind::Press {
return Ok(());
}
match key.code {
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(),
_ => (),
}
}

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;
@@ -83,7 +76,7 @@ fn draw_gauges(f: &mut Frame, app: &mut App, area: Rect) {
let line_gauge = LineGauge::default()
.block(Block::new().title("LineGauge:"))
.filled_style(Style::default().fg(Color::Magenta))
.gauge_style(Style::default().fg(Color::Magenta))
.line_set(if app.enhanced_graphics {
symbols::line::THICK
} else {

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 {
@@ -94,13 +82,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,8 +276,6 @@ fn render_glyph(glyph: [u8; 8], area: Rect, buf: &mut Buffer, pixel_size: PixelS
#[cfg(test)]
mod tests {
use ratatui::style::Stylize;
use super::*;
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;

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::default().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,
});
@@ -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},
};
@@ -61,6 +56,7 @@ fn hello_world(frame: &mut Frame) {
);
}
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()? {

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);

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;
@@ -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,24 +13,19 @@
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
#![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,
layout::Constraint::*,
prelude::*,
widgets::{Block, Paragraph},
};

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,7 +186,7 @@ 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()
@@ -258,8 +238,8 @@ 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()
@@ -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,14 +31,12 @@
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,
prelude::*,
widgets::{Block, Paragraph},
};

View File

@@ -19,17 +19,13 @@ 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},
prelude::*,
widgets::{Block, Paragraph, Wrap},
};

View File

@@ -18,16 +18,13 @@
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},
prelude::*,
widgets::{Block, Clear, Paragraph, Wrap},
};

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},
};

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,
}),

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);

View File

@@ -29,17 +29,13 @@
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},
prelude::*,
widgets::{Block, List, ListItem, Paragraph},
};
@@ -90,7 +86,7 @@ impl App {
///
/// 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 {
fn byte_index(&mut self) -> usize {
self.input
.char_indices()
.map(|(i, _)| i)

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, Colors, ContentStyle,
Print, SetAttribute, SetBackgroundColor, SetColors, 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>

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>
@@ -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.

View File

@@ -170,29 +170,26 @@ impl Backend for TestBackend {
}
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(())
}
@@ -329,7 +326,8 @@ 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]);
@@ -365,7 +363,8 @@ mod tests {
#[test]
fn clear() {
let mut backend = TestBackend::new(4, 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.clear().unwrap();

View File

@@ -54,14 +54,14 @@ 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)
Self::filled(area, &Cell::default())
}
/// 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 content = vec![cell.clone(); size];
Self { area, content }
}
@@ -278,22 +278,22 @@ 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);
self.content.resize(area.area() as usize, Cell::default());
// Move original content to the appropriate space
let size = self.area.area() as usize;
@@ -303,7 +303,7 @@ impl Buffer {
let k = ((y - area.y) * area.width + x - area.x) as usize;
if i != k {
self.content[k] = self.content[i].clone();
self.content[i].reset();
self.content[i] = Cell::default();
}
}
@@ -453,6 +453,12 @@ mod tests {
use super::*;
fn cell(s: &str) -> Cell {
let mut cell = Cell::default();
cell.set_symbol(s);
cell
}
#[test]
fn debug_empty_buffer() {
let buffer = Buffer::empty(Rect::ZERO);
@@ -743,14 +749,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,10 +764,10 @@ 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]
@@ -783,23 +789,22 @@ 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([
"┌Title─┐ ",
"└──────┘ ",
]);
#[rustfmt::skip]
let next = Buffer::with_lines([
"┌称号──┐ ",
"└──────┘ ",
@@ -807,12 +812,12 @@ mod tests {
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("")),
]
);
}
@@ -825,11 +830,7 @@ mod tests {
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("")),]
);
}
@@ -842,25 +843,59 @@ mod tests {
}
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_eq!(one, Buffer::with_lines(["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_eq!(one, Buffer::with_lines(["22 ", "22 ", " 11", " 11"]));
}
#[test]
fn merge3() {
let mut one = Buffer::filled(
Rect {
x: 3,
@@ -868,7 +903,7 @@ mod tests {
width: 2,
height: 2,
},
Cell::new("1"),
Cell::default().set_symbol("1"),
);
let two = Buffer::filled(
Rect {
@@ -877,48 +912,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(["222 ", "222 ", "2221", "2221"]);
merged.area = Rect {
x: 1,
y: 1,
width: 4,
height: 4,
};
assert_eq!(one, expected);
assert_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]

View File

@@ -34,29 +34,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 +86,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 +110,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 +126,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 +144,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;

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

@@ -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,

View File

@@ -119,8 +119,9 @@ impl Rect {
/// Returns a new `Rect` inside the current one, with the given margin on each side.
///
/// If the margin is larger than the `Rect`, the returned `Rect` will have no area.
#[allow(clippy::trivially_copy_pass_by_ref)] // See PR #1008
#[must_use = "method returns the modified value"]
pub const fn inner(self, margin: Margin) -> Self {
pub const fn inner(self, margin: &Margin) -> Self {
let doubled_margin_horizontal = margin.horizontal.saturating_mul(2);
let doubled_margin_vertical = margin.vertical.saturating_mul(2);
@@ -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)
);
}

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()?;
@@ -260,6 +255,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 +316,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 +355,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

@@ -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;
@@ -646,80 +652,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

@@ -313,7 +313,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,16 +333,6 @@ 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 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
@@ -588,7 +587,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
];

View File

@@ -137,9 +137,9 @@ macro_rules! modifier {
/// ```
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 +179,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

@@ -83,7 +83,6 @@ 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();
@@ -91,7 +90,6 @@ impl Frame<'_> {
/// let block = Block::new();
/// let area = Rect::new(0, 0, 5, 5);
/// frame.render_widget_ref(block, area);
/// # }
/// ```
#[allow(clippy::needless_pass_by_value)]
#[stability::unstable(feature = "widget-ref")]
@@ -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

@@ -1169,7 +1169,7 @@ mod tests {
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 mut buf = Buffer::filled(Rect::new(0, 0, 10, 1), Cell::default().set_symbol("X"));
let area = Rect::new(2, 0, 6, 1);
line.render_ref(area, &mut buf);
assert_eq!(buf, Buffer::with_lines([expected]));
@@ -1188,7 +1188,7 @@ mod tests {
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"));
let mut buf = Buffer::filled(area, Cell::default().set_symbol("X"));
line.render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines([expected]));
}

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

@@ -963,7 +963,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.

View File

@@ -168,7 +168,7 @@ impl<'a> Block<'a> {
border_style: Style::new(),
border_set: BorderType::Plain.to_border_set(),
style: Style::new(),
padding: Padding::ZERO,
padding: Padding::zero(),
}
}
@@ -469,38 +469,6 @@ impl<'a> Block<'a> {
self
}
/// Defines the padding inside a `Block`.
///
/// See [`Padding`] for more information.
///
/// # Examples
///
/// This renders a `Block` with no padding (the default).
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// Block::bordered().padding(Padding::ZERO);
/// // Renders
/// // ┌───────┐
/// // │content│
/// // └───────┘
/// ```
///
/// This example shows a `Block` with padding left and right ([`Padding::horizontal`]).
/// Notice the two spaces before and after the content.
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// Block::bordered().padding(Padding::horizontal(2));
/// // Renders
/// // ┌───────────┐
/// // │ content │
/// // └───────────┘
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub const fn padding(mut self, padding: Padding) -> Self {
self.padding = padding;
self
}
/// Compute the inner area of a block based on its border visibility rules.
///
/// # Examples
@@ -561,6 +529,38 @@ impl<'a> Block<'a> {
.iter()
.any(|title| title.position.unwrap_or(self.titles_position) == position)
}
/// Defines the padding inside a `Block`.
///
/// See [`Padding`] for more information.
///
/// # Examples
///
/// This renders a `Block` with no padding (the default).
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// Block::bordered().padding(Padding::zero());
/// // Renders
/// // ┌───────┐
/// // │content│
/// // └───────┘
/// ```
///
/// This example shows a `Block` with padding left and right ([`Padding::horizontal`]).
/// Notice the two spaces before and after the content.
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// Block::bordered().padding(Padding::horizontal(2));
/// // Renders
/// // ┌───────────┐
/// // │ content │
/// // └───────────┘
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub const fn padding(mut self, padding: Padding) -> Self {
self.padding = padding;
self
}
}
impl BorderType {
@@ -997,7 +997,7 @@ mod tests {
border_style: Style::new(),
border_set: BorderType::Plain.to_border_set(),
style: Style::new(),
padding: Padding::ZERO,
padding: Padding::zero(),
}
);
}

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

@@ -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

@@ -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

@@ -98,7 +98,7 @@ mod tests {
"██████████",
]);
expected.set_style(buffer.area, Style::new().red());
expected.set_style(buffer.area.inner(Margin::new(1, 1)), Style::reset());
expected.set_style(buffer.area.inner(&Margin::new(1, 1)), Style::reset());
assert_eq!(buffer, expected);
}
@@ -132,8 +132,8 @@ 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());
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);
}
@@ -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());
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);
}
}

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.
@@ -33,7 +31,7 @@ use crate::{prelude::*, widgets::Block};
///
/// - [`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 +41,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`].
///
@@ -224,20 +235,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:
///
@@ -246,7 +251,7 @@ fn get_unicode_block<'a>(frac: f64) -> &'a str {
///
/// LineGauge::default()
/// .block(Block::bordered().title("Progress"))
/// .filled_style(
/// .gauge_style(
/// Style::default()
/// .fg(Color::White)
/// .bg(Color::Black)
@@ -266,8 +271,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 +343,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 +379,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 +478,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."
);
}
}

View File

@@ -706,6 +706,48 @@ impl<'a> List<'a> {
self
}
/// Defines the list direction (up or down)
///
/// Defines if the `List` is displayed *top to bottom* (default) or *bottom to top*. Use
/// [`Corner::BottomLeft`] to go *bottom to top*. **Any** other variant will go *top to bottom*.
/// If there is too few items to fill the screen, the list will stick to the starting edge.
///
/// This is set to [`Corner::TopLeft`] by default.
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// ## Note
///
/// Despite its name, this method doesn't change the horizontal alignment, i.e. the `List`
/// **won't** start in a corner.
///
/// # Example
///
/// Same as default, i.e. *top to bottom*. Despite the name implying otherwise.
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # let items = ["Item 1"];
/// let list = List::new(items).start_corner(Corner::BottomRight);
/// ```
///
/// Bottom to top
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # let items = ["Item 1"];
/// let list = List::new(items).start_corner(Corner::BottomLeft);
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
#[deprecated(since = "0.25.0", note = "You should use `List::direction` instead.")]
pub fn start_corner(self, corner: Corner) -> Self {
if corner == Corner::BottomLeft {
self.direction(ListDirection::BottomToTop)
} else {
self.direction(ListDirection::TopToBottom)
}
}
/// Returns the number of [`ListItem`]s in the list
pub fn len(&self) -> usize {
self.items.len()
@@ -1649,6 +1691,30 @@ mod tests {
assert_eq!(buffer, Buffer::with_lines(expected));
}
#[rstest]
#[case::topleft(Corner::TopLeft, [
"Item 0 ",
"Item 1 ",
"Item 2 ",
" ",
])]
#[case::bottomleft(Corner::BottomLeft, [
" ",
"Item 2 ",
"Item 1 ",
"Item 0 ",
])]
fn start_corner<'line, Lines>(#[case] corner: Corner, #[case] expected: Lines)
where
Lines: IntoIterator,
Lines::Item: Into<Line<'line>>,
{
#[allow(deprecated)] // For start_corner
let list = List::new(["Item 0", "Item 1", "Item 2"]).start_corner(corner);
let buffer = render_widget(list, 10, 4);
assert_eq!(buffer, Buffer::with_lines(expected));
}
#[test]
fn test_list_truncate_items() {
let list = List::new(["Item 0", "Item 1", "Item 2", "Item 3", "Item 4"]);

View File

@@ -3,10 +3,7 @@ use unicode_width::UnicodeWidthStr;
use crate::{
prelude::*,
text::StyledGrapheme,
widgets::{
reflow::{LineComposer, LineTruncator, WordWrapper, WrappedLine},
Block,
},
widgets::{reflow::*, Block},
};
const fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Alignment) -> u16 {

View File

@@ -336,7 +336,6 @@ fn trim_offset(src: &str, mut offset: usize) -> &str {
break;
}
}
#[allow(clippy::string_slice)] // Is safe as it comes from UnicodeSegmentation
&src[start..]
}
@@ -432,17 +431,17 @@ mod test {
let (line_truncator, _, _) = run_composer(Composer::LineTruncator, text, width as u16);
let wrapped = vec![
text.get(..width).unwrap(),
text.get(width..width * 2).unwrap(),
text.get(width * 2..width * 3).unwrap(),
text.get(width * 3..).unwrap(),
&text[..width],
&text[width..width * 2],
&text[width * 2..width * 3],
&text[width * 3..],
];
assert_eq!(
word_wrapper, wrapped,
"WordWrapper should detect the line cannot be broken on word boundary and \
break it at line width limit."
);
assert_eq!(line_truncator, [text.get(..width).unwrap()]);
assert_eq!(line_truncator, vec![&text[..width]]);
}
#[test]
@@ -472,7 +471,7 @@ mod test {
assert_eq!(word_wrapper_single_space, word_wrapped);
assert_eq!(word_wrapper_multi_space, word_wrapped);
assert_eq!(line_truncator, [text.get(..width).unwrap()]);
assert_eq!(line_truncator, vec![&text[..width]]);
}
#[test]

View File

@@ -3,7 +3,8 @@
clippy::cast_possible_truncation,
clippy::cast_precision_loss,
clippy::cast_sign_loss,
clippy::module_name_repetitions
clippy::module_name_repetitions,
clippy::wildcard_imports
)]
use std::iter;
@@ -11,10 +12,7 @@ use std::iter;
use strum::{Display, EnumString};
use unicode_width::UnicodeWidthStr;
use crate::{
prelude::*,
symbols::scrollbar::{Set, DOUBLE_HORIZONTAL, DOUBLE_VERTICAL},
};
use crate::{prelude::*, symbols::scrollbar::*};
/// A widget to display a scrollbar
///
@@ -65,7 +63,7 @@ use crate::{
/// // and the scrollbar, those are separate widgets
/// frame.render_stateful_widget(
/// scrollbar,
/// area.inner(Margin {
/// area.inner(&Margin {
/// // using an inner vertical margin of 1 unit makes the scrollbar inside the block
/// vertical: 1,
/// horizontal: 0,
@@ -134,7 +132,7 @@ pub enum ScrollbarOrientation {
///
/// If you don't have multi-line content, you can leave the `viewport_content_length` set to the
/// default and it'll use the track size as a `viewport_content_length`.
#[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 ScrollbarState {
/// The total length of the scrollable content.
@@ -393,6 +391,12 @@ impl<'a> Scrollbar<'a> {
}
}
impl Default for ScrollbarState {
fn default() -> Self {
Self::new(0)
}
}
impl ScrollbarState {
/// Constructs a new [`ScrollbarState`] with the specified content length.
///

View File

@@ -30,7 +30,7 @@ use crate::{prelude::*, widgets::Block};
/// .direction(RenderDirection::RightToLeft)
/// .style(Style::default().red().on_white());
/// ```
#[derive(Debug, Default, Clone, Eq, PartialEq)]
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Sparkline<'a> {
/// A block to wrap the widget in
block: Option<Block<'a>>,
@@ -59,6 +59,19 @@ pub enum RenderDirection {
RightToLeft,
}
impl<'a> Default for Sparkline<'a> {
fn default() -> Self {
Self {
block: None,
style: Style::default(),
data: &[],
max: None,
bar_set: symbols::bar::NINE_LEVELS,
direction: RenderDirection::LeftToRight,
}
}
}
impl<'a> Sparkline<'a> {
/// Wraps the sparkline with the given `block`.
#[must_use = "method moves the value of self and returns the modified value"]
@@ -159,9 +172,10 @@ impl Sparkline<'_> {
return;
}
let max = self
.max
.unwrap_or_else(|| *self.data.iter().max().unwrap_or(&1));
let max = match self.max {
Some(v) => v,
None => *self.data.iter().max().unwrap_or(&1),
};
let max_index = min(spark_area.width as usize, self.data.len());
let mut data = self
.data
@@ -239,7 +253,9 @@ mod tests {
// filled with x symbols to make it easier to assert on the result
fn render(widget: Sparkline, width: u16) -> Buffer {
let area = Rect::new(0, 0, width, 1);
let mut buffer = Buffer::filled(area, Cell::new("x"));
let mut cell = Cell::default();
cell.set_symbol("x");
let mut buffer = Buffer::filled(area, &cell);
widget.render(area, &mut buffer);
buffer
}

View File

@@ -1,4 +1,4 @@
use super::Cell;
use super::*;
use crate::prelude::*;
/// A single row of data to be displayed in a [`Table`] widget.

View File

@@ -1,8 +1,6 @@
use itertools::Itertools;
#[allow(unused_imports)] // `Cell` is used in the doc comment but not the code
use super::Cell;
use super::{HighlightSpacing, Row, TableState};
use super::*;
use crate::{layout::Flex, prelude::*, widgets::Block};
/// A widget to display data in formatted columns.
@@ -178,11 +176,7 @@ use crate::{layout::Flex, prelude::*, widgets::Block};
/// Row::new(vec!["Row21", "Row22", "Row23"]),
/// Row::new(vec!["Row31", "Row32", "Row33"]),
/// ];
/// let widths = [
/// Constraint::Length(5),
/// Constraint::Length(5),
/// Constraint::Length(10),
/// ];
/// let widths = [Constraint::Length(5), Constraint::Length(5), Constraint::Length(10)];
/// let table = Table::new(rows, widths)
/// .block(Block::new().title("Table"))
/// .highlight_style(Style::new().add_modifier(Modifier::REVERSED))
@@ -190,7 +184,6 @@ use crate::{layout::Flex, prelude::*, widgets::Block};
///
/// frame.render_stateful_widget(table, area, &mut table_state);
/// # }
/// ```
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Table<'a> {
/// Data to display in each row
@@ -220,6 +213,15 @@ pub struct Table<'a> {
/// Symbol in front of the selected row
highlight_symbol: Text<'a>,
/// Symbol in front of the marked row
mark_symbol: Text<'a>,
/// Symbol in front of the unmarked row
unmark_symbol: Text<'a>,
/// Symbol in front of the marked and selected row
mark_highlight_symbol: Text<'a>,
/// Decides when to allocate spacing for the row selection
highlight_spacing: HighlightSpacing,
@@ -239,6 +241,9 @@ impl<'a> Default for Table<'a> {
style: Style::new(),
highlight_style: Style::new(),
highlight_symbol: Text::default(),
mark_symbol: Text::default(),
unmark_symbol: Text::default(),
mark_highlight_symbol: Text::default(),
highlight_spacing: HighlightSpacing::default(),
flex: Flex::Start,
}
@@ -510,6 +515,60 @@ impl<'a> Table<'a> {
self
}
/// Set the symbol to be displayed in front of the marked row
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # let rows = [Row::new(vec!["Cell1", "Cell2"])];
/// # let widths = [Constraint::Length(5), Constraint::Length(5)];
/// let table = Table::new(rows, widths).mark_symbol("\u{2714}");
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn mark_symbol<T: Into<Text<'a>>>(mut self, mark_symbol: T) -> Self {
self.mark_symbol = mark_symbol.into();
self
}
/// Set the symbol to be displayed in front of the unmarked row
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # let rows = [Row::new(vec!["Cell1", "Cell2"])];
/// # let widths = [Constraint::Length(5), Constraint::Length(5)];
/// let table = Table::new(rows, widths).unmark_symbol(" ");
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn unmark_symbol<T: Into<Text<'a>>>(mut self, unmark_symbol: T) -> Self {
self.unmark_symbol = unmark_symbol.into();
self
}
/// Set the symbol to be displayed in front of the marked and selected row
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # let rows = [Row::new(vec!["Cell1", "Cell2"])];
/// # let widths = [Constraint::Length(5), Constraint::Length(5)];
/// let table = Table::new(rows, widths).mark_highlight_symbol("\u{29bf}");
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn mark_highlight_symbol<T: Into<Text<'a>>>(mut self, mark_highlight_symbol: T) -> Self {
self.mark_highlight_symbol = mark_highlight_symbol.into();
self
}
/// Set when to show the highlight spacing
///
/// The highlight spacing is the spacing that is allocated for the selection symbol column (if
@@ -611,8 +670,8 @@ impl StatefulWidgetRef for Table<'_> {
return;
}
let selection_width = self.selection_width(state);
let columns_widths = self.get_columns_widths(table_area.width, selection_width);
let highlight_column_width = self.highlight_column_width(state);
let columns_widths = self.get_columns_widths(table_area.width, highlight_column_width);
let (header_area, rows_area, footer_area) = self.layout(table_area);
self.render_header(header_area, buf, &columns_widths);
@@ -621,8 +680,13 @@ impl StatefulWidgetRef for Table<'_> {
rows_area,
buf,
state,
selection_width,
&self.highlight_symbol,
highlight_column_width,
(
&self.highlight_symbol,
&self.mark_symbol,
&self.unmark_symbol,
&self.mark_highlight_symbol,
),
&columns_widths,
);
@@ -677,14 +741,16 @@ impl Table<'_> {
area: Rect,
buf: &mut Buffer,
state: &mut TableState,
selection_width: u16,
highlight_symbol: &Text<'_>,
highlight_column_width: u16,
symbols: (&Text<'_>, &Text<'_>, &Text<'_>, &Text<'_>),
columns_widths: &[(u16, u16)],
) {
if self.rows.is_empty() {
return;
}
let (highlight_symbol, mark_symbol, unmark_symbol, mark_highlight_symbol) = symbols;
let (start_index, end_index) =
self.get_row_bounds(state.selected, state.offset, area.height);
state.offset = start_index;
@@ -705,22 +771,37 @@ impl Table<'_> {
);
buf.set_style(row_area, row.style);
let is_selected = state.selected().is_some_and(|index| index == i);
if selection_width > 0 && is_selected {
let selection_area = Rect {
width: selection_width,
let is_marked = state.marked().contains(&(i + state.offset));
let is_highlighted = state.selected().is_some_and(|index| index == i);
if highlight_column_width > 0 {
let area = Rect {
width: highlight_column_width,
..row_area
};
buf.set_style(selection_area, row.style);
highlight_symbol.clone().render(selection_area, buf);
};
buf.set_style(area, row.style);
match (is_marked, is_highlighted) {
(true, true) => {
mark_highlight_symbol.render(area, buf);
}
(true, false) => {
mark_symbol.render(area, buf);
}
(false, true) => {
highlight_symbol.render(area, buf);
}
(false, false) => {
unmark_symbol.render(area, buf);
}
};
}
for ((x, width), cell) in columns_widths.iter().zip(row.cells.iter()) {
cell.render(
Rect::new(row_area.x + x, row_area.y, *width, row_area.height),
buf,
);
}
if is_selected {
if is_highlighted {
buf.set_style(row_area, self.highlight_style);
}
y_offset += row.height_with_margin();
@@ -795,15 +876,35 @@ impl Table<'_> {
(start, end)
}
/// Returns the width of the selection column if a row is selected, or the `highlight_spacing`
/// is set to show the column always, otherwise 0.
fn selection_width(&self, state: &TableState) -> u16 {
let has_selection = state.selected().is_some();
if self.highlight_spacing.should_add(has_selection) {
/// Returns the width of the indicator column if a row is selected, rows are marked,
/// or the `highlight_spacing` is set to show the column always, otherwise 0.
fn highlight_column_width(&self, state: &TableState) -> u16 {
let has_highlight = state.selected().is_some() || state.marked().len() > 0;
let highlight_column_width = if self.highlight_spacing.should_add(has_highlight) {
self.highlight_symbol.width() as u16
} else {
0
}
};
let mark_column_width = if self.highlight_spacing.should_add(has_highlight) {
self.mark_symbol.width() as u16
} else {
0
};
let mark_highlight_column_width = if self.highlight_spacing.should_add(has_highlight) {
self.mark_highlight_symbol.width() as u16
} else {
0
};
let unmark_column_width = if self.highlight_spacing.should_add(has_highlight) {
self.unmark_symbol.width() as u16
} else {
0
};
highlight_column_width
.max(mark_column_width)
.max(mark_highlight_column_width)
.max(unmark_column_width)
}
}
@@ -849,7 +950,7 @@ mod tests {
use std::vec;
use super::*;
use crate::{layout::Constraint::*, style::Style, text::Line, widgets::Cell};
use crate::{layout::Constraint::*, style::Style, text::Line};
#[test]
fn new() {
@@ -1197,6 +1298,100 @@ mod tests {
]);
assert_eq!(buf, expected);
}
#[test]
fn render_with_selected_marked_unmarked() {
let rows = vec![
Row::new(vec!["Cell", "Cell"]),
Row::new(vec!["Cell", "Cell"]),
Row::new(vec!["Cell", "Cell"]),
Row::new(vec!["Cell", "Cell"]),
Row::new(vec!["Cell", "Cell"]),
Row::new(vec!["Cell", "Cell"]),
Row::new(vec!["Cell", "Cell"]),
];
let table = Table::new(rows, [Constraint::Length(5); 2])
.highlight_symbol("")
.mark_symbol("")
.unmark_symbol(" ")
.mark_highlight_symbol("⦿");
let mut state = TableState::new().with_selected(0);
state.mark(1);
state.mark(3);
state.mark(5);
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 10));
StatefulWidget::render(table.clone(), Rect::new(0, 0, 15, 10), &mut buf, &mut state);
let expected = Buffer::with_lines(Text::from(vec![
"• Cell Cell ".into(),
"⦾ Cell Cell ".into(),
" Cell Cell ".into(),
"⦾ Cell Cell ".into(),
" Cell Cell ".into(),
"⦾ Cell Cell ".into(),
" Cell Cell ".into(),
" ".into(),
" ".into(),
" ".into(),
]));
assert_eq!(buf, expected);
state.mark(0);
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 10));
StatefulWidget::render(table.clone(), Rect::new(0, 0, 15, 10), &mut buf, &mut state);
let expected = Buffer::with_lines(Text::from(vec![
"⦿ Cell Cell ".into(),
"⦾ Cell Cell ".into(),
" Cell Cell ".into(),
"⦾ Cell Cell ".into(),
" Cell Cell ".into(),
"⦾ Cell Cell ".into(),
" Cell Cell ".into(),
" ".into(),
" ".into(),
" ".into(),
]));
assert_eq!(buf, expected);
state.select(Some(1));
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 10));
StatefulWidget::render(table.clone(), Rect::new(0, 0, 15, 10), &mut buf, &mut state);
let expected = Buffer::with_lines(Text::from(vec![
"⦾ Cell Cell ".into(),
"⦿ Cell Cell ".into(),
" Cell Cell ".into(),
"⦾ Cell Cell ".into(),
" Cell Cell ".into(),
"⦾ Cell Cell ".into(),
" Cell Cell ".into(),
" ".into(),
" ".into(),
" ".into(),
]));
assert_eq!(buf, expected);
state.unmark(0);
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 10));
StatefulWidget::render(table.clone(), Rect::new(0, 0, 15, 10), &mut buf, &mut state);
let expected = Buffer::with_lines(Text::from(vec![
" Cell Cell ".into(),
"⦿ Cell Cell ".into(),
" Cell Cell ".into(),
"⦾ Cell Cell ".into(),
" Cell Cell ".into(),
"⦾ Cell Cell ".into(),
" Cell Cell ".into(),
" ".into(),
" ".into(),
" ".into(),
]));
assert_eq!(buf, expected);
}
}
// test how constraints interact with table column width allocation
@@ -1395,6 +1590,214 @@ mod tests {
assert_eq!(table.get_columns_widths(10, 0), [(0, 5), (5, 5)]);
}
#[track_caller]
fn test_table_with_selection_and_marks<'line, Lines, Marks>(
highlight_spacing: HighlightSpacing,
columns: u16,
spacing: u16,
selection: Option<usize>,
marks: Marks,
expected: Lines,
) where
Lines: IntoIterator,
Lines::Item: Into<Line<'line>>,
Marks: IntoIterator<Item = usize>,
{
let table = Table::default()
.rows(vec![Row::new(vec!["ABCDE", "12345"])])
.highlight_spacing(highlight_spacing)
.highlight_symbol(">>>")
.mark_symbol(" MMM ")
.mark_highlight_symbol(" >M> ")
.column_spacing(spacing);
let area = Rect::new(0, 0, columns, 3);
let mut buf = Buffer::empty(area);
let mut state = TableState::default().with_selected(selection);
for mark in marks {
state.mark(mark);
}
StatefulWidget::render(table, area, &mut buf, &mut state);
assert_eq!(buf, Buffer::with_lines(expected));
}
#[test]
#[allow(clippy::too_many_lines)]
fn highlight_symbol_mark_symbol_and_column_spacing_with_highlight_spacing() {
// no highlight_symbol or mark_symbol rendered ever
test_table_with_selection_and_marks(
HighlightSpacing::Never,
15, // width
0, // spacing
None, // selection
[], // marks
[
"ABCDE 12345 ", /* default layout is Flex::Start but columns length
* constraints are calculated as `max_area / n_columns`,
* i.e. they are distributed amongst available space */
" ", // row 2
" ", // row 3
],
);
// no highlight_symbol or mark_symbol rendered ever
test_table_with_selection_and_marks(
HighlightSpacing::Never,
15, // width
0, // spacing
None, // selection
[0], // marks
[
"ABCDE 12345 ", /* default layout is Flex::Start but columns length
* constraints are calculated as `max_area / n_columns`,
* i.e. they are distributed amongst available space */
" ", // row 2
" ", // row 3
],
);
// no highlight_symbol or mark_symbol rendered ever
test_table_with_selection_and_marks(
HighlightSpacing::Never,
15, // width
0, // spacing
None, // selection
[], // marks
[
"ABCDE 12345 ", /* default layout is Flex::Start but columns length
* constraints are calculated as `max_area / n_columns`,
* i.e. they are distributed amongst available space */
" ", // row 2
" ", // row 3
],
);
// no highlight_symbol or mark_symbol rendered
test_table_with_selection_and_marks(
HighlightSpacing::WhenSelected,
15, // width
0, // spacing
None, // selection
[], // marks
[
"ABCDE 12345 ", /* default layout is Flex::Start but columns length
* constraints are calculated as `max_area / n_columns`,
* i.e. they are distributed amongst available space */
" ", // row 2
" ", // row 3
],
);
// mark_symbol rendered
test_table_with_selection_and_marks(
HighlightSpacing::WhenSelected,
15, // width
0, // spacing
None, // selection
[0], // marks
[
" MMM ABCDE12345", /* default layout is Flex::Start but columns length
* constraints are calculated as `max_area / n_columns`,
* i.e. they are distributed amongst available space */
" ", // row 2
" ", // row 3
],
);
// highlight symbol rendered with mark symbol width
test_table_with_selection_and_marks(
HighlightSpacing::WhenSelected,
15, // width
0, // spacing
Some(0), // selection
[], // marks
[
">>> ABCDE12345", /* default layout is Flex::Start but columns length
* constraints are calculated as `max_area / n_columns`,
* i.e. they are distributed amongst available space */
" ", // row 2
" ", // row 3
],
);
// mark highlight symbol rendered
test_table_with_selection_and_marks(
HighlightSpacing::WhenSelected,
15, // width
0, // spacing
Some(0), // selection
[0], // marks
[
" >M> ABCDE12345", /* default layout is Flex::Start but columns length
* constraints are calculated as `max_area / n_columns`,
* i.e. they are distributed amongst available space */
" ", // row 2
" ", // row 3
],
);
// no highlight_symbol or mark_symbol rendered
test_table_with_selection_and_marks(
HighlightSpacing::Always,
15, // width
0, // spacing
None, // selection
[], // marks
[
" ABCDE12345", /* default layout is Flex::Start but columns length
* constraints are calculated as `max_area / n_columns`,
* i.e. they are distributed amongst available space */
" ", // row 2
" ", // row 3
],
);
// mark_symbol rendered
test_table_with_selection_and_marks(
HighlightSpacing::Always,
15, // width
0, // spacing
None, // selection
[0], // marks
[
" MMM ABCDE12345", /* default layout is Flex::Start but columns length
* constraints are calculated as `max_area / n_columns`,
* i.e. they are distributed amongst available space */
" ", // row 2
" ", // row 3
],
);
// highlight symbol rendered with mark symbol width
test_table_with_selection_and_marks(
HighlightSpacing::Always,
15, // width
0, // spacing
Some(0), // selection
[], // marks
[
">>> ABCDE12345", /* default layout is Flex::Start but columns length
* constraints are calculated as `max_area / n_columns`,
* i.e. they are distributed amongst available space */
" ", // row 2
" ", // row 3
],
);
// mark highlight symbol rendered
test_table_with_selection_and_marks(
HighlightSpacing::Always,
15, // width
0, // spacing
Some(0), // selection
[0], // marks
[
" >M> ABCDE12345", /* default layout is Flex::Start but columns length
* constraints are calculated as `max_area / n_columns`,
* i.e. they are distributed amongst available space */
" ", // row 2
" ", // row 3
],
);
}
#[track_caller]
fn test_table_with_selection<'line, Lines>(
highlight_spacing: HighlightSpacing,

View File

@@ -49,6 +49,7 @@
pub struct TableState {
pub(crate) offset: usize,
pub(crate) selected: Option<usize>,
pub(crate) marked: Vec<usize>,
}
impl TableState {
@@ -64,6 +65,7 @@ impl TableState {
Self {
offset: 0,
selected: None,
marked: vec![],
}
}
@@ -175,6 +177,78 @@ impl TableState {
self.offset = 0;
}
}
/// Sets the index of the row as marked
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let mut state = TableState::default();
/// state.mark(1);
/// ```
pub fn mark(&mut self, index: usize) {
if !self.marked.contains(&index) {
self.marked.push(index);
}
}
/// Sets the index of the row as unmarked
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let mut state = TableState::default();
/// state.unmark(1);
/// ```
pub fn unmark(&mut self, index: usize) {
self.marked.retain(|i| *i != index);
}
/// Toggles the index of the row as marked or unmarked
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let mut state = TableState::default();
/// state.toggle_mark(1);
/// ```
pub fn toggle_mark(&mut self, index: usize) {
if self.marked.contains(&index) {
self.unmark(index);
} else {
self.mark(index);
}
}
/// Returns a iterator of all marked rows
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # use itertools::Itertools;
/// let mut state = TableState::default();
/// state.marked().contains(&1);
/// ```
pub fn marked(&self) -> std::slice::Iter<'_, usize> {
self.marked.iter()
}
/// Clears all marks from all rows
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let mut state = TableState::default();
/// state.clear_marks();
/// ```
pub fn clear_marks(&mut self) {
self.marked.drain(..);
}
}
#[cfg(test)]

View File

@@ -30,7 +30,7 @@ fn backend_termion_should_only_write_diffs() -> Result<(), Box<dyn std::error::E
}
let expected = {
use ratatui::termion::{color, cursor, style};
use termion::{color, cursor, style};
let mut s = String::new();
// First draw
write!(s, "{}", cursor::Goto(1, 1))?;

View File

@@ -14,6 +14,7 @@
// not too happy about the redundancy in these tests,
// but if that helps readability then it's ok i guess /shrug
use pretty_assertions::assert_eq;
use ratatui::{backend::TestBackend, prelude::*, widgets::*};
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
@@ -98,7 +99,8 @@ const DEFAULT_STATE_REPR: &str = r#"{
},
"table": {
"offset": 0,
"selected": null
"selected": null,
"marked": []
},
"scrollbar": {
"content_length": 10,
@@ -135,7 +137,8 @@ const SELECTED_STATE_REPR: &str = r#"{
},
"table": {
"offset": 0,
"selected": 1
"selected": 1,
"marked": []
},
"scrollbar": {
"content_length": 10,
@@ -174,7 +177,8 @@ const SCROLLED_STATE_REPR: &str = r#"{
},
"table": {
"offset": 4,
"selected": 8
"selected": 8,
"marked": []
},
"scrollbar": {
"content_length": 10,

View File

@@ -180,8 +180,7 @@ fn widgets_line_gauge_renders() {
terminal
.draw(|f| {
let gauge = LineGauge::default()
.filled_style(Style::default().fg(Color::Green))
.unfilled_style(Style::default().fg(Color::White))
.gauge_style(Style::default().fg(Color::Green).bg(Color::White))
.ratio(0.43);
f.render_widget(
gauge,
@@ -194,7 +193,7 @@ fn widgets_line_gauge_renders() {
);
let gauge = LineGauge::default()
.block(Block::bordered().title("Gauge 2"))
.filled_style(Style::default().fg(Color::Green))
.gauge_style(Style::default().fg(Color::Green))
.line_set(symbols::line::THICK)
.ratio(0.211_313_934_313_1);
f.render_widget(