Compare commits

..

23 Commits

Author SHA1 Message Date
Hichem
2fd85af33c refactor(barchart): simplify internal implementation (#544)
Replace `remove_invisible_groups_and_bars` with `group_ticks`
`group_ticks` calculates the visible bar length in ticks. (A cell contains 8 ticks).

It is used for 2 purposes:
1. to get the bar length in ticks for rendering
2. since it delivers only the values of the visible bars, If we zip these values
   with the groups and bars, then we will filter out the invisible groups and bars

Signed-off-by: Ben Fekih, Hichem <hichem.f@live.de>
2023-09-29 15:04:20 -07:00
Dheepak Krishnamurthy
401a7a7f71 docs: Improve clarity in documentation for Frame and Terminal 📚 (#545) 2023-09-28 20:18:54 -07:00
Dheepak Krishnamurthy
e35e4135c9 docs: Fix terminal comment (#547) 2023-09-28 18:44:52 -07:00
Dheepak Krishnamurthy
8ae4403b63 docs: Fix Terminal docstring (#546) 2023-09-28 18:42:31 -07:00
Josh McKinney
11076d0af3 fix(rect): fix arithmetic overflow edge cases (#543)
Fixes https://github.com/ratatui-org/ratatui/issues/258
2023-09-28 03:19:33 -07:00
Josh McKinney
9cfb133a98 docs: document alpha release process (#542)
Fixes https://github.com/ratatui-org/ratatui/issues/412
2023-09-28 11:27:03 +02:00
Josh McKinney
4548a9b7e2 docs: add BREAKING-CHANGES.md (#538)
Document the breaking changes in each version. This document is
manually curated by summarizing the breaking changes in the changelog.
2023-09-28 01:00:43 -07:00
Josh McKinney
61af0d9906 docs(examples): make custom widget example into a button (#539)
The widget also now supports mouse
2023-09-27 20:07:45 -07:00
Josh McKinney
c0991cc576 docs: make library and README consistent (#526)
* docs: make library and README consistent

Generate the bulk of the README from the library documentation, so that
they are consistent using cargo-rdme.

- Removed the Contributors section, as it is redundant with the github
  contributors list.
- Removed the info about the other backends and replaced it with a
  pointer to the documentation.
- add docsrs example, vhs tape and images that will end up in the README

Fixes: https://github.com/ratatui-org/ratatui/issues/512
2023-09-27 19:57:04 -07:00
Hichem
301366c4fa feat(barchart): render charts smaller than 3 lines (#532)
The bar values are not shown if the value width is equal the bar width
and the bar is height is less than one line

Add an internal structure `LabelInfo` which stores the reserved height
for the labels (0, 1 or 2) and also whether the labels will be shown.

Fixes ratatui-org#513

Signed-off-by: Ben Fekih, Hichem <hichem.f@live.de>
2023-09-26 13:54:04 -07:00
Valentin271
3bda372847 docs(tabs): add documentation to Tabs (#535) 2023-09-25 23:22:14 -07:00
Josh McKinney
082cbcbc50 feat(frame)!: Remove generic Backend parameter (#530)
This change simplifys UI code that uses the Frame type. E.g.:

```rust
fn draw<B: Backend>(frame: &mut Frame<B>) {
    // ...
}
```

Frame was generic over Backend because it stored a reference to the
terminal in the field. Instead it now directly stores the viewport area
and current buffer. These are provided at creation time and are valid
for the duration of the frame.

BREAKING CHANGE: Frame is no longer generic over Backend. Code that
accepted `Frame<Backend>` will now need to accept `Frame`. To migrate
existing code, remove any generic parameters from code that uses an
instance of a Frame. E.g. the above code becomes:

```rust
fn draw(frame: &mut Frame) {
    // ...
}
```
2023-09-25 22:30:36 -07:00
Josh McKinney
cbf86da0e7 feat(rect): add is_empty() to simplify some common checks (#534)
- add `Rect::is_empty()` that checks whether either height or width == 0
- refactored `Rect` into layout/rect.rs from layout.rs. No public API change as
   the module is private and the type is re-exported under the `layout` module.
2023-09-25 21:45:29 -07:00
Josh McKinney
32e461953c feat(block)!: allow custom symbols for borders (#529)
Adds a new `Block::border_set` method that allows the user to specify
the symbols used for the border.

Added two new border types: `BorderType::QuadrantOutside` and
`BorderType::QuadrantInside`. These are used to draw borders using the
unicode quadrant characters (which look like half block "pixels").

QuadrantOutside:
```
▛▀▀▜
▌  ▐
▙▄▄▟
```

QuadrantInside:
```
▗▄▄▖
▐  ▌
▝▀▀▘
```
Fixes: https://github.com/ratatui-org/ratatui/issues/528

BREAKING CHANGES:
- BorderType::to_line_set is renamed to to_border_set
- BorderType::line_symbols is renamed to border_symbols
2023-09-23 22:08:32 -07:00
Orhun Parmaksız
d67fa2c00d feat(line): add Line::raw constructor (#511)
* feat(line): add `Line::raw` constructor

There is already `Span::raw` and `Text::raw` methods
and this commit simply adds `Line::raw` method for symmetry.

Multi-line content is converted to multiple spans with the new line removed
2023-09-22 04:34:58 -07:00
Valentin271
c3155a2489 fix(barchart): add horizontal labels(#518)
Labels were missed in the initial implementation of the horizontal
mode for the BarChart widget. This adds them.

Fixes https://github.com/ratatui-org/ratatui/issues/499
2023-09-22 00:38:58 -07:00
Josh McKinney
c5ea656385 fix(barchart): avoid divide by zero in rendering (#525)
Fixes: https://github.com/ratatui-org/ratatui/issues/521
2023-09-21 16:35:28 -07:00
Josh McKinney
be55a5fbcd feat(examples): add demo2 example (#500) 2023-09-21 01:47:23 -07:00
BlakStar
21303f2167 fix(rect): prevent overflow in inner() and area() (#523) 2023-09-20 15:56:32 -07:00
Hichem
c9b8e7cf41 fix(barchart): render value labels with unicode correctly (#515)
An earlier change introduced a bug where the width of value labels with
unicode characters was incorrectly using the string length in bytes
instead of the unicode character count. This reverts the earlier change.

Signed-off-by: Ben Fekih, Hichem <hichem.f@live.de>
2023-09-19 18:06:32 -07:00
Josh McKinney
5498a889ae chore(spans): remove deprecated Spans type (#426)
The `Spans` type (plural, not singular) was replaced with a more ergonomic `Line` type
in Ratatui v0.21.0 and marked deprecated byt left for backwards compatibility. This is now
removed.

- `Line` replaces `Spans`
- `Buffer::set_line` replaces `Buffer::set_spans`
2023-09-19 02:58:52 -07:00
Valentin271
0fe738500c docs(Gauge): add docs for Gauge and LineGauge (#514) 2023-09-18 15:21:59 -07:00
Aizon
dd9a8df03a docs(table): add documentation for block and header methods of the Table widget (#505) 2023-09-17 14:19:21 -07:00
68 changed files with 4059 additions and 1382 deletions

View File

@@ -1,10 +1,13 @@
# configuration for https://github.com/DavidAnson/markdownlint
first-line-heading: false
no-inline-html:
allowed_elements:
- img
- details
- summary
- div
- br
line-length:
line_length: 100

200
BREAKING-CHANGES.md Normal file
View File

@@ -0,0 +1,200 @@
# Breaking Changes
This document contains a list of breaking changes in each version and some notes to help migrate
between versions. It is compile manually from the commit history and changelog. We also tag PRs on
github with a [breaking change] label.
[breaking change]: (https://github.com/ratatui-org/ratatui/issues?q=label%3A%22breaking+change%22)
## Unreleased (0.24.0)
### ScrollbarState field type changed from `u16` to `usize` ([#456])
[#456]: https://github.com/ratatui-org/ratatui/pull/456
In order to support larger content lengths, the `position`, `content_length` and
`viewport_content_length` methods on `ScrollbarState` now take `usize` instead of `u16`
### `BorderType::line_symbols` renamed to `border_symbols` ([#529])
[#529]: https://github.com/ratatui-org/ratatui/issues/529
Applications can now set custom borders on a `Block` by calling `border_set()`. The
`BorderType::line_symbols()` is renamed to `border_symbols()` and now returns a new struct
`symbols::border::Set`. E.g.:
```rust
let line_set: symbols::line::Set = BorderType::line_symbols(BorderType::Plain);
// becomes
let border_set: symbols::border::Set = BorderType::border_symbols(BorderType::Plain);
```
### Generic `Backend` parameter removed from `Frame` ([#530])
[#530]: https://github.com/ratatui-org/ratatui/issues/530
`Frame` is no longer generic over Backend. Code that accepted `Frame<Backend>` will now need to
accept `Frame`. To migrate existing code, remove any generic parameters from code that uses an
instance of a Frame. E.g.:
```rust
fn ui<B: Backend>(frame: &mut Frame<B>) { ... }
// becomes
fn ui(frame: Frame) { ... }
```
### `Stylize` shorthands now consume rather than borrow `String` ([#466])
[#466]: https://github.com/ratatui-org/ratatui/issues/466
In order to support using `Stylize` shorthands (e.g. `"foo".red()`) on temporary `String` values, a
new implementation of `Stylize` was added that returns a `Span<'static>`. This causes the value to
be consumed rather than borrowed. Existing code that expects to use the string after a call will no
longer compile. E.g.
```rust
let s = String::new("foo");
let span1 = s.red();
let span2 = s.blue(); // will no longer compile as s is consumed by the previous line
// becomes
let span1 = s.clone().red();
let span2 = s.blue();
```
### Deprecated `Spans` type removed (replaced with `Line`) ([#426])
[#426]: https://github.com/ratatui-org/ratatui/issues/426
`Spans` was replaced with `Line` in 0.21.0. `Buffer::set_spans` was replaced with
`Buffer::set_line`.
```rust
let spans = Spans::from(some_string_str_span_or_vec_span);
buffer.set_spans(0, 0, spans, 10);
// becomes
let line - Line::from(some_string_str_span_or_vec_span);
buffer.set_line(0, 0, line, 10);
```
## [v0.23.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.23.0)
### `Scrollbar::track_symbol()` now takes an `Option<&str>` instead of `&str` ([#360])
[#360]: https://github.com/ratatui-org/ratatui/issues/360
The track symbol of `Scrollbar` is now optional, this method now takes an optional value.
```rust
let scrollbar = Scrollbar::default().track_symbol("|");
// becomes
let scrollbar = Scrollbar::default().track_symbol(Some("|"));
```
### `Scrollbar` symbols moved to `symbols::scrollbar` and `widgets::scrollbar` module is private ([#330])
[#330]: https://github.com/ratatui-org/ratatui/issues/330
The symbols for defining scrollbars have been moved to the `symbols` module from the
`widgets::scrollbar` module which is no longer public. To update your code update any imports to the
new module locations. E.g.:
```rust
use ratatui::{widgets::scrollbar::{Scrollbar, Set}};
// becomes
use ratatui::{widgets::Scrollbar, symbols::scrollbar::Set}
```
### MSRV updated to 1.67 ([#361])
[#361]: https://github.com/ratatui-org/ratatui/issues/361
The MSRV of ratatui is now 1.67 due to an MSRV update in a dependency (`time`).
## [v0.22.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.22.0)
### bitflags updated to 2.3 ([#205])
[#205]: https://github.com/ratatui-org/ratatui/issues/205
The serde representation of bitflags has changed. Any existing serialized types that have Borders or
Modifiers will need to be re-serialized. This is documented in the [bitflags
changelog](https://github.com/bitflags/bitflags/blob/main/CHANGELOG.md#200-rc2)..
## [v0.21.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.21.0)
### MSRV is 1.65.0 ([#171])
[#171]: https://github.com/ratatui-org/ratatui/issues/171
The minimum supported rust version is now 1.65.0.
### `Terminal::with_options()` stabilized to allow configuring the viewport ([#114])
[#114]: https://github.com/ratatui-org/ratatui/issues/114
In order to support inline viewports, the unstable method `Terminal::with_options()` was stabilized
and `ViewPort` was changed from a struct to an enum.
```rust
let terminal = Terminal::with_options(backend, TerminalOptions {
viewport: Viewport::fixed(area),
});
// becomes
let terminal = Terminal::with_options(backend, TerminalOptions {
viewport: Viewport::Fixed(area),
});
```
### Code that binds `Into<Text<'a>>` now requires type annotations ([#168])
[#168]: https://github.com/ratatui-org/ratatui/issues/168
A new type `Masked` was introduced that implements `From<Text<'a>>`. This causes any code that did
previously did not need to use type annotations to fail to compile. To fix this, annotate or call
to_string() / to_owned() / as_str() on the value. E.g.:
```rust
let paragraph = Paragraph::new("".as_ref());
// becomes
let paragraph = Paragraph::new("".as_str());
```
### `Marker::Block` now renders as a block rather than a bar character ([#133])
[#133]: https://github.com/ratatui-org/ratatui/issues/133
Code using the `Block` marker that previously rendered using a half block character (`'▀'``) now
renders using the full block character (`'█'`). A new marker variant`Bar` is introduced to replace
the existing code.
```rust
let canvas = Canvas::default().marker(Marker::Block);
// becomes
let canvas = Canvas::default().marker(Marker::Bar);
```
## [v0.20.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.20.0)
v0.20.0 was the first release of Ratatui - versions prior to this were release as tui-rs. See the
[Changelog](./CHANGELOG.md) for more details.
### MSRV is update to 1.63.0 ([#80])
[#80]: https://github.com/ratatui-org/ratatui/issues/80
The minimum supported rust version is 1.63.0
### List no longer ignores empty string in items ([#42])
[#42]: https://github.com/ratatui-org/ratatui/issues/42
The following code now renders 3 items instead of 2. Code which relies on the previous behavior will
need to manually filter empty items prior to display.
```rust
let items = vec![
ListItem::new("line one"),
ListItem::new(""),
ListItem::new("line four"),
];
```

View File

@@ -48,14 +48,15 @@ lru = "0.11.1"
[dev-dependencies]
anyhow = "1.0.71"
argh = "0.1"
argh = "0.1.12"
better-panic = "0.3.0"
cargo-husky = { version = "1.5.0", default-features = false, features = [
"user-hooks",
] }
criterion = { version = "0.5", features = ["html_reports"] }
criterion = { version = "0.5.1", features = ["html_reports"] }
fakeit = "1.1"
rand = "0.8"
rand = "0.8.5"
palette = "0.7.3"
pretty_assertions = "1.4.0"
[features]
@@ -149,6 +150,16 @@ name = "demo"
# this runs for all of the terminal backends, so it can't be built using --all-features or scraped
doc-scrape-examples = false
[[example]]
name = "demo2"
required-features = ["crossterm", "widget-calendar"]
doc-scrape-examples = true
[[example]]
name = "docsrs"
required-features = ["crossterm"]
doc-scrape-examples = false
[[example]]
name = "gauge"
required-features = ["crossterm"]

500
README.md
View File

@@ -1,129 +1,347 @@
# Ratatui
<img align="left" src="https://avatars.githubusercontent.com/u/125200832?s=128&v=4">
`ratatui` is a [Rust](https://www.rust-lang.org) library that is all about cooking up terminal user interfaces.
It is a community fork of the original [tui-rs](https://github.com/fdehau/tui-rs)
project.
[![Crates.io](https://img.shields.io/crates/v/ratatui?logo=rust&style=flat-square)](https://crates.io/crates/ratatui)
[![License](https://img.shields.io/crates/l/ratatui?style=flat-square)](./LICENSE) [![GitHub CI
Status](https://img.shields.io/github/actions/workflow/status/ratatui-org/ratatui/ci.yml?style=flat-square&logo=github)](https://github.com/ratatui-org/ratatui/actions?query=workflow%3ACI+)
[![Docs.rs](https://img.shields.io/docsrs/ratatui?logo=rust&style=flat-square)](https://docs.rs/crate/ratatui/)
[![Dependency
Status](https://deps.rs/repo/github/ratatui-org/ratatui/status.svg?style=flat-square)](https://deps.rs/repo/github/ratatui-org/ratatui)
[![Codecov](https://img.shields.io/codecov/c/github/ratatui-org/ratatui?logo=codecov&style=flat-square&token=BAQ8SOKEST)](https://app.codecov.io/gh/ratatui-org/ratatui)
[![Discord](https://img.shields.io/discord/1070692720437383208?label=discord&logo=discord&style=flat-square)](https://discord.gg/pMCEU9hNEj)
[![Matrix](https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix)](https://matrix.to/#/#ratatui:matrix.org)
<!-- See RELEASE.md for instructions on creating the demo gif --->
![Demo of Ratatui](https://vhs.charm.sh/vhs-tF0QbuPbtHgUeG0sTVgFr.gif)
<details>
<summary>Table of Contents</summary>
* [Ratatui](#ratatui)
* [Installation](#installation)
* [Introduction](#introduction)
* [Quickstart](#quickstart)
* [Status of this fork](#status-of-this-fork)
* [Rust version requirements](#rust-version-requirements)
* [Documentation](#documentation)
* [Examples](#examples)
* [Widgets](#widgets)
* [Built in](#built-in)
* [Third\-party libraries, bootstrapping templates and
- [Ratatui](#ratatui)
- [Installation](#installation)
- [Introduction](#introduction)
- [Other Documentation](#other-documentation)
- [Quickstart](#quickstart)
- [Status of this fork](#status-of-this-fork)
- [Rust version requirements](#rust-version-requirements)
- [Widgets](#widgets)
- [Built in](#built-in)
- [Third\-party libraries, bootstrapping templates and
widgets](#third-party-libraries-bootstrapping-templates-and-widgets)
* [Apps](#apps)
* [Alternatives](#alternatives)
* [Contributors](#contributors)
* [Acknowledgments](#acknowledgments)
* [License](#license)
- [Apps](#apps)
- [Alternatives](#alternatives)
- [Acknowledgments](#acknowledgments)
- [License](#license)
</details>
<!-- cargo-rdme start -->
![Demo](https://raw.githubusercontent.com/ratatui-org/ratatui/aa09e59dc0058347f68d7c1e0c91f863c6f2b8c9/examples/demo2.gif)
<div align="center">
[![Crate Badge]](https://crates.io/crates/ratatui) [![License Badge]](./LICENSE) [![CI
Badge]](https://github.com/ratatui-org/ratatui/actions?query=workflow%3ACI+) [![Docs
Badge]](https://docs.rs/crate/ratatui/)<br>
[![Dependencies Badge]](https://deps.rs/repo/github/ratatui-org/ratatui) [![Codecov
Badge]](https://app.codecov.io/gh/ratatui-org/ratatui) [![Discord
Badge]](https://discord.gg/pMCEU9hNEj) [![Matrix
Badge]](https://matrix.to/#/#ratatui:matrix.org)<br>
[Documentation](https://docs.rs/ratatui) · [Ratatui Book](https://ratatui.rs) ·
[Examples](https://github.com/ratatui-org/ratatui/tree/main/examples) · [Report a
bug](https://github.com/ratatui-org/ratatui/issues/new?labels=bug&projects=&template=bug_report.md)
· [Request a
Feature](https://github.com/ratatui-org/ratatui/issues/new?labels=enhancement&projects=&template=feature_request.md)
· [Send a Pull Request](https://github.com/ratatui-org/ratatui/compare)
</div>
# Ratatui
[Ratatui] is a crate for cooking up terminal user interfaces in rust. It is a lightweight
library that provides a set of widgets and utilities to build complex rust TUIs. Ratatui was
forked from the [Tui-rs crate] in 2023 in order to continue its development.
## Installation
Add `ratatui` and `crossterm` as dependencies to your cargo.toml:
```shell
cargo add ratatui --features all-widgets
cargo add ratatui crossterm
```
Or modify your `Cargo.toml`
```toml
[dependencies]
ratatui = { version = "0.23.0", features = ["all-widgets"]}
```
Ratatui uses [Crossterm] by default as it works on most platforms. See the [Installation]
section of the [Ratatui Book] for more details on how to use other backends ([Termion] /
[Termwiz]).
## Introduction
`ratatui` is a terminal UI library that supports multiple backends:
Ratatui is based on the principle of immediate rendering with intermediate buffers. This means
that for each frame, your app must render all widgets that are supposed to be part of the UI.
This is in contrast to the retained mode style of rendering where widgets are updated and then
automatically redrawn on the next frame. See the [Rendering] section of the [Ratatui Book] for
more info.
* [crossterm](https://github.com/crossterm-rs/crossterm) [default]
* [termion](https://github.com/ticki/termion)
* [termwiz](https://github.com/wez/wezterm/tree/master/termwiz)
## Other documentation
The library is based on the principle of immediate rendering with intermediate buffers. This means
that at each new frame you should build all widgets that are supposed to be part of the UI. While
providing a great flexibility for rich and interactive UI, this may introduce overhead for highly
dynamic content. So, the implementation try to minimize the number of ansi escapes sequences
generated to draw the updated UI. In practice, given the speed of `Rust` the overhead rather comes
from the terminal emulator than the library itself.
Moreover, the library does not provide any input handling nor any event system and you may rely on
the previously cited libraries to achieve such features.
We keep a [CHANGELOG](./CHANGELOG.md) generated by [git-cliff](https://github.com/orhun/git-cliff)
utilizing [Conventional Commits](https://www.conventionalcommits.org/).
- [Ratatui Book] - explains the library's concepts and provides step-by-step tutorials
- [Examples] - a collection of examples that demonstrate how to use the library.
- [API Documentation] - the full API documentation for the library on docs.rs.
- [Changelog] - generated by [git-cliff] utilizing [Conventional Commits].
- [Contributing] - Please read this if you are interested in contributing to the project.
- [Breaking Changes] - a list of breaking changes in the library.
## Quickstart
The following example demonstrates the minimal amount of code necessary to setup a terminal and
render "Hello World!". The full code for this example which contains a little more detail is in
[hello_world.rs](./examples/hello_world.rs). For more guidance on how to create Ratatui apps, see
the [Docs](https://docs.rs/ratatui) and [Examples](#examples). There is also a starter template
available at [rust-tui-template](https://github.com/ratatui-org/rust-tui-template).
[hello_world.rs]. For more guidance on different ways to structure your application see the
[Application Patterns] and [Hello World tutorial] sections in the [Ratatui Book] and the various
[Examples]. There are also several starter templates available:
- [rust-tui-template]
- [ratatui-async-template] (book and template)
- [simple-tui-rs]
Every application built with `ratatui` needs to implement the following steps:
- Initialize the terminal
- A main loop to:
- Handle input events
- Draw the UI
- Restore the terminal state
The library contains a [`prelude`] module that re-exports the most commonly used traits and
types for convenience. Most examples in the documentation will use this instead of showing the
full path of each type.
### Initialize and restore the terminal
The [`Terminal`] type is the main entry point for any Ratatui application. It is a light
abstraction over a choice of [`Backend`] implementations that provides functionality to draw
each frame, clear the screen, hide the cursor, etc. It is parametrized over any type that
implements the [`Backend`] trait which has implementations for [Crossterm], [Termion] and
[Termwiz].
Most applications should enter the Alternate Screen when starting and leave it when exiting and
also enable raw mode to disable line buffering and enable reading key events. See the [`backend`
module] and the [Backends] section of the [Ratatui Book] for more info.
### Drawing the UI
The drawing logic is delegated to a closure that takes a [`Frame`] instance as argument. The
[`Frame`] provides the size of the area to draw to and allows the app to render any [`Widget`]
using the provided [`render_widget`] method. See the [Widgets] section of the [Ratatui Book] for
more info.
### Handling events
Ratatui does not include any input handling. Instead event handling can be implemented by
calling backend library methods directly. See the [Handling Events] section of the [Ratatui
Book] for more info. For example, if you are using [Crossterm], you can use the
[`crossterm::event`] module to handle events.
### Example
```rust
fn main() -> Result<(), Box<dyn Error>> {
let mut terminal = setup_terminal()?;
run(&mut terminal)?;
restore_terminal(&mut terminal)?;
use std::io::{self, stdout};
use crossterm::{
event::{self, Event, KeyCode},
ExecutableCommand,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}
};
use ratatui::{prelude::*, widgets::*};
fn main() -> io::Result<()> {
enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
let mut should_quit = false;
while !should_quit {
terminal.draw(ui)?;
should_quit = handle_events()?;
}
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
Ok(())
}
fn setup_terminal() -> Result<Terminal<CrosstermBackend<Stdout>>, Box<dyn Error>> {
let mut stdout = io::stdout();
enable_raw_mode()?;
execute!(stdout, EnterAlternateScreen)?;
Ok(Terminal::new(CrosstermBackend::new(stdout))?)
}
fn restore_terminal(
terminal: &mut Terminal<CrosstermBackend<Stdout>>,
) -> Result<(), Box<dyn Error>> {
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen,)?;
Ok(terminal.show_cursor()?)
}
fn run(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<(), Box<dyn Error>> {
Ok(loop {
terminal.draw(|frame| {
let greeting = Paragraph::new("Hello World!");
frame.render_widget(greeting, frame.size());
})?;
if event::poll(Duration::from_millis(250))? {
if let Event::Key(key) = event::read()? {
if KeyCode::Char('q') == key.code {
break;
}
fn handle_events() -> io::Result<bool> {
if event::poll(std::time::Duration::from_millis(50))? {
if let Event::Key(key) = event::read()? {
if key.kind == event::KeyEventKind::Press && key.code == KeyCode::Char('q') {
return Ok(true);
}
}
})
}
}
Ok(false)
}
fn ui(frame: &mut Frame) {
frame.render_widget(
Paragraph::new("Hello World!")
.block(Block::default().title("Greeting").borders(Borders::ALL)),
frame.size(),
);
}
```
Running this example produces the following output:
![docsrs-hello]
## Layout
The library comes with a basic yet useful layout management object called [`Layout`] which
allows you to split the available space into multiple areas and then render widgets in each
area. This lets you describe a responsive terminal UI by nesting layouts. See the [Layout]
section of the [Ratatui Book] for more info.
```rust
use ratatui::{prelude::*, widgets::*};
fn ui(frame: &mut Frame) {
let main_layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![
Constraint::Length(1),
Constraint::Min(0),
Constraint::Length(1),
])
.split(frame.size());
frame.render_widget(
Block::new().borders(Borders::TOP).title("Title Bar"),
main_layout[0],
);
frame.render_widget(
Block::new().borders(Borders::TOP).title("Status Bar"),
main_layout[2],
);
let inner_layout = Layout::default()
.direction(Direction::Horizontal)
.constraints(vec![Constraint::Percentage(50), Constraint::Percentage(50)])
.split(main_layout[1]);
frame.render_widget(
Block::default().borders(Borders::ALL).title("Left"),
inner_layout[0],
);
frame.render_widget(
Block::default().borders(Borders::ALL).title("Right"),
inner_layout[1],
);
}
```
Running this example produces the following output:
![docsrs-layout]
## Text and styling
The [`Text`], [`Line`] and [`Span`] types are the building blocks of the library and are used in
many places. [`Text`] is a list of [`Line`]s and a [`Line`] is a list of [`Span`]s. A [`Span`]
is a string with a specific style.
The [`style` module] provides types that represent the various styling options. The most
important one is [`Style`] which represents the foreground and background colors and the text
attributes of a [`Span`]. The [`style` module] also provides a [`Stylize`] trait that allows
short-hand syntax to apply a style to widgets and text. See the [Styling Text] section of the
[Ratatui Book] for more info.
```rust
use ratatui::{prelude::*, widgets::*};
fn ui(frame: &mut Frame) {
let areas = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
Constraint::Min(0),
])
.split(frame.size());
let span1 = Span::raw("Hello ");
let span2 = Span::styled(
"World",
Style::new()
.fg(Color::Green)
.bg(Color::White)
.add_modifier(Modifier::BOLD),
);
let span3 = "!".red().on_light_yellow().italic();
let line = Line::from(vec![span1, span2, span3]);
let text: Text = Text::from(vec![line]);
frame.render_widget(Paragraph::new(text), areas[0]);
// or using the short-hand syntax and implicit conversions
frame.render_widget(
Paragraph::new("Hello World!".red().on_white().bold()),
areas[1],
);
// to style the whole widget instead of just the text
frame.render_widget(
Paragraph::new("Hello World!").style(Style::new().red().on_white()),
areas[2],
);
// or using the short-hand syntax
frame.render_widget(Paragraph::new("Hello World!").blue().on_yellow(), areas[3]);
}
```
Running this example produces the following output:
![docsrs-styling]
[Ratatui Book]: https://ratatui.rs
[Installation]: https://ratatui.rs/installation.html
[Rendering]: https://ratatui.rs/concepts/rendering/index.html
[Application Patterns]: https://ratatui.rs/concepts/application_patterns/index.html
[Hello World tutorial]: https://ratatui.rs/tutorial/hello_world.html
[Backends]: https://ratatui.rs/concepts/backends/index.html
[Widgets]: https://ratatui.rs/how-to/widgets/index.html
[Handling Events]: https://ratatui.rs/concepts/event_handling.html
[Layout]: https://ratatui.rs/how-to/layout/index.html
[Styling Text]: https://ratatui.rs/how-to/render/style-text.html
[rust-tui-template]: https://github.com/ratatui-org/rust-tui-template
[ratatui-async-template]: https://ratatui-org.github.io/ratatui-async-template/
[simple-tui-rs]: https://github.com/pmsanford/simple-tui-rs
[Examples]: https://github.com/ratatui-org/ratatui/tree/main/examples
[git-cliff]: https://github.com/orhun/git-cliff
[Conventional Commits]: https://www.conventionalcommits.org
[API Documentation]: https://docs.rs/ratatui
[Changelog]: https://github.com/ratatui-org/ratatui/blob/main/CHANGELOG.md
[Contributing]: https:://github.com/ratatui-org/ratatui/blob/main/CONTRIBUTING.md
[Breaking Changes]: https:://github.com/ratatui-org/ratatui/blob/main/BREAKING-CHANGES.md
[docsrs-hello]: https://github.com/ratatui-org/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-hello.png?raw=true
[docsrs-layout]: https://github.com/ratatui-org/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-layout.png?raw=true
[docsrs-styling]: https://github.com/ratatui-org/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-styling.png?raw=true
[`Frame`]: terminal::Frame
[`render_widget`]: terminal::Frame::render_widget
[`Widget`]: widgets::Widget
[`Layout`]: layout::Layout
[`Text`]: text::Text
[`Line`]: text::Line
[`Span`]: text::Span
[`Style`]: style::Style
[`style` module]: style
[`Stylize`]: style::Stylize
[`Backend`]: backend::Backend
[`backend` module]: backend
[`crossterm::event`]: https://docs.rs/crossterm/latest/crossterm/event/index.html
[Ratatui]: https://ratatui.rs
[Crossterm]: https://crates.io/crates/crossterm
[Termion]: https://crates.io/crates/termion
[Termwiz]: https://crates.io/crates/termwiz
[Tui-rs crate]: https://crates.io/crates/tui
[hello_world.rs]: https://github.com/ratatui-org/ratatui/blob/main/examples/hello_world.rs
[Crate Badge]: https://img.shields.io/crates/v/ratatui?logo=rust&style=flat-square
[CI Badge]:
https://img.shields.io/github/actions/workflow/status/ratatui-org/ratatui/ci.yml?style=flat-square&logo=github
[Codecov Badge]:
https://img.shields.io/codecov/c/github/ratatui-org/ratatui?logo=codecov&style=flat-square&token=BAQ8SOKEST
[Dependencies Badge]: https://deps.rs/repo/github/ratatui-org/ratatui/status.svg?style=flat-square
[Discord Badge]:
https://img.shields.io/discord/1070692720437383208?label=discord&logo=discord&style=flat-square
[Docs Badge]: https://img.shields.io/docsrs/ratatui?logo=rust&style=flat-square
[License Badge]: https://img.shields.io/crates/l/ratatui?style=flat-square
[Matrix Badge]:
https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix
<!-- cargo-rdme end -->
## Status of this fork
In response to the original maintainer [**Florian Dehau**](https://github.com/fdehau)'s issue
@@ -146,35 +364,6 @@ you are interested in working on a PR or issue opened in the previous repository
Since version 0.23.0, The Minimum Supported Rust Version (MSRV) of `ratatui` is 1.67.0.
## Documentation
The documentation can be found on [docs.rs.](https://docs.rs/ratatui)
## Examples
The demo shown in the gif above is available on all available backends.
```shell
# crossterm
cargo run --example demo
# termion
cargo run --example demo --no-default-features --features=termion
# termwiz
cargo run --example demo --no-default-features --features=termwiz
```
The UI code for this is in [examples/demo/ui.rs](./examples/demo/ui.rs) while the application state
is in [examples/demo/app.rs](./examples/demo/app.rs).
If the user interface contains glyphs that are not displayed correctly by your terminal, you may
want to run the demo without those symbols:
```shell
cargo run --example demo --release -- --tick-rate 200 --enhanced-graphics false
```
More examples are available in the [examples](./examples/) folder.
## Widgets
### Built in
@@ -182,21 +371,21 @@ More examples are available in the [examples](./examples/) folder.
The library comes with the following
[widgets](https://docs.rs/ratatui/latest/ratatui/widgets/index.html):
* [BarChart](https://docs.rs/ratatui/latest/ratatui/widgets/struct.BarChart.html)
* [Block](https://docs.rs/ratatui/latest/ratatui/widgets/block/struct.Block.html)
* [Calendar](https://docs.rs/ratatui/latest/ratatui/widgets/calendar/index.html)
* [Canvas](https://docs.rs/ratatui/latest/ratatui/widgets/canvas/struct.Canvas.html) which allows
- [BarChart](https://docs.rs/ratatui/latest/ratatui/widgets/struct.BarChart.html)
- [Block](https://docs.rs/ratatui/latest/ratatui/widgets/block/struct.Block.html)
- [Calendar](https://docs.rs/ratatui/latest/ratatui/widgets/calendar/index.html)
- [Canvas](https://docs.rs/ratatui/latest/ratatui/widgets/canvas/struct.Canvas.html) which allows
rendering [points, lines, shapes and a world
map](https://docs.rs/ratatui/latest/ratatui/widgets/canvas/index.html)
* [Chart](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Chart.html)
* [Clear](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Clear.html)
* [Gauge](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Gauge.html)
* [List](https://docs.rs/ratatui/latest/ratatui/widgets/struct.List.html)
* [Paragraph](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Paragraph.html)
* [Scrollbar](https://docs.rs/ratatui/latest/ratatui/widgets/scrollbar/struct.Scrollbar.html)
* [Sparkline](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Sparkline.html)
* [Table](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Table.html)
* [Tabs](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Tabs.html)
- [Chart](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Chart.html)
- [Clear](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Clear.html)
- [Gauge](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Gauge.html)
- [List](https://docs.rs/ratatui/latest/ratatui/widgets/struct.List.html)
- [Paragraph](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Paragraph.html)
- [Scrollbar](https://docs.rs/ratatui/latest/ratatui/widgets/scrollbar/struct.Scrollbar.html)
- [Sparkline](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Sparkline.html)
- [Table](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Table.html)
- [Tabs](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Tabs.html)
Each widget has an associated example which can be found in the [examples](./examples/) folder. Run
each examples with cargo (e.g. to run the gauge example `cargo run --example gauge`), and quit by
@@ -207,31 +396,31 @@ be installed with `cargo install cargo-make`).
### Third-party libraries, bootstrapping templates and widgets
* [ansi-to-tui](https://github.com/uttarayan21/ansi-to-tui) — Convert ansi colored text to
- [ansi-to-tui](https://github.com/uttarayan21/ansi-to-tui) — Convert ansi colored text to
`ratatui::text::Text`
* [color-to-tui](https://github.com/uttarayan21/color-to-tui) — Parse hex colors to
- [color-to-tui](https://github.com/uttarayan21/color-to-tui) — Parse hex colors to
`ratatui::style::Color`
* [rust-tui-template](https://github.com/ratatui-org/rust-tui-template) — A template for bootstrapping a
Rust TUI application with Tui-rs & crossterm
* [simple-tui-rs](https://github.com/pmsanford/simple-tui-rs) — A simple example tui-rs app
* [tui-builder](https://github.com/jkelleyrtp/tui-builder) — Batteries-included MVC framework for
- [rust-tui-template](https://github.com/ratatui-org/rust-tui-template) — A template for
bootstrapping a Rust TUI application with Tui-rs & crossterm
- [simple-tui-rs](https://github.com/pmsanford/simple-tui-rs) — A simple example tui-rs app
- [tui-builder](https://github.com/jkelleyrtp/tui-builder) — Batteries-included MVC framework for
Tui-rs + Crossterm apps
* [tui-clap](https://github.com/kegesch/tui-clap-rs) — Use clap-rs together with Tui-rs
* [tui-log](https://github.com/kegesch/tui-log-rs) — Example of how to use logging with Tui-rs
* [tui-logger](https://github.com/gin66/tui-logger) — Logger and Widget for Tui-rs
* [tui-realm](https://github.com/veeso/tui-realm) — Tui-rs framework to build stateful applications
- [tui-clap](https://github.com/kegesch/tui-clap-rs) — Use clap-rs together with Tui-rs
- [tui-log](https://github.com/kegesch/tui-log-rs) — Example of how to use logging with Tui-rs
- [tui-logger](https://github.com/gin66/tui-logger) — Logger and Widget for Tui-rs
- [tui-realm](https://github.com/veeso/tui-realm) — Tui-rs framework to build stateful applications
with a React/Elm inspired approach
* [tui-realm-treeview](https://github.com/veeso/tui-realm-treeview) — Treeview component for
- [tui-realm-treeview](https://github.com/veeso/tui-realm-treeview) — Treeview component for
Tui-realm
* [tui-rs-tree-widgets](https://github.com/EdJoPaTo/tui-rs-tree-widget): Widget for tree data
- [tui-rs-tree-widgets](https://github.com/EdJoPaTo/tui-rs-tree-widget): Widget for tree data
structures.
* [tui-windows](https://github.com/markatk/tui-windows-rs) — Tui-rs abstraction to handle multiple
- [tui-windows](https://github.com/markatk/tui-windows-rs) — Tui-rs abstraction to handle multiple
windows and their rendering
* [tui-textarea](https://github.com/rhysd/tui-textarea): Simple yet powerful multi-line text editor
- [tui-textarea](https://github.com/rhysd/tui-textarea): Simple yet powerful multi-line text editor
widget supporting several key shortcuts, undo/redo, text search, etc.
* [tui-input](https://github.com/sayanarijit/tui-input): TUI input library supporting multiple
- [tui-input](https://github.com/sayanarijit/tui-input): TUI input library supporting multiple
backends and tui-rs.
* [tui-term](https://github.com/a-kenji/tui-term): A pseudoterminal widget library
- [tui-term](https://github.com/a-kenji/tui-term): A pseudoterminal widget library
that enables the rendering of terminal applications as ratatui widgets.
## Apps
@@ -244,11 +433,6 @@ Check out the list of more than 50 [Apps using
You might want to checkout [Cursive](https://github.com/gyscos/Cursive) for an alternative solution
to build text user interfaces in Rust.
## Contributors
[![GitHub
Contributors](https://contrib.rocks/image?repo=ratatui-org/ratatui)](https://github.com/ratatui-org/ratatui/graphs/contributors)
## Acknowledgments
Special thanks to [**Pavel Fomchenkov**](https://github.com/nawok) for his work in designing **an

View File

@@ -7,19 +7,39 @@ actions](.github/workflows/cd.yml) and triggered by pushing a tag.
[vhs](https://github.com/charmbracelet/vhs) (installation instructions in README).
```shell
cargo build --example demo
vhs examples/demo.tape --publish --quiet
cargo build --example demo2
vhs examples/demo2.tape
```
Then update the link in the [examples README](./examples/README) and the main README. Avoid
adding the gif to the git repo as binary files tend to bloat repositories.
1. Switch branches to the images branch and copy demo2.gif to examples/, commit, and push.
1. Grab the permalink from <https://github.com/ratatui-org/ratatui/blob/images/examples/demo2.gif> and
append `?raw=true` to redirect to the actual image url. Then update the link in the main README.
Avoid adding the gif to the git repo as binary files tend to bloat repositories.
1. Bump the version in [Cargo.toml](Cargo.toml).
1. Bump versions in the doc comments of [lib.rs](src/lib.rs).
1. Ensure [CHANGELOG.md](CHANGELOG.md) is updated. [git-cliff](https://github.com/orhun/git-cliff)
can be used for generating the entries.
1. Ensure that any breaking changes are documented in [BREAKING-CHANGES.md](./BREAKING-CHANGES.md)
1. Commit and push the changes.
1. Create a new tag: `git tag -a v[X.Y.Z]`
1. Push the tag: `git push --tags`
1. Wait for [Continuous Deployment](https://github.com/ratatui-org/ratatui/actions) workflow to
finish.
## Alpha Releases
Alpha releases are automatically released every Saturday via [cd.yml](./.github/workflows/cd.yml)
and can be manually be created when necessary by triggering the [Continuous
Deployment](https://github.com/ratatui-org/ratatui/actions/workflows/cd.yml) workflow.
We automatically release an alpha release with a patch level bump + alpha.num weekly (and when we
need to manually). E.g. the last release was 0.22.0, and the most recent alpha release is
0.22.1-alpha.1.
These releases will have whatever happened to be in main at the time of release, so they're useful
for apps that need to get releases from crates.io, but may contain more bugs and be generally less
tested than normal releases.
See [#147](https://github.com/ratatui-org/ratatui/issues/147) and
[#359](https://github.com/ratatui-org/ratatui/pull/359) for more info on the alpha release process.

View File

@@ -8,7 +8,7 @@ occur in a terminal.
## Demo
This is the demo example from the main README. It is available for each of the backends. Source:
This is the previous demo example from the main README. It is available for each of the backends. Source:
[demo.rs](./demo/).
```shell

View File

@@ -136,7 +136,7 @@ fn run_app<B: Backend>(
}
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
fn ui(f: &mut Frame, app: &App) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)].as_ref())
@@ -198,10 +198,7 @@ fn create_groups<'a>(app: &'a App, combine_values_and_labels: bool) -> Vec<BarGr
.collect()
}
fn draw_bar_with_group_labels<B>(f: &mut Frame<B>, app: &App, area: Rect)
where
B: Backend,
{
fn draw_bar_with_group_labels(f: &mut Frame, app: &App, area: Rect) {
let groups = create_groups(app, false);
let mut barchart = BarChart::default()
@@ -228,10 +225,7 @@ where
}
}
fn draw_horizontal_bars<B>(f: &mut Frame<B>, app: &App, area: Rect)
where
B: Backend,
{
fn draw_horizontal_bars(f: &mut Frame, app: &App, area: Rect) {
let groups = create_groups(app, true);
let mut barchart = BarChart::default()
@@ -260,10 +254,7 @@ where
}
}
fn draw_legend<B>(f: &mut Frame<B>, area: Rect)
where
B: Backend,
{
fn draw_legend(f: &mut Frame, area: Rect) {
let text = vec![
Line::from(Span::styled(
TOTAL_REVENUE,

View File

@@ -21,7 +21,6 @@ use ratatui::{
// These type aliases are used to make the code more readable by reducing repetition of the generic
// types. They are not necessary for the functionality of the code.
type Frame<'a> = ratatui::Frame<'a, CrosstermBackend<Stdout>>;
type Terminal = ratatui::Terminal<CrosstermBackend<Stdout>>;
type Result<T> = std::result::Result<T, Box<dyn Error>>;

View File

@@ -16,7 +16,7 @@ fn main() -> Result<(), Box<dyn Error>> {
let mut terminal = Terminal::new(backend)?;
loop {
let _ = terminal.draw(|f| draw(f));
let _ = terminal.draw(draw);
if let Event::Key(key) = event::read()? {
#[allow(clippy::single_match)]
@@ -35,7 +35,7 @@ fn main() -> Result<(), Box<dyn Error>> {
Ok(())
}
fn draw<B: Backend>(f: &mut Frame<B>) {
fn draw(f: &mut Frame) {
let app_area = f.size();
let calarea = Rect {

View File

@@ -156,7 +156,7 @@ fn run_app<B: Backend>(
}
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
fn ui(f: &mut Frame, app: &App) {
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())

View File

@@ -142,7 +142,7 @@ fn run_app<B: Backend>(
}
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
fn ui(f: &mut Frame, app: &App) {
let size = f.size();
let chunks = Layout::default()
.direction(Direction::Vertical)

View File

@@ -41,7 +41,7 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
}
}
fn ui<B: Backend>(frame: &mut Frame<B>) {
fn ui(frame: &mut Frame) {
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![
@@ -75,7 +75,7 @@ const NAMED_COLORS: [Color; 16] = [
Color::White,
];
fn render_named_colors<B: Backend>(frame: &mut Frame<B>, area: Rect) {
fn render_named_colors(frame: &mut Frame, area: Rect) {
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![Constraint::Length(3); 10])
@@ -94,7 +94,7 @@ fn render_named_colors<B: Backend>(frame: &mut Frame<B>, area: Rect) {
render_bg_named_colors(frame, Color::White, layout[9]);
}
fn render_fg_named_colors<B: Backend>(frame: &mut Frame<B>, bg: Color, area: Rect) {
fn render_fg_named_colors(frame: &mut Frame, bg: Color, area: Rect) {
let block = title_block(format!("Foreground colors on {bg} background"));
let inner = block.inner(area);
frame.render_widget(block, area);
@@ -119,7 +119,7 @@ fn render_fg_named_colors<B: Backend>(frame: &mut Frame<B>, bg: Color, area: Rec
}
}
fn render_bg_named_colors<B: Backend>(frame: &mut Frame<B>, fg: Color, area: Rect) {
fn render_bg_named_colors(frame: &mut Frame, fg: Color, area: Rect) {
let block = title_block(format!("Background colors with {fg} foreground"));
let inner = block.inner(area);
frame.render_widget(block, area);
@@ -144,7 +144,7 @@ fn render_bg_named_colors<B: Backend>(frame: &mut Frame<B>, fg: Color, area: Rec
}
}
fn render_indexed_colors<B: Backend>(frame: &mut Frame<B>, area: Rect) {
fn render_indexed_colors(frame: &mut Frame, area: Rect) {
let block = title_block("Indexed colors".into());
let inner = block.inner(area);
frame.render_widget(block, area);
@@ -243,7 +243,7 @@ fn title_block(title: String) -> Block<'static> {
.title_style(Style::new().reset())
}
fn render_indexed_grayscale<B: Backend>(frame: &mut Frame<B>, area: Rect) {
fn render_indexed_grayscale(frame: &mut Frame, area: Rect) {
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![

View File

@@ -13,7 +13,10 @@ use crossterm::{
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
};
use itertools::Itertools;
use palette::{
convert::{FromColorUnclamped, IntoColorUnclamped},
Okhsv, Srgb,
};
use ratatui::{prelude::*, widgets::*};
type Result<T> = std::result::Result<T, Box<dyn Error>>;
@@ -77,9 +80,8 @@ struct RgbColors;
impl Widget for RgbColors {
fn render(self, area: Rect, buf: &mut Buffer) {
let layout = Self::layout(area);
let rgb_colors = Self::create_rgb_color_grid(area.width, area.height * 2);
Self::render_title(layout[0], buf);
Self::render_colors(layout[1], buf, rgb_colors);
Self::render_colors(layout[1], buf);
}
}
@@ -99,41 +101,26 @@ impl RgbColors {
}
/// Render a colored grid of half block characters (`"▀"`) each with a different RGB color.
fn render_colors(area: Rect, buf: &mut Buffer, rgb_colors: Vec<Vec<Color>>) {
for (x, column) in (area.left()..area.right()).zip(rgb_colors.iter()) {
for (y, (fg, bg)) in (area.top()..area.bottom()).zip(column.iter().tuples()) {
let cell = buf.get_mut(x, y);
cell.fg = *fg;
cell.bg = *bg;
cell.symbol = "".into();
}
}
}
fn render_colors(area: Rect, buf: &mut Buffer) {
for (xi, x) in (area.left()..area.right()).enumerate() {
for (yi, y) in (area.top()..area.bottom()).enumerate() {
let hue = xi as f32 * 360.0 / area.width as f32;
/// Generate a smooth grid of colors
///
/// Red ranges from 0 to 255 across the x axis. Green ranges from 0 to 255 across the y axis.
/// Blue repeats every 32 pixels in both directions, but flipped every 16 pixels so that it
/// doesn't transition sharply from light to dark.
///
/// The result stored in a 2d vector of colors with the x axis as the first dimension, and the
/// y axis the second dimension.
fn create_rgb_color_grid(width: u16, height: u16) -> Vec<Vec<Color>> {
let mut result = vec![];
for x in 0..width {
let mut column = vec![];
for y in 0..height {
// flip both axes every 16 pixels. E.g. [0, 1, ... 15, 15, ... 1, 0]
let yy = if (y % 32) < 16 { y % 32 } else { 31 - y % 32 };
let xx = if (x % 32) < 16 { x % 32 } else { 31 - x % 32 };
let r = (256 * x / width) as u8;
let g = (256 * y / height) as u8;
let b = (yy * 16 + xx) as u8;
column.push(Color::Rgb(r, g, b))
let value_fg = (yi as f32) / (area.height as f32 - 0.5);
let fg = Okhsv::<f32>::new(hue, Okhsv::max_saturation(), value_fg);
let fg: Srgb = fg.into_color_unclamped();
let fg: Srgb<u8> = fg.into_format();
let fg = Color::Rgb(fg.red, fg.green, fg.blue);
let value_bg = (yi as f32 + 0.5) / (area.height as f32 - 0.5);
let bg = Okhsv::new(hue, Okhsv::max_saturation(), value_bg);
let bg = Srgb::<f32>::from_color_unclamped(bg);
let bg: Srgb<u8> = bg.into_format();
let bg = Color::Rgb(bg.red, bg.green, bg.blue);
buf.get_mut(x, y).set_char('▀').set_fg(fg).set_bg(bg);
}
result.push(column);
}
result
}
}

View File

@@ -1,27 +1,121 @@
use std::{error::Error, io};
use std::{error::Error, io, ops::ControlFlow, time::Duration};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
event::{
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, MouseButton, MouseEvent,
MouseEventKind,
},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{prelude::*, widgets::*};
#[derive(Default)]
struct Label<'a> {
text: &'a str,
/// A custom widget that renders a button with a label, theme and state.
#[derive(Debug, Clone)]
struct Button<'a> {
label: Line<'a>,
theme: Theme,
state: State,
}
impl<'a> Widget for Label<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
buf.set_string(area.left(), area.top(), self.text, Style::default());
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum State {
Normal,
Selected,
Active,
}
#[derive(Debug, Clone, Copy)]
struct Theme {
text: Color,
background: Color,
highlight: Color,
shadow: Color,
}
const BLUE: Theme = Theme {
text: Color::Rgb(16, 24, 48),
background: Color::Rgb(48, 72, 144),
highlight: Color::Rgb(64, 96, 192),
shadow: Color::Rgb(32, 48, 96),
};
const RED: Theme = Theme {
text: Color::Rgb(48, 16, 16),
background: Color::Rgb(144, 48, 48),
highlight: Color::Rgb(192, 64, 64),
shadow: Color::Rgb(96, 32, 32),
};
const GREEN: Theme = Theme {
text: Color::Rgb(16, 48, 16),
background: Color::Rgb(48, 144, 48),
highlight: Color::Rgb(64, 192, 64),
shadow: Color::Rgb(32, 96, 32),
};
/// A button with a label that can be themed.
impl<'a> Button<'a> {
pub fn new<T: Into<Line<'a>>>(label: T) -> Button<'a> {
Button {
label: label.into(),
theme: BLUE,
state: State::Normal,
}
}
pub fn theme(mut self, theme: Theme) -> Button<'a> {
self.theme = theme;
self
}
pub fn state(mut self, state: State) -> Button<'a> {
self.state = state;
self
}
}
impl<'a> Label<'a> {
fn text(mut self, text: &'a str) -> Label<'a> {
self.text = text;
self
impl<'a> Widget for Button<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
let (background, text, shadow, highlight) = self.colors();
buf.set_style(area, Style::new().bg(background).fg(text));
// render top line if there's enough space
if area.height > 2 {
buf.set_string(
area.x,
area.y,
"".repeat(area.width as usize),
Style::new().fg(highlight).bg(background),
);
}
// render bottom line if there's enough space
if area.height > 1 {
buf.set_string(
area.x,
area.y + area.height - 1,
"".repeat(area.width as usize),
Style::new().fg(shadow).bg(background),
);
}
// render label centered
buf.set_line(
area.x + (area.width.saturating_sub(self.label.width() as u16)) / 2,
area.y + (area.height.saturating_sub(1)) / 2,
&self.label,
area.width,
);
}
}
impl Button<'_> {
fn colors(&self) -> (Color, Color, Color, Color) {
let theme = self.theme;
match self.state {
State::Normal => (theme.background, theme.text, theme.shadow, theme.highlight),
State::Selected => (theme.highlight, theme.text, theme.shadow, theme.highlight),
State::Active => (theme.background, theme.text, theme.highlight, theme.shadow),
}
}
}
@@ -53,19 +147,122 @@ fn main() -> Result<(), Box<dyn Error>> {
}
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
let mut selected_button: usize = 0;
let button_states = &mut [State::Selected, State::Normal, State::Normal];
loop {
terminal.draw(ui)?;
if let Event::Key(key) = event::read()? {
if let KeyCode::Char('q') = key.code {
return Ok(());
terminal.draw(|frame| ui(frame, button_states))?;
if !event::poll(Duration::from_millis(100))? {
continue;
}
match event::read()? {
Event::Key(key) => {
if key.kind != event::KeyEventKind::Press {
continue;
}
if handle_key_event(key, button_states, &mut selected_button).is_break() {
break;
}
}
Event::Mouse(mouse) => handle_mouse_event(mouse, button_states, &mut selected_button),
_ => (),
}
}
Ok(())
}
fn ui<B: Backend>(f: &mut Frame<B>) {
let size = f.size();
let label = Label::default().text("Test");
f.render_widget(label, size);
fn ui(frame: &mut Frame, states: &[State; 3]) {
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![
Constraint::Length(1),
Constraint::Max(3),
Constraint::Length(1),
Constraint::Min(0), // ignore remaining space
])
.split(frame.size());
frame.render_widget(
Paragraph::new("Custom Widget Example (mouse enabled)"),
layout[0],
);
render_buttons(frame, layout[1], states);
frame.render_widget(
Paragraph::new("←/→: select, Space: toggle, q: quit"),
layout[2],
);
}
fn render_buttons(frame: &mut Frame<'_>, area: Rect, states: &[State; 3]) {
let layout = Layout::default()
.direction(Direction::Horizontal)
.constraints(vec![
Constraint::Length(15),
Constraint::Length(15),
Constraint::Length(15),
Constraint::Min(0), // ignore remaining space
])
.split(area);
frame.render_widget(Button::new("Red").theme(RED).state(states[0]), layout[0]);
frame.render_widget(
Button::new("Green").theme(GREEN).state(states[1]),
layout[1],
);
frame.render_widget(Button::new("Blue").theme(BLUE).state(states[2]), layout[2]);
}
fn handle_key_event(
key: event::KeyEvent,
button_states: &mut [State; 3],
selected_button: &mut usize,
) -> ControlFlow<()> {
match key.code {
KeyCode::Char('q') => return ControlFlow::Break(()),
KeyCode::Left => {
button_states[*selected_button] = State::Normal;
*selected_button = selected_button.saturating_sub(1);
button_states[*selected_button] = State::Selected;
}
KeyCode::Right => {
button_states[*selected_button] = State::Normal;
*selected_button = selected_button.saturating_add(1).min(2);
button_states[*selected_button] = State::Selected;
}
KeyCode::Char(' ') => {
if button_states[*selected_button] == State::Active {
button_states[*selected_button] = State::Normal;
} else {
button_states[*selected_button] = State::Active;
}
}
_ => (),
}
ControlFlow::Continue(())
}
fn handle_mouse_event(
mouse: MouseEvent,
button_states: &mut [State; 3],
selected_button: &mut usize,
) {
match mouse.kind {
MouseEventKind::Moved => {
let old_selected_button = *selected_button;
*selected_button = match mouse.column {
x if x < 15 => 0,
x if x < 30 => 1,
_ => 2,
};
if old_selected_button != *selected_button {
button_states[old_selected_button] = State::Normal;
button_states[*selected_button] = State::Selected;
}
}
MouseEventKind::Down(MouseButton::Left) => {
if button_states[*selected_button] == State::Active {
button_states[*selected_button] = State::Normal;
} else {
button_states[*selected_button] = State::Active;
}
}
_ => (),
}
}

View File

@@ -2,11 +2,20 @@
# To run this script, install vhs and run `vhs ./examples/custom_widget.tape`
Output "target/custom_widget.gif"
Set Theme "OceanicMaterial"
Set Width 1200
Set Height 200
Set Width 760
Set Height 260
Hide
Type "cargo run --example=custom_widget --features=crossterm"
Enter
Sleep 1s
Sleep 2s
Show
Sleep 5s
Sleep 1s
Set TypingSpeed 0.7s
Right
Right
Space
Space
Left
Space
Left
Space

View File

@@ -5,7 +5,7 @@ use ratatui::{
use crate::app::App;
pub fn draw<B: Backend>(f: &mut Frame<B>, app: &mut App) {
pub fn draw(f: &mut Frame, app: &mut App) {
let chunks = Layout::default()
.constraints([Constraint::Length(3), Constraint::Min(0)].as_ref())
.split(f.size());
@@ -28,10 +28,7 @@ pub fn draw<B: Backend>(f: &mut Frame<B>, app: &mut App) {
};
}
fn draw_first_tab<B>(f: &mut Frame<B>, app: &mut App, area: Rect)
where
B: Backend,
{
fn draw_first_tab(f: &mut Frame, app: &mut App, area: Rect) {
let chunks = Layout::default()
.constraints(
[
@@ -47,10 +44,7 @@ where
draw_text(f, chunks[2]);
}
fn draw_gauges<B>(f: &mut Frame<B>, app: &mut App, area: Rect)
where
B: Backend,
{
fn draw_gauges(f: &mut Frame, app: &mut App, area: Rect) {
let chunks = Layout::default()
.constraints(
[
@@ -102,10 +96,7 @@ where
f.render_widget(line_gauge, chunks[2]);
}
fn draw_charts<B>(f: &mut Frame<B>, app: &mut App, area: Rect)
where
B: Backend,
{
fn draw_charts(f: &mut Frame, app: &mut App, area: Rect) {
let constraints = if app.show_chart {
vec![Constraint::Percentage(50), Constraint::Percentage(50)]
} else {
@@ -249,10 +240,7 @@ where
}
}
fn draw_text<B>(f: &mut Frame<B>, area: Rect)
where
B: Backend,
{
fn draw_text(f: &mut Frame, area: Rect) {
let text = vec![
text::Line::from("This is a paragraph with several lines. You can change style your text the way you want"),
text::Line::from(""),
@@ -290,10 +278,7 @@ where
f.render_widget(paragraph, area);
}
fn draw_second_tab<B>(f: &mut Frame<B>, app: &mut App, area: Rect)
where
B: Backend,
{
fn draw_second_tab(f: &mut Frame, app: &mut App, area: Rect) {
let chunks = Layout::default()
.constraints([Constraint::Percentage(30), Constraint::Percentage(70)].as_ref())
.direction(Direction::Horizontal)
@@ -379,10 +364,7 @@ where
f.render_widget(map, chunks[1]);
}
fn draw_third_tab<B>(f: &mut Frame<B>, _app: &mut App, area: Rect)
where
B: Backend,
{
fn draw_third_tab(f: &mut Frame, _app: &mut App, area: Rect) {
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)])

View File

@@ -0,0 +1,43 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/demo.tape`
Output "target/demo2-social.gif"
Set Theme "OceanicMaterial"
# Github social preview size (1280x640 with 80px padding) and must be < 1MB
# This puts some constraints on the amount of interactivity we can do.
Set Width 1280
Set Height 640
Set Padding 80
Hide
Type "cargo run --example demo2 --features crossterm,widget-calendar"
Enter
Sleep 2s
Show
# About screen
Sleep 3.5s
Down # Red eye
Sleep 0.4s
Down # black eye
Sleep 1s
Tab
# Recipe
Sleep 1s
Set TypingSpeed 500ms
Down 7
Sleep 1s
Tab
# Email
Sleep 2s
Down 4
Sleep 2s
Tab
# Trace route
Sleep 1s
Set TypingSpeed 200ms
Down 10
Sleep 2s
Tab
# Weather
Set TypingSpeed 100ms
Down 40
Sleep 2s

49
examples/demo2.tape Normal file
View File

@@ -0,0 +1,49 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/demo.tape`
# NOTE: Requires VHS 0.6.1 or later for Screenshot support
Output "target/demo2.gif"
Set Theme "OceanicMaterial"
# The reason for this strange size is that the social preview image for this
# demo is 1280x64 with 80 pixels of padding on each side. We want a version
# without the padding for README.md, etc.
Set Width 1120
Set Height 480
Set Padding 0
Hide
Type "cargo run --example demo2 --features crossterm,widget-calendar"
Enter
Sleep 2s
Show
# About screen
Screenshot "target/demo2-about.png"
Sleep 3.5s
Down # Red eye
Sleep 0.4s
Down # black eye
Sleep 1s
Tab
# Recipe
Screenshot "target/demo2-recipe.png"
Sleep 1s
Set TypingSpeed 500ms
Down 7
Sleep 1s
Tab
# Email
Screenshot "target/demo2-email.png"
Sleep 2s
Down 4
Sleep 2s
Tab
# Trace route
Screenshot "target/demo2-trace.png"
Sleep 1s
Set TypingSpeed 200ms
Down 10
Sleep 2s
Tab
# Weather
Screenshot "target/demo2-weather.png"
Set TypingSpeed 100ms
Down 40
Sleep 2s

99
examples/demo2/app.rs Normal file
View File

@@ -0,0 +1,99 @@
use std::time::Duration;
use anyhow::{Context, Result};
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
use ratatui::prelude::Rect;
use crate::{Root, Term};
#[derive(Debug)]
pub struct App {
term: Term,
should_quit: bool,
context: AppContext,
}
#[derive(Debug, Default, Clone, Copy)]
pub struct AppContext {
pub tab_index: usize,
pub row_index: usize,
}
impl App {
fn new() -> Result<Self> {
Ok(Self {
term: Term::start()?,
should_quit: false,
context: AppContext::default(),
})
}
pub fn run() -> Result<()> {
install_panic_hook();
let mut app = Self::new()?;
while !app.should_quit {
app.draw()?;
app.handle_events()?;
}
Term::stop()?;
Ok(())
}
fn draw(&mut self) -> Result<()> {
self.term
.draw(|frame| frame.render_widget(Root::new(&self.context), frame.size()))
.context("terminal.draw")?;
Ok(())
}
fn handle_events(&mut self) -> Result<()> {
match Term::next_event(Duration::from_millis(16))? {
Some(Event::Key(key)) => self.handle_key_event(key),
Some(Event::Resize(width, height)) => {
Ok(self.term.resize(Rect::new(0, 0, width, height))?)
}
_ => Ok(()),
}
}
fn handle_key_event(&mut self, key: KeyEvent) -> Result<()> {
if key.kind != KeyEventKind::Press {
return Ok(());
}
let context = &mut self.context;
const TAB_COUNT: usize = 5;
match key.code {
KeyCode::Char('q') | KeyCode::Esc => {
self.should_quit = true;
}
KeyCode::Tab | KeyCode::BackTab if key.modifiers.contains(KeyModifiers::SHIFT) => {
let tab_index = context.tab_index + TAB_COUNT; // to wrap around properly
context.tab_index = tab_index.saturating_sub(1) % TAB_COUNT;
context.row_index = 0;
}
KeyCode::Tab | KeyCode::BackTab => {
context.tab_index = context.tab_index.saturating_add(1) % TAB_COUNT;
context.row_index = 0;
}
KeyCode::Up | KeyCode::Char('k') => {
context.row_index = context.row_index.saturating_sub(1);
}
KeyCode::Down | KeyCode::Char('j') => {
context.row_index = context.row_index.saturating_add(1);
}
_ => {}
};
Ok(())
}
}
pub fn install_panic_hook() {
better_panic::install();
let hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
let _ = Term::stop();
hook(info);
std::process::exit(1);
}));
}

34
examples/demo2/colors.rs Normal file
View File

@@ -0,0 +1,34 @@
use palette::{IntoColor, Okhsv, Srgb};
use ratatui::{prelude::*, widgets::*};
/// A widget that renders a color swatch of RGB colors.
///
/// The widget is rendered as a rectangle with the hue changing along the x-axis from 0.0 to 360.0
/// and the value changing along the y-axis (from 1.0 to 0.0). Each pixel is rendered as a block
/// character with the top half slightly lighter than the bottom half.
pub struct RgbSwatch;
impl Widget for RgbSwatch {
fn render(self, area: Rect, buf: &mut Buffer) {
for (yi, y) in (area.top()..area.bottom()).enumerate() {
let value = area.height as f32 - yi as f32;
let value_fg = value / (area.height as f32);
let value_bg = (value - 0.5) / (area.height as f32);
for (xi, x) in (area.left()..area.right()).enumerate() {
let hue = xi as f32 * 360.0 / area.width as f32;
let fg = color_from_oklab(hue, Okhsv::max_saturation(), value_fg);
let bg = color_from_oklab(hue, Okhsv::max_saturation(), value_bg);
buf.get_mut(x, y).set_char('▀').set_fg(fg).set_bg(bg);
}
}
}
}
/// Convert a hue and value into an RGB color via the OkLab color space.
///
/// See <https://bottosson.github.io/posts/oklab/> for more details.
pub fn color_from_oklab(hue: f32, saturation: f32, value: f32) -> Color {
let color: Srgb = Okhsv::new(hue, saturation, value).into_color();
let color = color.into_format();
Color::Rgb(color.red, color.green, color.blue)
}

17
examples/demo2/main.rs Normal file
View File

@@ -0,0 +1,17 @@
use anyhow::Result;
pub use app::*;
pub use colors::*;
pub use root::*;
pub use term::*;
pub use theme::*;
mod app;
mod colors;
mod root;
mod tabs;
mod term;
mod theme;
fn main() -> Result<()> {
App::run()
}

93
examples/demo2/root.rs Normal file
View File

@@ -0,0 +1,93 @@
use std::rc::Rc;
use itertools::Itertools;
use ratatui::{prelude::*, widgets::*};
use crate::{tabs::*, AppContext, THEME};
pub struct Root<'a> {
context: &'a AppContext,
}
impl<'a> Root<'a> {
pub fn new(context: &'a AppContext) -> Self {
Root { context }
}
}
impl Widget for Root<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
Block::new().style(THEME.root).render(area, buf);
let area = layout(area, Direction::Vertical, vec![1, 0, 1]);
self.render_title_bar(area[0], buf);
self.render_selected_tab(area[1], buf);
self.render_bottom_bar(area[2], buf);
}
}
impl Root<'_> {
fn render_title_bar(&self, area: Rect, buf: &mut Buffer) {
let area = layout(area, Direction::Horizontal, vec![0, 45]);
Paragraph::new(Span::styled("Ratatui", THEME.app_title)).render(area[0], buf);
let titles = vec!["", " Recipe ", " Email ", " Traceroute ", " Weather "];
Tabs::new(titles)
.style(THEME.tabs)
.highlight_style(THEME.tabs_selected)
.select(self.context.tab_index)
.divider("")
.render(area[1], buf);
}
fn render_selected_tab(&self, area: Rect, buf: &mut Buffer) {
let row_index = self.context.row_index;
match self.context.tab_index {
0 => AboutTab::new(row_index).render(area, buf),
1 => RecipeTab::new(row_index).render(area, buf),
2 => EmailTab::new(row_index).render(area, buf),
3 => TracerouteTab::new(row_index).render(area, buf),
4 => WeatherTab::new(row_index).render(area, buf),
_ => unreachable!(),
};
}
fn render_bottom_bar(&self, area: Rect, buf: &mut Buffer) {
let keys = [
("Q/Esc", "Quit"),
("Tab", "Next Tab"),
("↑/k", "Up"),
("↓/j", "Down"),
];
let spans = keys
.iter()
.flat_map(|(key, desc)| {
let key = Span::styled(format!(" {} ", key), THEME.key_binding.key);
let desc = Span::styled(format!(" {} ", desc), THEME.key_binding.description);
[key, desc]
})
.collect_vec();
Paragraph::new(Line::from(spans))
.alignment(Alignment::Center)
.fg(Color::Indexed(236))
.bg(Color::Indexed(232))
.render(area, buf);
}
}
/// simple helper method to split an area into multiple sub-areas
pub fn layout(area: Rect, direction: Direction, heights: Vec<u16>) -> Rc<[Rect]> {
let constraints = heights
.iter()
.map(|&h| {
if h > 0 {
Constraint::Length(h)
} else {
Constraint::Min(0)
}
})
.collect_vec();
Layout::default()
.direction(direction)
.constraints(constraints)
.split(area)
}

11
examples/demo2/tabs.rs Normal file
View File

@@ -0,0 +1,11 @@
mod about;
mod email;
mod recipe;
mod traceroute;
mod weather;
pub use about::AboutTab;
pub use email::EmailTab;
pub use recipe::RecipeTab;
pub use traceroute::TracerouteTab;
pub use weather::WeatherTab;

View File

@@ -0,0 +1,157 @@
use itertools::Itertools;
use ratatui::{prelude::*, widgets::*};
use crate::{layout, RgbSwatch, THEME};
const RATATUI_LOGO: [&str; 32] = [
" ███ ",
" ██████ ",
" ███████ ",
" ████████ ",
" █████████ ",
" ██████████ ",
" ████████████ ",
" █████████████ ",
" █████████████ ██████",
" ███████████ ████████",
" █████ ███████████ ",
" ███ ██ee████████ ",
" █ █████████████ ",
" ████ █████████████ ",
" █████████████████ ",
" ████████████████ ",
" ████████████████ ",
" ███ ██████████ ",
" ██ █████████ ",
" █xx█ █████████ ",
" █xxxx█ ██████████ ",
" █xx█xxx█ █████████ ",
" █xx██xxxx█ ████████ ",
" █xxxxxxxxxx█ ██████████ ",
" █xxxxxxxxxxxx█ ██████████ ",
" █xxxxxxx██xxxxx█ █████████ ",
" █xxxxxxxxx██xxxxx█ ████ ███ ",
" █xxxxxxxxxxxxxxxxxx█ ██ ███ ",
"█xxxxxxxxxxxxxxxxxxxx█ █ ███ ",
"█xxxxxxxxxxxxxxxxxxxxx█ ███ ",
" █xxxxxxxxxxxxxxxxxxxxx█ ███ ",
" █xxxxxxxxxxxxxxxxxxxxx█ █ ",
];
pub struct AboutTab {
selected_row: usize,
}
impl AboutTab {
pub fn new(selected_row: usize) -> Self {
Self { selected_row }
}
}
impl Widget for AboutTab {
fn render(self, area: Rect, buf: &mut Buffer) {
RgbSwatch.render(area, buf);
let area = layout(area, Direction::Horizontal, vec![34, 0]);
render_crate_description(area[1], buf);
render_logo(self.selected_row, area[0], buf);
}
}
fn render_crate_description(area: Rect, buf: &mut Buffer) {
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 text = "- cooking up terminal user interfaces -
Ratatui is a Rust crate that provides widgets (e.g. Paragraph, Table) and draws them to the \
screen efficiently every frame.";
Paragraph::new(text)
.style(THEME.description)
.block(
Block::new()
.title(" Ratatui ")
.title_alignment(Alignment::Center)
.borders(Borders::TOP)
.border_style(THEME.description_title)
.padding(Padding::new(0, 0, 0, 0)),
)
.wrap(Wrap { trim: true })
.scroll((0, 0))
.render(area, buf);
}
/// Use half block characters to render a logo based on the RATATUI_LOGO const.
///
/// The logo is rendered in three colors, one for the rat, one for the terminal, and one for the
/// rat's eye. The eye color alternates between two colors based on the selected row.
pub fn render_logo(selected_row: usize, area: Rect, buf: &mut Buffer) {
let eye_color = if selected_row % 2 == 0 {
THEME.logo.rat_eye
} else {
THEME.logo.rat_eye_alt
};
let area = area.inner(&Margin {
vertical: 0,
horizontal: 2,
});
for (y, (line1, line2)) in RATATUI_LOGO.iter().tuples().enumerate() {
for (x, (ch1, ch2)) in line1.chars().zip(line2.chars()).enumerate() {
let x = area.left() + x as u16;
let y = area.top() + y as u16;
let cell = buf.get_mut(x, y);
let rat_color = THEME.logo.rat;
let term_color = THEME.logo.term;
match (ch1, ch2) {
('█', '█') => {
cell.set_char('█');
cell.fg = rat_color;
}
('█', ' ') => {
cell.set_char('▀');
cell.fg = rat_color;
}
(' ', '█') => {
cell.set_char('▄');
cell.fg = rat_color;
}
('█', 'x') => {
cell.set_char('▀');
cell.fg = rat_color;
cell.bg = term_color;
}
('x', '█') => {
cell.set_char('▄');
cell.fg = rat_color;
cell.bg = term_color;
}
('x', 'x') => {
cell.set_char(' ');
cell.fg = term_color;
cell.bg = term_color;
}
('█', 'e') => {
cell.set_char('▀');
cell.fg = rat_color;
cell.bg = eye_color;
}
('e', '█') => {
cell.set_char('▄');
cell.fg = rat_color;
cell.bg = eye_color;
}
(_, _) => {}
};
}
}
}

View File

@@ -0,0 +1,143 @@
use itertools::Itertools;
use ratatui::{prelude::*, widgets::*};
use unicode_width::UnicodeWidthStr;
use crate::{layout, RgbSwatch, THEME};
#[derive(Debug, Default)]
pub struct Email {
from: &'static str,
subject: &'static str,
body: &'static str,
}
const EMAILS: &[Email] = &[
Email {
from: "Alice <alice@example.com>",
subject: "Hello",
body: "Hi Bob,\nHow are you?\n\nAlice",
},
Email {
from: "Bob <bob@example.com>",
subject: "Re: Hello",
body: "Hi Alice,\nI'm fine, thanks!\n\nBob",
},
Email {
from: "Charlie <charlie@example.com>",
subject: "Re: Hello",
body: "Hi Alice,\nI'm fine, thanks!\n\nCharlie",
},
Email {
from: "Dave <dave@example.com>",
subject: "Re: Hello (STOP REPLYING TO ALL)",
body: "Hi Everyone,\nPlease stop replying to all.\n\nDave",
},
Email {
from: "Eve <eve@example.com>",
subject: "Re: Hello (STOP REPLYING TO ALL)",
body: "Hi Everyone,\nI'm reading all your emails.\n\nEve",
},
];
#[derive(Debug, Default)]
pub struct EmailTab {
selected_index: usize,
}
impl EmailTab {
pub fn new(selected_index: usize) -> Self {
Self {
selected_index: selected_index % EMAILS.len(),
}
}
}
impl Widget for EmailTab {
fn render(self, area: Rect, buf: &mut Buffer) {
RgbSwatch.render(area, buf);
let area = area.inner(&Margin {
vertical: 1,
horizontal: 2,
});
Clear.render(area, buf);
let area = layout(area, Direction::Vertical, vec![5, 0]);
render_inbox(self.selected_index, area[0], buf);
render_email(self.selected_index, area[1], buf);
}
}
fn render_inbox(selected_index: usize, area: Rect, buf: &mut Buffer) {
let area = layout(area, Direction::Vertical, vec![1, 0]);
let theme = THEME.email;
Tabs::new(vec![" Inbox ", " Sent ", " Drafts "])
.style(theme.tabs)
.highlight_style(theme.tabs_selected)
.select(0)
.divider("")
.render(area[0], buf);
let highlight_symbol = ">>";
let from_width = EMAILS
.iter()
.map(|e| e.from.width())
.max()
.unwrap_or_default();
let items = EMAILS
.iter()
.map(|e| {
let from = format!("{:width$}", e.from, width = from_width).into();
ListItem::new(Line::from(vec![from, " ".into(), e.subject.into()]))
})
.collect_vec();
let mut state = ListState::default().with_selected(Some(selected_index));
StatefulWidget::render(
List::new(items)
.style(theme.inbox)
.highlight_style(theme.selected_item)
.highlight_symbol(highlight_symbol),
area[1],
buf,
&mut state,
);
let mut scrollbar_state = ScrollbarState::default()
.content_length(EMAILS.len())
.position(selected_index);
Scrollbar::default()
.begin_symbol(None)
.end_symbol(None)
.track_symbol(None)
.thumb_symbol("")
.render(area[1], buf, &mut scrollbar_state);
}
fn render_email(selected_index: usize, area: Rect, buf: &mut Buffer) {
let theme = THEME.email;
let email = EMAILS.get(selected_index);
let block = Block::new()
.style(theme.body)
.padding(Padding::new(2, 2, 0, 0))
.borders(Borders::TOP)
.border_type(BorderType::Thick);
let inner = block.inner(area);
block.render(area, buf);
if let Some(email) = email {
let area = layout(inner, Direction::Vertical, vec![3, 0]);
let headers = vec![
Line::from(vec![
"From: ".set_style(theme.header),
email.from.set_style(theme.header_value),
]),
Line::from(vec![
"Subject: ".set_style(theme.header),
email.subject.set_style(theme.header_value),
]),
"-".repeat(inner.width as usize).dim().into(),
];
Paragraph::new(headers)
.style(theme.body)
.render(area[0], buf);
let body = email.body.lines().map(Line::from).collect_vec();
Paragraph::new(body).style(theme.body).render(area[1], buf);
} else {
Paragraph::new("No email selected").render(inner, buf);
}
}

View File

@@ -0,0 +1,171 @@
use itertools::Itertools;
use ratatui::{prelude::*, widgets::*};
use crate::{layout, RgbSwatch, THEME};
#[derive(Debug, Default, Clone, Copy)]
struct Ingredient {
quantity: &'static str,
name: &'static str,
}
impl Ingredient {
fn height(&self) -> u16 {
self.name.lines().count() as u16
}
}
impl<'a> From<Ingredient> for Row<'a> {
fn from(i: Ingredient) -> Self {
Row::new(vec![i.quantity, i.name]).height(i.height())
}
}
// https://www.realsimple.com/food-recipes/browse-all-recipes/ratatouille
const RECIPE: &[(&str, &str)] = &[
(
"Step 1: ",
"Over medium-low heat, add the oil to a large skillet with the onion, garlic, and bay \
leaf, stirring occasionally, until the onion has softened.",
),
(
"Step 2: ",
"Add the eggplant and cook, stirring occasionally, for 8 minutes or until the eggplant \
has softened. Stir in the zucchini, red bell pepper, tomatoes, and salt, and cook over \
medium heat, stirring occasionally, for 5 to 7 minutes or until the vegetables are \
tender. Stir in the basil and few grinds of pepper to taste.",
),
];
const INGREDIENTS: &[Ingredient] = &[
Ingredient {
quantity: "4 tbsp",
name: "olive oil",
},
Ingredient {
quantity: "1",
name: "onion thinly sliced",
},
Ingredient {
quantity: "4",
name: "cloves garlic\npeeled and sliced",
},
Ingredient {
quantity: "1",
name: "small bay leaf",
},
Ingredient {
quantity: "1",
name: "small eggplant cut\ninto 1/2 inch cubes",
},
Ingredient {
quantity: "1",
name: "small zucchini halved\nlengthwise and cut\ninto thin slices",
},
Ingredient {
quantity: "1",
name: "red bell pepper cut\ninto slivers",
},
Ingredient {
quantity: "4",
name: "plum tomatoes\ncoarsely chopped",
},
Ingredient {
quantity: "1 tsp",
name: "kosher salt",
},
Ingredient {
quantity: "1/4 cup",
name: "shredded fresh basil\nleaves",
},
Ingredient {
quantity: "",
name: "freshly ground black\npepper",
},
];
#[derive(Debug)]
pub struct RecipeTab {
selected_row: usize,
}
impl RecipeTab {
pub fn new(selected_row: usize) -> Self {
Self {
selected_row: selected_row % INGREDIENTS.len(),
}
}
}
impl Widget for RecipeTab {
fn render(self, area: Rect, buf: &mut Buffer) {
RgbSwatch.render(area, buf);
let area = area.inner(&Margin {
vertical: 1,
horizontal: 2,
});
Clear.render(area, buf);
Block::new()
.title("Ratatouille Recipe".bold().white())
.title_alignment(Alignment::Center)
.style(THEME.content)
.padding(Padding::new(1, 1, 2, 1))
.render(area, buf);
let scrollbar_area = Rect {
y: area.y + 2,
height: area.height - 3,
..area
};
render_scrollbar(self.selected_row, scrollbar_area, buf);
let area = area.inner(&Margin {
horizontal: 2,
vertical: 1,
});
let area = layout(area, Direction::Horizontal, vec![44, 0]);
render_recipe(area[0], buf);
render_ingredients(self.selected_row, area[1], buf);
}
}
fn render_recipe(area: Rect, buf: &mut Buffer) {
let lines = RECIPE
.iter()
.map(|(step, text)| Line::from(vec![step.white().bold(), text.gray()]))
.collect_vec();
Paragraph::new(lines)
.wrap(Wrap { trim: true })
.block(Block::new().padding(Padding::new(0, 1, 0, 0)))
.render(area, buf);
}
fn render_ingredients(selected_row: usize, area: Rect, buf: &mut Buffer) {
let mut state = TableState::default().with_selected(Some(selected_row));
let rows = INGREDIENTS.iter().map(|&i| i.into()).collect_vec();
let theme = THEME.recipe;
StatefulWidget::render(
Table::new(rows)
.block(Block::new().style(theme.ingredients))
.header(Row::new(vec!["Qty", "Ingredient"]).style(theme.ingredients_header))
.widths(&[Constraint::Length(7), Constraint::Length(30)])
.highlight_style(Style::new().light_yellow()),
area,
buf,
&mut state,
);
}
fn render_scrollbar(position: usize, area: Rect, buf: &mut Buffer) {
let mut state = ScrollbarState::default()
.content_length(INGREDIENTS.len())
.viewport_content_length(6)
.position(position);
Scrollbar::new(ScrollbarOrientation::VerticalRight)
.begin_symbol(None)
.end_symbol(None)
.track_symbol(None)
.thumb_symbol("")
.render(area, buf, &mut state)
}

View File

@@ -0,0 +1,199 @@
use itertools::Itertools;
use ratatui::{
prelude::*,
widgets::{canvas::*, *},
};
use crate::{layout, RgbSwatch, THEME};
#[derive(Debug)]
pub struct TracerouteTab {
selected_row: usize,
}
impl TracerouteTab {
pub fn new(selected_row: usize) -> Self {
Self {
selected_row: selected_row % HOPS.len(),
}
}
}
impl Widget for TracerouteTab {
fn render(self, area: Rect, buf: &mut Buffer) {
RgbSwatch.render(area, buf);
let area = area.inner(&Margin {
vertical: 1,
horizontal: 2,
});
Clear.render(area, buf);
Block::new().style(THEME.content).render(area, buf);
let area = Layout::default()
.direction(Direction::Horizontal)
.constraints(vec![Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)])
.split(area);
let left_area = layout(area[0], Direction::Vertical, vec![0, 3]);
render_hops(self.selected_row, left_area[0], buf);
render_ping(self.selected_row, left_area[1], buf);
render_map(self.selected_row, area[1], buf);
}
}
fn render_hops(selected_row: usize, area: Rect, buf: &mut Buffer) {
let mut state = TableState::default().with_selected(Some(selected_row));
let rows = HOPS
.iter()
.map(|hop| Row::new(vec![hop.host, hop.address]))
.collect_vec();
let block = Block::default()
.title("Traceroute bad.horse".bold().white())
.title_alignment(Alignment::Center)
.padding(Padding::new(1, 1, 1, 1));
StatefulWidget::render(
Table::new(rows)
.header(Row::new(vec!["Host", "Address"]).set_style(THEME.traceroute.header))
.widths(&[Constraint::Max(100), Constraint::Length(15)])
.highlight_style(THEME.traceroute.selected)
.block(block),
area,
buf,
&mut state,
);
let mut scrollbar_state = ScrollbarState::default()
.content_length(HOPS.len())
.position(selected_row);
let area = Rect {
width: area.width + 1,
y: area.y + 3,
height: area.height - 4,
..area
};
Scrollbar::default()
.orientation(ScrollbarOrientation::VerticalLeft)
.begin_symbol(None)
.end_symbol(None)
.track_symbol(None)
.thumb_symbol("")
.render(area, buf, &mut scrollbar_state);
}
pub fn render_ping(progress: usize, area: Rect, buf: &mut Buffer) {
let mut data = [
8, 8, 8, 8, 7, 7, 7, 6, 6, 5, 4, 3, 3, 2, 2, 1, 1, 1, 2, 2, 3, 4, 5, 6, 7, 7, 8, 8, 8, 7,
7, 6, 5, 4, 3, 2, 1, 1, 1, 1, 1, 2, 4, 6, 7, 8, 8, 8, 8, 6, 4, 2, 1, 1, 1, 1, 2, 2, 2, 3,
3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7,
];
let mid = progress % data.len();
data.rotate_left(mid);
Sparkline::default()
.block(
Block::new()
.title("Ping")
.title_alignment(Alignment::Center)
.border_type(BorderType::Thick),
)
.data(&data)
.style(THEME.traceroute.ping)
.render(area, buf);
}
fn render_map(selected_row: usize, area: Rect, buf: &mut Buffer) {
let theme = THEME.traceroute.map;
let path: Option<(&Hop, &Hop)> = HOPS.iter().tuple_windows().nth(selected_row);
let map = Map {
resolution: canvas::MapResolution::High,
color: theme.color,
};
Canvas::default()
.background_color(theme.background_color)
.block(
Block::new()
.padding(Padding::new(1, 0, 1, 0))
.style(theme.style),
)
.marker(Marker::Dot)
// picked to show Australia for the demo as it's the most interesting part of the map
// (and the only part with hops ;))
.x_bounds([113.0, 154.0])
.y_bounds([-42.0, -11.0])
.paint(|context| {
context.draw(&map);
if let Some(path) = path {
context.draw(&canvas::Line::new(
path.0.location.0,
path.0.location.1,
path.1.location.0,
path.1.location.1,
theme.path,
));
context.draw(&Points {
color: theme.source,
coords: &[path.0.location], // sydney
});
context.draw(&Points {
color: theme.destination,
coords: &[path.1.location], // perth
});
}
})
.render(area, buf);
}
#[derive(Debug)]
struct Hop {
host: &'static str,
address: &'static str,
location: (f64, f64),
}
impl Hop {
const fn new(name: &'static str, address: &'static str, location: (f64, f64)) -> Self {
Self {
host: name,
address,
location,
}
}
}
const CANBERRA: (f64, f64) = (149.1, -35.3);
const SYDNEY: (f64, f64) = (151.1, -33.9);
const MELBOURNE: (f64, f64) = (144.9, -37.8);
const PERTH: (f64, f64) = (115.9, -31.9);
const DARWIN: (f64, f64) = (130.8, -12.4);
const BRISBANE: (f64, f64) = (153.0, -27.5);
const ADELAIDE: (f64, f64) = (138.6, -34.9);
// Go traceroute bad.horse some time, it's fun. these locations are made up and don't correspond
// to the actual IP addresses (which are in Toronto, Canada).
const HOPS: &[Hop] = &[
Hop::new("home", "127.0.0.1", CANBERRA),
Hop::new("bad.horse", "162.252.205.130", SYDNEY),
Hop::new("bad.horse", "162.252.205.131", MELBOURNE),
Hop::new("bad.horse", "162.252.205.132", BRISBANE),
Hop::new("bad.horse", "162.252.205.133", SYDNEY),
Hop::new("he.rides.across.the.nation", "162.252.205.134", PERTH),
Hop::new("the.thoroughbred.of.sin", "162.252.205.135", DARWIN),
Hop::new("he.got.the.application", "162.252.205.136", BRISBANE),
Hop::new("that.you.just.sent.in", "162.252.205.137", ADELAIDE),
Hop::new("it.needs.evaluation", "162.252.205.138", DARWIN),
Hop::new("so.let.the.games.begin", "162.252.205.139", PERTH),
Hop::new("a.heinous.crime", "162.252.205.140", BRISBANE),
Hop::new("a.show.of.force", "162.252.205.141", CANBERRA),
Hop::new("a.murder.would.be.nice.of.course", "162.252.205.142", PERTH),
Hop::new("bad.horse", "162.252.205.143", MELBOURNE),
Hop::new("bad.horse", "162.252.205.144", DARWIN),
Hop::new("bad.horse", "162.252.205.145", MELBOURNE),
Hop::new("he-s.bad", "162.252.205.146", PERTH),
Hop::new("the.evil.league.of.evil", "162.252.205.147", BRISBANE),
Hop::new("is.watching.so.beware", "162.252.205.148", DARWIN),
Hop::new("the.grade.that.you.receive", "162.252.205.149", PERTH),
Hop::new("will.be.your.last.we.swear", "162.252.205.150", ADELAIDE),
Hop::new("so.make.the.bad.horse.gleeful", "162.252.205.151", SYDNEY),
Hop::new("or.he-ll.make.you.his.mare", "162.252.205.152", MELBOURNE),
Hop::new("o_o", "162.252.205.153", BRISBANE),
Hop::new("you-re.saddled.up", "162.252.205.154", DARWIN),
Hop::new("there-s.no.recourse", "162.252.205.155", PERTH),
Hop::new("it-s.hi-ho.silver", "162.252.205.156", SYDNEY),
Hop::new("signed.bad.horse", "162.252.205.157", CANBERRA),
];

View File

@@ -0,0 +1,141 @@
use itertools::Itertools;
use palette::Okhsv;
use ratatui::{
prelude::*,
widgets::{calendar::CalendarEventStore, *},
};
use time::OffsetDateTime;
use crate::{color_from_oklab, layout, RgbSwatch, THEME};
pub struct WeatherTab {
pub selected_row: usize,
}
impl WeatherTab {
pub fn new(selected_row: usize) -> Self {
Self { selected_row }
}
}
impl Widget for WeatherTab {
fn render(self, area: Rect, buf: &mut Buffer) {
RgbSwatch.render(area, buf);
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 {
horizontal: 2,
vertical: 1,
});
let area = layout(area, Direction::Vertical, vec![0, 1, 1]);
render_gauges(self.selected_row, area[2], buf);
let area = layout(area[0], Direction::Horizontal, vec![23, 0]);
render_calendar(area[0], buf);
let area = layout(area[1], Direction::Horizontal, vec![29, 0]);
render_simple_barchart(area[0], buf);
render_horizontal_barchart(area[1], buf);
}
}
fn render_calendar(area: Rect, buf: &mut Buffer) {
let date = OffsetDateTime::now_utc().date();
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())
.render(area, buf);
}
fn render_simple_barchart(area: Rect, buf: &mut Buffer) {
let data = [
("Sat", 76),
("Sun", 69),
("Mon", 65),
("Tue", 67),
("Wed", 65),
("Thu", 69),
("Fri", 73),
];
let data = data
.into_iter()
.map(|(label, value)| {
Bar::default()
.value(value)
// This doesn't actually render correctly as the text is too wide for the bar
// See https://github.com/ratatui-org/ratatui/issues/513 for more info
// (the demo GIFs hack around this by hacking the calculation in bars.rs)
.text_value(format!("{}°", value))
.style(if value > 70 {
Style::new().fg(Color::Red)
} else {
Style::new().fg(Color::Yellow)
})
.value_style(if value > 70 {
Style::new().fg(Color::Gray).bg(Color::Red).bold()
} else {
Style::new().fg(Color::DarkGray).bg(Color::Yellow).bold()
})
.label(label.into())
})
.collect_vec();
let group = BarGroup::default().bars(&data);
BarChart::default()
.data(group)
.bar_width(3)
.bar_gap(1)
.render(area, buf);
}
fn render_horizontal_barchart(area: Rect, buf: &mut Buffer) {
let bg = Color::Rgb(32, 48, 96);
let data = [
Bar::default().text_value("Winter 37-51".into()).value(51),
Bar::default().text_value("Spring 40-65".into()).value(65),
Bar::default().text_value("Summer 54-77".into()).value(77),
Bar::default()
.text_value("Fall 41-71".into())
.value(71)
.value_style(Style::new().bold()), // current season
];
let group = BarGroup::default().label("GPU".into()).bars(&data);
BarChart::default()
.block(Block::new().padding(Padding::new(0, 0, 2, 0)))
.direction(Direction::Horizontal)
.data(group)
.bar_gap(1)
.bar_style(Style::new().fg(bg))
.value_style(Style::new().bg(bg).fg(Color::Gray))
.render(area, buf);
}
pub fn render_gauges(progress: usize, area: Rect, buf: &mut Buffer) {
let percent = (progress * 3).min(100) as f64;
render_line_gauge(percent, area, buf);
}
fn render_line_gauge(percent: f64, area: Rect, buf: &mut Buffer) {
// cycle color hue based on the percent for a neat effect yellow -> red
let hue = 90.0 - (percent as f32 * 0.6);
let value = Okhsv::max_value();
let fg = color_from_oklab(hue, Okhsv::max_saturation(), value);
let bg = color_from_oklab(hue, Okhsv::max_saturation(), value * 0.5);
let label = if percent < 100.0 {
format!("Downloading: {}%", percent)
} else {
"Download Complete!".into()
};
LineGauge::default()
.ratio(percent / 100.0)
.label(label)
.style(Style::new().light_blue())
.gauge_style(Style::new().fg(fg).bg(bg))
.line_set(symbols::line::THICK)
.render(area, buf);
}

71
examples/demo2/term.rs Normal file
View File

@@ -0,0 +1,71 @@
use std::{
io::{self, stdout, Stdout},
ops::{Deref, DerefMut},
time::Duration,
};
use anyhow::{Context, Result};
use crossterm::{
event::{self, Event},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
};
use ratatui::prelude::*;
/// A wrapper around the terminal that handles setting up and tearing down the terminal
/// and provides a helper method to read events from the terminal.
#[derive(Debug)]
pub struct Term {
terminal: Terminal<CrosstermBackend<Stdout>>,
}
impl Term {
pub fn start() -> Result<Self> {
// this size is to match the size of the terminal when running the demo
// using vhs in a 1280x640 sized window (github social preview size)
let options = TerminalOptions {
viewport: Viewport::Fixed(Rect::new(0, 0, 81, 18)),
};
let terminal = Terminal::with_options(CrosstermBackend::new(io::stdout()), options)?;
enable_raw_mode().context("enable raw mode")?;
stdout()
.execute(EnterAlternateScreen)
.context("enter alternate screen")?;
Ok(Self { terminal })
}
pub fn stop() -> Result<()> {
disable_raw_mode().context("disable raw mode")?;
stdout()
.execute(LeaveAlternateScreen)
.context("leave alternate screen")?;
Ok(())
}
pub fn next_event(timeout: Duration) -> io::Result<Option<Event>> {
if !event::poll(timeout)? {
return Ok(None);
}
let event = event::read()?;
Ok(Some(event))
}
}
impl Deref for Term {
type Target = Terminal<CrosstermBackend<Stdout>>;
fn deref(&self) -> &Self::Target {
&self.terminal
}
}
impl DerefMut for Term {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.terminal
}
}
impl Drop for Term {
fn drop(&mut self) {
let _ = Term::stop();
}
}

136
examples/demo2/theme.rs Normal file
View File

@@ -0,0 +1,136 @@
use ratatui::prelude::*;
pub struct Theme {
pub root: Style,
pub content: Style,
pub app_title: Style,
pub tabs: Style,
pub tabs_selected: Style,
pub borders: Style,
pub description: Style,
pub description_title: Style,
pub key_binding: KeyBinding,
pub logo: Logo,
pub email: Email,
pub traceroute: Traceroute,
pub recipe: Recipe,
}
pub struct KeyBinding {
pub key: Style,
pub description: Style,
}
pub struct Logo {
pub rat: Color,
pub rat_eye: Color,
pub rat_eye_alt: Color,
pub term: Color,
}
pub struct Email {
pub tabs: Style,
pub tabs_selected: Style,
pub inbox: Style,
pub item: Style,
pub selected_item: Style,
pub header: Style,
pub header_value: Style,
pub body: Style,
}
pub struct Traceroute {
pub header: Style,
pub selected: Style,
pub ping: Style,
pub map: Map,
}
pub struct Map {
pub style: Style,
pub color: Color,
pub path: Color,
pub source: Color,
pub destination: Color,
pub background_color: Color,
}
pub struct Recipe {
pub ingredients: Style,
pub ingredients_header: Style,
}
pub const THEME: Theme = Theme {
root: Style::new().bg(DARK_BLUE),
content: Style::new().bg(DARK_BLUE).fg(LIGHT_GRAY),
app_title: Style::new()
.fg(WHITE)
.bg(DARK_BLUE)
.add_modifier(Modifier::BOLD),
tabs: Style::new().fg(MID_GRAY).bg(DARK_BLUE),
tabs_selected: Style::new()
.fg(WHITE)
.bg(DARK_BLUE)
.add_modifier(Modifier::BOLD)
.add_modifier(Modifier::REVERSED),
borders: Style::new().fg(LIGHT_GRAY),
description: Style::new().fg(LIGHT_GRAY).bg(DARK_BLUE),
description_title: Style::new().fg(LIGHT_GRAY).add_modifier(Modifier::BOLD),
logo: Logo {
rat: WHITE,
rat_eye: BLACK,
rat_eye_alt: RED,
term: BLACK,
},
key_binding: KeyBinding {
key: Style::new().fg(BLACK).bg(DARK_GRAY),
description: Style::new().fg(DARK_GRAY).bg(BLACK),
},
email: Email {
tabs: Style::new().fg(MID_GRAY).bg(DARK_BLUE),
tabs_selected: Style::new()
.fg(WHITE)
.bg(DARK_BLUE)
.add_modifier(Modifier::BOLD),
inbox: Style::new().bg(DARK_BLUE).fg(LIGHT_GRAY),
item: Style::new().fg(LIGHT_GRAY),
selected_item: Style::new().fg(LIGHT_YELLOW),
header: Style::new().add_modifier(Modifier::BOLD),
header_value: Style::new().fg(LIGHT_GRAY),
body: Style::new().bg(DARK_BLUE).fg(LIGHT_GRAY),
},
traceroute: Traceroute {
header: Style::new()
.bg(DARK_BLUE)
.add_modifier(Modifier::BOLD)
.add_modifier(Modifier::UNDERLINED),
selected: Style::new().fg(LIGHT_YELLOW),
ping: Style::new().fg(WHITE),
map: Map {
style: Style::new().bg(DARK_BLUE),
background_color: DARK_BLUE,
color: LIGHT_GRAY,
path: LIGHT_BLUE,
source: LIGHT_GREEN,
destination: LIGHT_RED,
},
},
recipe: Recipe {
ingredients: Style::new().bg(DARK_BLUE).fg(LIGHT_GRAY),
ingredients_header: Style::new()
.add_modifier(Modifier::BOLD)
.add_modifier(Modifier::UNDERLINED),
},
};
const DARK_BLUE: Color = Color::Rgb(16, 24, 48);
const LIGHT_BLUE: Color = Color::Rgb(64, 96, 192);
const LIGHT_YELLOW: Color = Color::Rgb(192, 192, 96);
const LIGHT_GREEN: Color = Color::Rgb(64, 192, 96);
const LIGHT_RED: Color = Color::Rgb(192, 96, 96);
const RED: Color = Color::Indexed(160);
const BLACK: Color = Color::Indexed(232); // not really black, often #080808
const DARK_GRAY: Color = Color::Indexed(238);
const MID_GRAY: Color = Color::Indexed(244);
const LIGHT_GRAY: Color = Color::Indexed(250);
const WHITE: Color = Color::Indexed(255); // not really white, often #eeeeee

126
examples/docsrs.rs Normal file
View File

@@ -0,0 +1,126 @@
use std::io::{self, stdout};
use crossterm::{
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
};
use ratatui::{prelude::*, widgets::*};
/// Example code for libr.rs
///
/// When cargo-rdme supports doc comments that import from code, this will be imported
/// rather than copied to the lib.rs file.
fn main() -> io::Result<()> {
let arg = std::env::args().nth(1).unwrap_or_default();
enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
let mut should_quit = false;
while !should_quit {
terminal.draw(match arg.as_str() {
"hello_world" => hello_world,
"layout" => layout,
"styling" => styling,
_ => hello_world,
})?;
should_quit = handle_events()?;
}
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
Ok(())
}
fn hello_world(frame: &mut Frame) {
frame.render_widget(
Paragraph::new("Hello World!")
.block(Block::default().title("Greeting").borders(Borders::ALL)),
frame.size(),
);
}
use crossterm::event::{self, Event, KeyCode};
fn handle_events() -> io::Result<bool> {
if event::poll(std::time::Duration::from_millis(50))? {
if let Event::Key(key) = event::read()? {
if key.kind == event::KeyEventKind::Press && key.code == KeyCode::Char('q') {
return Ok(true);
}
}
}
Ok(false)
}
fn layout(frame: &mut Frame) {
let main_layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![
Constraint::Length(1),
Constraint::Min(0),
Constraint::Length(1),
])
.split(frame.size());
frame.render_widget(
Block::new().borders(Borders::TOP).title("Title Bar"),
main_layout[0],
);
frame.render_widget(
Block::new().borders(Borders::TOP).title("Status Bar"),
main_layout[2],
);
let inner_layout = Layout::default()
.direction(Direction::Horizontal)
.constraints(vec![Constraint::Percentage(50), Constraint::Percentage(50)])
.split(main_layout[1]);
frame.render_widget(
Block::default().borders(Borders::ALL).title("Left"),
inner_layout[0],
);
frame.render_widget(
Block::default().borders(Borders::ALL).title("Right"),
inner_layout[1],
);
}
fn styling(frame: &mut Frame) {
let areas = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
Constraint::Min(0),
])
.split(frame.size());
let span1 = Span::raw("Hello ");
let span2 = Span::styled(
"World",
Style::new()
.fg(Color::Green)
.bg(Color::White)
.add_modifier(Modifier::BOLD),
);
let span3 = "!".red().on_light_yellow().italic();
let line = Line::from(vec![span1, span2, span3]);
let text: Text = Text::from(vec![line]);
frame.render_widget(Paragraph::new(text), areas[0]);
// or using the short-hand syntax and implicit conversions
frame.render_widget(
Paragraph::new("Hello World!".red().on_white().bold()),
areas[1],
);
// to style the whole widget instead of just the text
frame.render_widget(
Paragraph::new("Hello World!").style(Style::new().red().on_white()),
areas[2],
);
// or using the short-hand syntax
frame.render_widget(Paragraph::new("Hello World!").blue().on_yellow(), areas[3]);
}

39
examples/docsrs.tape Normal file
View File

@@ -0,0 +1,39 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/demo.tape`
# NOTE: Requires VHS 0.6.1 or later for Screenshot support
Output "target/docsrs.gif"
Set Theme "OceanicMaterial"
# The reason for this strange size is that the social preview image for this
# demo is 1280x64 with 80 pixels of padding on each side. We want a version
# without the padding for README.md, etc.
Set Width 640
Set Height 160
Set Padding 0
Hide
Type "cargo run --example docsrs --features crossterm"
Enter
Sleep 2s
Show
Sleep 1s
Screenshot "target/docsrs-hello.png"
Sleep 1s
Hide
Type "q"
Type "cargo run --example docsrs --features crossterm -- layout"
Enter
Sleep 2s
Show
Sleep 1s
Screenshot "target/docsrs-layout.png"
Sleep 1s
Hide
Type "q"
Type "cargo run --example docsrs --features crossterm -- styling"
Enter
Sleep 2s
Show
Sleep 1s
Screenshot "target/docsrs-styling.png"
Sleep 1s
Hide
Type "q"

View File

@@ -103,7 +103,7 @@ fn run_app<B: Backend>(
}
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
fn ui(f: &mut Frame, app: &App) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(

View File

@@ -61,7 +61,7 @@ fn run(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
/// Render the application. This is where you would draw the application UI. This example just
/// draws a greeting.
fn render_app(frame: &mut ratatui::Frame<CrosstermBackend<Stdout>>) {
fn render_app(frame: &mut Frame) {
let greeting = Paragraph::new("Hello World! (press 'q' to quit)");
frame.render_widget(greeting, frame.size());
}

View File

@@ -216,7 +216,7 @@ fn run_app<B: Backend>(
Ok(())
}
fn ui<B: Backend>(f: &mut Frame<B>, downloads: &Downloads) {
fn ui(f: &mut Frame, downloads: &Downloads) {
let size = f.size();
let block = Block::default().title(block::Title::from("Progress").alignment(Alignment::Center));

View File

@@ -37,7 +37,7 @@ fn main() -> Result<(), Box<dyn Error>> {
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
loop {
terminal.draw(|f| ui(f))?;
terminal.draw(ui)?;
if let Event::Key(key) = event::read()? {
if let KeyCode::Char('q') = key.code {
@@ -47,7 +47,7 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
}
}
fn ui<B: Backend>(frame: &mut Frame<B>) {
fn ui(frame: &mut Frame) {
let main_layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![
@@ -169,8 +169,8 @@ fn ui<B: Backend>(frame: &mut Frame<B>) {
}
/// Renders a single example box
fn render_example_combination<B: Backend>(
frame: &mut Frame<B>,
fn render_example_combination(
frame: &mut Frame,
area: Rect,
title: &str,
constraints: Vec<(Constraint, Constraint)>,
@@ -195,11 +195,7 @@ fn render_example_combination<B: Backend>(
}
/// Renders a single example line
fn render_single_example<B: Backend>(
frame: &mut Frame<B>,
area: Rect,
constraints: Vec<Constraint>,
) {
fn render_single_example(frame: &mut Frame, area: Rect, constraints: Vec<Constraint>) {
let red = Paragraph::new(constraint_label(constraints[0])).on_red();
let blue = Paragraph::new(constraint_label(constraints[1])).on_blue();
let green = Paragraph::new("·".repeat(12)).on_green();

View File

@@ -198,7 +198,7 @@ fn run_app<B: Backend>(
}
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
fn ui(f: &mut Frame, app: &mut App) {
// Create two chunks with equal horizontal screen space
let chunks = Layout::default()
.direction(Direction::Horizontal)

View File

@@ -43,7 +43,7 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
}
}
fn ui<B: Backend>(frame: &mut Frame<B>) {
fn ui(frame: &mut Frame) {
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![Constraint::Length(1), Constraint::Min(0)])

View File

@@ -102,7 +102,7 @@ fn run_tui<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) -> io::Result<
}
/// Render the TUI.
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
fn ui(f: &mut Frame, app: &App) {
let text = vec![
if app.hook_enabled {
Line::from("HOOK IS CURRENTLY **ENABLED**")

View File

@@ -81,7 +81,7 @@ fn run_app<B: Backend>(
}
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
fn ui(f: &mut Frame, app: &App) {
let size = f.size();
// Words made "loooong" to demonstrate line breaking.

View File

@@ -61,7 +61,7 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
}
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
fn ui(f: &mut Frame, app: &App) {
let size = f.size();
let chunks = Layout::default()

View File

@@ -94,7 +94,7 @@ fn run_app<B: Backend>(
}
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
fn ui(f: &mut Frame, app: &mut App) {
let size = f.size();
// Words made "loooong" to demonstrate line breaking.

View File

@@ -126,7 +126,7 @@ fn run_app<B: Backend>(
}
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
fn ui(f: &mut Frame, app: &App) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(

View File

@@ -113,7 +113,7 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
}
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
fn ui(f: &mut Frame, app: &mut App) {
let rects = Layout::default()
.constraints([Constraint::Percentage(100)].as_ref())
.split(f.size());

View File

@@ -78,7 +78,7 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
}
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
fn ui(f: &mut Frame, app: &App) {
let size = f.size();
let chunks = Layout::default()
.direction(Direction::Vertical)
@@ -98,12 +98,8 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
let tabs = Tabs::new(titles)
.block(Block::default().borders(Borders::ALL).title("Tabs"))
.select(app.index)
.style(Style::default().fg(Color::Cyan))
.highlight_style(
Style::default()
.add_modifier(Modifier::BOLD)
.bg(Color::Black),
);
.style(Style::default().cyan().on_gray())
.highlight_style(Style::default().bold().on_black());
f.render_widget(tabs, chunks[0]);
let inner = match app.index {
0 => Block::default().title("Inner 0").borders(Borders::ALL),

View File

@@ -171,7 +171,7 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
}
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
fn ui(f: &mut Frame, app: &App) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(

View File

@@ -91,6 +91,7 @@
//!
//! [`TermionBackend`]: termion/struct.TermionBackend.html
//! [`Terminal`]: crate::terminal::Terminal
//! [`TermionBackend`]: termion/struct.TermionBackend.html
//! [Crossterm]: https://crates.io/crates/crossterm
//! [Termion]: https://crates.io/crates/termion
//! [Termwiz]: https://crates.io/crates/termwiz

View File

@@ -6,11 +6,10 @@ use std::{
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
#[allow(deprecated)]
use crate::{
layout::Rect,
style::{Color, Modifier, Style},
text::{Line, Span, Spans},
text::{Line, Span},
};
/// A buffer cell
@@ -334,29 +333,6 @@ impl Buffer {
(x_offset as u16, y)
}
#[allow(deprecated)]
#[deprecated(note = "Use `Buffer::set_line` instead")]
pub fn set_spans(&mut self, x: u16, y: u16, spans: &Spans<'_>, width: u16) -> (u16, u16) {
let mut remaining_width = width;
let mut x = x;
for span in &spans.0 {
if remaining_width == 0 {
break;
}
let pos = self.set_stringn(
x,
y,
span.content.as_ref(),
remaining_width as usize,
span.style,
);
let w = pos.0.saturating_sub(x);
x = pos.0;
remaining_width = remaining_width.saturating_sub(w);
}
(x, y)
}
pub fn set_line(&mut self, x: u16, y: u16, line: &Line<'_>, width: u16) -> (u16, u16) {
let mut remaining_width = width;
let mut x = x;

View File

@@ -1,12 +1,4 @@
use std::{
cell::RefCell,
cmp::{max, min},
collections::HashMap,
fmt,
num::NonZeroUsize,
rc::Rc,
sync::OnceLock,
};
use std::{cell::RefCell, collections::HashMap, fmt, num::NonZeroUsize, rc::Rc, sync::OnceLock};
use cassowary::{
strength::{MEDIUM, REQUIRED, STRONG, WEAK},
@@ -33,6 +25,9 @@ pub enum Direction {
Vertical,
}
mod rect;
pub use rect::*;
/// Constraints to apply
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub enum Constraint {
@@ -165,113 +160,6 @@ pub enum Alignment {
Right,
}
/// A simple rectangle used in the computation of the layout and to give widgets a hint about the
/// area they are supposed to render to.
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Rect {
pub x: u16,
pub y: u16,
pub width: u16,
pub height: u16,
}
impl fmt::Display for Rect {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}x{}+{}+{}", self.width, self.height, self.x, self.y)
}
}
impl Rect {
/// Creates a new rect, with width and height limited to keep the area under max u16.
/// If clipped, aspect ratio will be preserved.
pub fn new(x: u16, y: u16, width: u16, height: u16) -> Rect {
let max_area = u16::max_value();
let (clipped_width, clipped_height) =
if u32::from(width) * u32::from(height) > u32::from(max_area) {
let aspect_ratio = f64::from(width) / f64::from(height);
let max_area_f = f64::from(max_area);
let height_f = (max_area_f / aspect_ratio).sqrt();
let width_f = height_f * aspect_ratio;
(width_f as u16, height_f as u16)
} else {
(width, height)
};
Rect {
x,
y,
width: clipped_width,
height: clipped_height,
}
}
pub const fn area(self) -> u16 {
self.width * self.height
}
pub const fn left(self) -> u16 {
self.x
}
pub const fn right(self) -> u16 {
self.x.saturating_add(self.width)
}
pub const fn top(self) -> u16 {
self.y
}
pub const fn bottom(self) -> u16 {
self.y.saturating_add(self.height)
}
pub fn inner(self, margin: &Margin) -> Rect {
if self.width < 2 * margin.horizontal || self.height < 2 * margin.vertical {
Rect::default()
} else {
Rect {
x: self.x + margin.horizontal,
y: self.y + margin.vertical,
width: self.width - 2 * margin.horizontal,
height: self.height - 2 * margin.vertical,
}
}
}
pub fn union(self, other: Rect) -> Rect {
let x1 = min(self.x, other.x);
let y1 = min(self.y, other.y);
let x2 = max(self.x + self.width, other.x + other.width);
let y2 = max(self.y + self.height, other.y + other.height);
Rect {
x: x1,
y: y1,
width: x2 - x1,
height: y2 - y1,
}
}
pub fn intersection(self, other: Rect) -> Rect {
let x1 = max(self.x, other.x);
let y1 = max(self.y, other.y);
let x2 = min(self.x + self.width, other.x + other.width);
let y2 = min(self.y + self.height, other.y + other.height);
Rect {
x: x1,
y: y1,
width: x2 - x1,
height: y2 - y1,
}
}
pub const fn intersects(self, other: Rect) -> bool {
self.x < other.x + other.width
&& self.x + self.width > other.x
&& self.y < other.y + other.height
&& self.y + self.height > other.y
}
}
#[derive(Debug, Default, Display, EnumString, Clone, Eq, PartialEq, Hash)]
pub(crate) enum SegmentSize {
EvenDistribution,
@@ -307,7 +195,7 @@ pub(crate) enum SegmentSize {
/// ```rust
/// use ratatui::{prelude::*, widgets::*};
///
/// fn render<B: Backend>(frame: &mut Frame<B>, area: Rect) {
/// fn render(frame: &mut Frame, area: Rect) {
/// let layout = Layout::default()
/// .direction(Direction::Vertical)
/// .constraints(vec![Constraint::Length(5), Constraint::Min(0)])
@@ -805,79 +693,6 @@ mod tests {
assert_eq!("".parse::<Alignment>(), Err(ParseError::VariantNotFound));
}
#[test]
fn rect_to_string() {
assert_eq!(Rect::new(1, 2, 3, 4).to_string(), "3x4+1+2");
}
#[test]
fn rect_new() {
assert_eq!(
Rect::new(1, 2, 3, 4),
Rect {
x: 1,
y: 2,
width: 3,
height: 4
}
);
}
#[test]
fn rect_area() {
assert_eq!(Rect::new(1, 2, 3, 4).area(), 12);
}
#[test]
fn rect_left() {
assert_eq!(Rect::new(1, 2, 3, 4).left(), 1);
}
#[test]
fn rect_right() {
assert_eq!(Rect::new(1, 2, 3, 4).right(), 4);
}
#[test]
fn rect_top() {
assert_eq!(Rect::new(1, 2, 3, 4).top(), 2);
}
#[test]
fn rect_bottom() {
assert_eq!(Rect::new(1, 2, 3, 4).bottom(), 6);
}
#[test]
fn rect_inner() {
assert_eq!(
Rect::new(1, 2, 3, 4).inner(&Margin::new(1, 2)),
Rect::new(2, 4, 1, 0)
);
}
#[test]
fn rect_union() {
assert_eq!(
Rect::new(1, 2, 3, 4).union(Rect::new(2, 3, 4, 5)),
Rect::new(1, 2, 5, 6)
);
}
#[test]
fn rect_intersection() {
assert_eq!(
Rect::new(1, 2, 3, 4).intersection(Rect::new(2, 3, 4, 5)),
Rect::new(2, 3, 2, 3)
);
}
#[test]
fn rect_intersects() {
assert!(Rect::new(1, 2, 3, 4).intersects(Rect::new(2, 3, 4, 5)));
assert!(!Rect::new(1, 2, 3, 4).intersects(Rect::new(5, 6, 7, 8)));
}
#[test]
fn segment_size_to_string() {
assert_eq!(
@@ -994,50 +809,6 @@ mod tests {
);
}
#[test]
fn test_rect_size_truncation() {
for width in 256u16..300u16 {
for height in 256u16..300u16 {
let rect = Rect::new(0, 0, width, height);
rect.area(); // Should not panic.
assert!(rect.width < width || rect.height < height);
// The target dimensions are rounded down so the math will not be too precise
// but let's make sure the ratios don't diverge crazily.
assert!(
(f64::from(rect.width) / f64::from(rect.height)
- f64::from(width) / f64::from(height))
.abs()
< 1.0
);
}
}
// One dimension below 255, one above. Area above max u16.
let width = 900;
let height = 100;
let rect = Rect::new(0, 0, width, height);
assert_ne!(rect.width, 900);
assert_ne!(rect.height, 100);
assert!(rect.width < width || rect.height < height);
}
#[test]
fn test_rect_size_preservation() {
for width in 0..256u16 {
for height in 0..256u16 {
let rect = Rect::new(0, 0, width, height);
rect.area(); // Should not panic.
assert_eq!(rect.width, width);
assert_eq!(rect.height, height);
}
}
// One dimension below 255, one above. Area below max u16.
let rect = Rect::new(0, 0, 300, 100);
assert_eq!(rect.width, 300);
assert_eq!(rect.height, 100);
}
#[test]
fn test_constraint_apply() {
assert_eq!(Constraint::Percentage(0).apply(100), 0);
@@ -1075,22 +846,6 @@ mod tests {
assert_eq!(Constraint::Min(u16::MAX).apply(100), u16::MAX);
}
#[test]
fn rect_can_be_const() {
const RECT: Rect = Rect {
x: 0,
y: 0,
width: 10,
height: 10,
};
const _AREA: u16 = RECT.area();
const _LEFT: u16 = RECT.left();
const _RIGHT: u16 = RECT.right();
const _TOP: u16 = RECT.top();
const _BOTTOM: u16 = RECT.bottom();
assert!(RECT.intersects(RECT));
}
#[test]
fn layout_can_be_const() {
const _LAYOUT: Layout = Layout::new();

291
src/layout/rect.rs Normal file
View File

@@ -0,0 +1,291 @@
#![warn(missing_docs)]
use std::{
cmp::{max, min},
fmt,
};
use crate::prelude::*;
/// A simple rectangle used in the computation of the layout and to give widgets a hint about the
/// area they are supposed to render to.
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Rect {
/// The x coordinate of the top left corner of the rect.
pub x: u16,
/// The y coordinate of the top left corner of the rect.
pub y: u16,
/// The width of the rect.
pub width: u16,
/// The height of the rect.
pub height: u16,
}
impl fmt::Display for Rect {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}x{}+{}+{}", self.width, self.height, self.x, self.y)
}
}
impl Rect {
/// Creates a new rect, with width and height limited to keep the area under max u16. If
/// clipped, aspect ratio will be preserved.
pub fn new(x: u16, y: u16, width: u16, height: u16) -> Rect {
let max_area = u16::max_value();
let (clipped_width, clipped_height) =
if u32::from(width) * u32::from(height) > u32::from(max_area) {
let aspect_ratio = f64::from(width) / f64::from(height);
let max_area_f = f64::from(max_area);
let height_f = (max_area_f / aspect_ratio).sqrt();
let width_f = height_f * aspect_ratio;
(width_f as u16, height_f as u16)
} else {
(width, height)
};
Rect {
x,
y,
width: clipped_width,
height: clipped_height,
}
}
/// The area of the rect. If the area is larger than the maximum value of u16, it will be
/// clamped to u16::MAX.
pub const fn area(self) -> u16 {
self.width.saturating_mul(self.height)
}
/// Returns true if the rect has no area.
pub const fn is_empty(self) -> bool {
self.width == 0 || self.height == 0
}
/// Returns the left coordinate of the rect.
pub const fn left(self) -> u16 {
self.x
}
/// Returns the right coordinate of the rect. This is the first coordinate outside of the rect.
///
/// If the right coordinate is larger than the maximum value of u16, it will be clamped to
/// u16::MAX.
pub const fn right(self) -> u16 {
self.x.saturating_add(self.width)
}
/// Returns the top coordinate of the rect.
pub const fn top(self) -> u16 {
self.y
}
/// Returns the bottom coordinate of the rect. This is the first coordinate outside of the rect.
///
/// If the bottom coordinate is larger than the maximum value of u16, it will be clamped to
/// u16::MAX.
pub const fn bottom(self) -> u16 {
self.y.saturating_add(self.height)
}
/// 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.
pub fn inner(self, margin: &Margin) -> Rect {
let doubled_margin_horizontal = margin.horizontal.saturating_mul(2);
let doubled_margin_vertical = margin.vertical.saturating_mul(2);
if self.width < doubled_margin_horizontal || self.height < doubled_margin_vertical {
Rect::default()
} else {
Rect {
x: self.x.saturating_add(margin.horizontal),
y: self.y.saturating_add(margin.vertical),
width: self.width.saturating_sub(doubled_margin_horizontal),
height: self.height.saturating_sub(doubled_margin_vertical),
}
}
}
/// Returns a new rect that contains both the current one and the given one.
pub fn union(self, other: Rect) -> Rect {
let x1 = min(self.x, other.x);
let y1 = min(self.y, other.y);
let x2 = max(self.right(), other.right());
let y2 = max(self.bottom(), other.bottom());
Rect {
x: x1,
y: y1,
width: x2.saturating_sub(x1),
height: y2.saturating_sub(y1),
}
}
/// Returns a new rect that is the intersection of the current one and the given one.
///
/// If the two rects do not intersect, the returned rect will have no area.
pub fn intersection(self, other: Rect) -> Rect {
let x1 = max(self.x, other.x);
let y1 = max(self.y, other.y);
let x2 = min(self.right(), other.right());
let y2 = min(self.bottom(), other.bottom());
Rect {
x: x1,
y: y1,
width: x2 - x1,
height: y2 - y1,
}
}
/// Returns true if the two rects intersect.
pub const fn intersects(self, other: Rect) -> bool {
self.x < other.right()
&& self.right() > other.x
&& self.y < other.bottom()
&& self.bottom() > other.y
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn to_string() {
assert_eq!(Rect::new(1, 2, 3, 4).to_string(), "3x4+1+2");
}
#[test]
fn new() {
assert_eq!(
Rect::new(1, 2, 3, 4),
Rect {
x: 1,
y: 2,
width: 3,
height: 4
}
);
}
#[test]
fn area() {
assert_eq!(Rect::new(1, 2, 3, 4).area(), 12);
}
#[test]
fn is_empty() {
assert!(!Rect::new(1, 2, 3, 4).is_empty());
assert!(Rect::new(1, 2, 0, 4).is_empty());
assert!(Rect::new(1, 2, 3, 0).is_empty());
}
#[test]
fn left() {
assert_eq!(Rect::new(1, 2, 3, 4).left(), 1);
}
#[test]
fn right() {
assert_eq!(Rect::new(1, 2, 3, 4).right(), 4);
}
#[test]
fn top() {
assert_eq!(Rect::new(1, 2, 3, 4).top(), 2);
}
#[test]
fn bottom() {
assert_eq!(Rect::new(1, 2, 3, 4).bottom(), 6);
}
#[test]
fn inner() {
assert_eq!(
Rect::new(1, 2, 3, 4).inner(&Margin::new(1, 2)),
Rect::new(2, 4, 1, 0)
);
}
#[test]
fn union() {
assert_eq!(
Rect::new(1, 2, 3, 4).union(Rect::new(2, 3, 4, 5)),
Rect::new(1, 2, 5, 6)
);
}
#[test]
fn intersection() {
assert_eq!(
Rect::new(1, 2, 3, 4).intersection(Rect::new(2, 3, 4, 5)),
Rect::new(2, 3, 2, 3)
);
}
#[test]
fn intersects() {
assert!(Rect::new(1, 2, 3, 4).intersects(Rect::new(2, 3, 4, 5)));
assert!(!Rect::new(1, 2, 3, 4).intersects(Rect::new(5, 6, 7, 8)));
}
#[test]
fn size_truncation() {
for width in 256u16..300u16 {
for height in 256u16..300u16 {
let rect = Rect::new(0, 0, width, height);
rect.area(); // Should not panic.
assert!(rect.width < width || rect.height < height);
// The target dimensions are rounded down so the math will not be too precise
// but let's make sure the ratios don't diverge crazily.
assert!(
(f64::from(rect.width) / f64::from(rect.height)
- f64::from(width) / f64::from(height))
.abs()
< 1.0
);
}
}
// One dimension below 255, one above. Area above max u16.
let width = 900;
let height = 100;
let rect = Rect::new(0, 0, width, height);
assert_ne!(rect.width, 900);
assert_ne!(rect.height, 100);
assert!(rect.width < width || rect.height < height);
}
#[test]
fn size_preservation() {
for width in 0..256u16 {
for height in 0..256u16 {
let rect = Rect::new(0, 0, width, height);
rect.area(); // Should not panic.
assert_eq!(rect.width, width);
assert_eq!(rect.height, height);
}
}
// One dimension below 255, one above. Area below max u16.
let rect = Rect::new(0, 0, 300, 100);
assert_eq!(rect.width, 300);
assert_eq!(rect.height, 100);
}
#[test]
fn can_be_const() {
const RECT: Rect = Rect {
x: 0,
y: 0,
width: 10,
height: 10,
};
const _AREA: u16 = RECT.area();
const _LEFT: u16 = RECT.left();
const _RIGHT: u16 = RECT.right();
const _TOP: u16 = RECT.top();
const _BOTTOM: u16 = RECT.bottom();
assert!(RECT.intersects(RECT));
}
}

View File

@@ -1,179 +1,341 @@
#![forbid(unsafe_code)]
//! [ratatui](https://github.com/ratatui-org/ratatui) is a library that is all about cooking up terminal user
//! interfaces (TUIs).
//! ![Demo](https://raw.githubusercontent.com/ratatui-org/ratatui/aa09e59dc0058347f68d7c1e0c91f863c6f2b8c9/examples/demo2.gif)
//!
//! ![Demo](https://vhs.charm.sh/vhs-tF0QbuPbtHgUeG0sTVgFr.gif)
//! <div align="center">
//!
//! # Get started
//! [![Crate Badge]](https://crates.io/crates/ratatui) [![License Badge]](./LICENSE) [![CI
//! Badge]](https://github.com/ratatui-org/ratatui/actions?query=workflow%3ACI+) [![Docs
//! Badge]](https://docs.rs/crate/ratatui/)<br>
//! [![Dependencies Badge]](https://deps.rs/repo/github/ratatui-org/ratatui) [![Codecov
//! Badge]](https://app.codecov.io/gh/ratatui-org/ratatui) [![Discord
//! Badge]](https://discord.gg/pMCEU9hNEj) [![Matrix
//! Badge]](https://matrix.to/#/#ratatui:matrix.org)<br>
//! [Documentation](https://docs.rs/ratatui) · [Ratatui Book](https://ratatui.rs) ·
//! [Examples](https://github.com/ratatui-org/ratatui/tree/main/examples) · [Report a
//! bug](https://github.com/ratatui-org/ratatui/issues/new?labels=bug&projects=&template=bug_report.md)
//! · [Request a
//! Feature](https://github.com/ratatui-org/ratatui/issues/new?labels=enhancement&projects=&template=feature_request.md)
//! · [Send a Pull Request](https://github.com/ratatui-org/ratatui/compare)
//!
//! ## Adding `ratatui` as a dependency
//! </div>
//!
//! Add the following to your `Cargo.toml`:
//! ```toml
//! [dependencies]
//! crossterm = "0.27"
//! ratatui = "0.23"
//! # Ratatui
//!
//! [Ratatui] is a crate for cooking up terminal user interfaces in rust. It is a lightweight
//! library that provides a set of widgets and utilities to build complex rust TUIs. Ratatui was
//! forked from the [Tui-rs crate] in 2023 in order to continue its development.
//!
//! ## Installation
//!
//! Add `ratatui` and `crossterm` as dependencies to your cargo.toml:
//!
//! ```shell
//! cargo add ratatui crossterm
//! ```
//!
//! The crate is using the `crossterm` backend by default that works on most platforms. But if for
//! example you want to use the `termion` backend instead. This can be done by changing your
//! dependencies specification to the following:
//! Ratatui uses [Crossterm] by default as it works on most platforms. See the [Installation]
//! section of the [Ratatui Book] for more details on how to use other backends ([Termion] /
//! [Termwiz]).
//!
//! ```toml
//! [dependencies]
//! termion = "2.0.1"
//! ratatui = { version = "0.23", default-features = false, features = ['termion'] }
//! ```
//! ## Introduction
//!
//! The same logic applies for all other available backends.
//! Ratatui is based on the principle of immediate rendering with intermediate buffers. This means
//! that for each frame, your app must render all widgets that are supposed to be part of the UI.
//! This is in contrast to the retained mode style of rendering where widgets are updated and then
//! automatically redrawn on the next frame. See the [Rendering] section of the [Ratatui Book] for
//! more info.
//!
//! ## Creating a `Terminal`
//! ## Other documentation
//!
//! Every application using `ratatui` should start by instantiating a `Terminal`. It is a light
//! abstraction over available backends that provides basic functionalities such as clearing the
//! screen, hiding the cursor, etc.
//! - [Ratatui Book] - explains the library's concepts and provides step-by-step tutorials
//! - [Examples] - a collection of examples that demonstrate how to use the library.
//! - [API Documentation] - the full API documentation for the library on docs.rs.
//! - [Changelog] - generated by [git-cliff] utilizing [Conventional Commits].
//! - [Contributing] - Please read this if you are interested in contributing to the project.
//! - [Breaking Changes] - a list of breaking changes in the library.
//!
//! ## Quickstart
//!
//! The following example demonstrates the minimal amount of code necessary to setup a terminal and
//! render "Hello World!". The full code for this example which contains a little more detail is in
//! [hello_world.rs]. For more guidance on different ways to structure your application see the
//! [Application Patterns] and [Hello World tutorial] sections in the [Ratatui Book] and the various
//! [Examples]. There are also several starter templates available:
//!
//! - [rust-tui-template]
//! - [ratatui-async-template] (book and template)
//! - [simple-tui-rs]
//!
//! Every application built with `ratatui` needs to implement the following steps:
//!
//! - Initialize the terminal
//! - A main loop to:
//! - Handle input events
//! - Draw the UI
//! - Restore the terminal state
//!
//! The library contains a [`prelude`] module that re-exports the most commonly used traits and
//! types for convenience. Most examples in the documentation will use this instead of showing the
//! full path of each type.
//!
//! ### Initialize and restore the terminal
//!
//! The [`Terminal`] type is the main entry point for any Ratatui application. It is a light
//! abstraction over a choice of [`Backend`] implementations that provides functionality to draw
//! each frame, clear the screen, hide the cursor, etc. It is parametrized over any type that
//! implements the [`Backend`] trait which has implementations for [Crossterm], [Termion] and
//! [Termwiz].
//!
//! Most applications should enter the Alternate Screen when starting and leave it when exiting and
//! also enable raw mode to disable line buffering and enable reading key events. See the [`backend`
//! module] and the [Backends] section of the [Ratatui Book] for more info.
//!
//! ### Drawing the UI
//!
//! The drawing logic is delegated to a closure that takes a [`Frame`] instance as argument. The
//! [`Frame`] provides the size of the area to draw to and allows the app to render any [`Widget`]
//! using the provided [`render_widget`] method. See the [Widgets] section of the [Ratatui Book] for
//! more info.
//!
//! ### Handling events
//!
//! Ratatui does not include any input handling. Instead event handling can be implemented by
//! calling backend library methods directly. See the [Handling Events] section of the [Ratatui
//! Book] for more info. For example, if you are using [Crossterm], you can use the
//! [`crossterm::event`] module to handle events.
//!
//! ### Example
//!
//! ```rust,no_run
//! use std::io;
//! use ratatui::prelude::*;
//!
//! fn main() -> Result<(), io::Error> {
//! let stdout = io::stdout();
//! let backend = CrosstermBackend::new(stdout);
//! let mut terminal = Terminal::new(backend)?;
//! Ok(())
//! }
//! ```
//!
//! If you had previously chosen `termion` as a backend, the terminal can be created in a similar
//! way:
//!
//! ```rust,ignore
//! use std::io;
//! use ratatui::prelude::*;
//! use termion::raw::IntoRawMode;
//!
//! fn main() -> Result<(), io::Error> {
//! let stdout = io::stdout().into_raw_mode()?;
//! let backend = TermionBackend::new(stdout);
//! let mut terminal = Terminal::new(backend)?;
//! Ok(())
//! }
//! ```
//!
//! You may also refer to the examples to find out how to create a `Terminal` for each available
//! backend.
//!
//! ## Building a User Interface (UI)
//!
//! Every component of your interface will be implementing the `Widget` trait. The library comes
//! with a predefined set of widgets that should meet most of your use cases. You are also free to
//! implement your own.
//!
//! Each widget follows a builder pattern API providing a default configuration along with methods
//! to customize them. The widget is then rendered using [`Frame::render_widget`] which takes your
//! widget instance and an area to draw to.
//!
//! The following example renders a block of the size of the terminal:
//!
//! ```rust,no_run
//! use std::{io, thread, time::Duration};
//! use ratatui::{prelude::*, widgets::*};
//! use std::io::{self, stdout};
//! use crossterm::{
//! event::{self, DisableMouseCapture, EnableMouseCapture},
//! execute,
//! terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
//! event::{self, Event, KeyCode},
//! ExecutableCommand,
//! terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}
//! };
//! use ratatui::{prelude::*, widgets::*};
//!
//! fn main() -> Result<(), io::Error> {
//! // setup terminal
//! fn main() -> io::Result<()> {
//! enable_raw_mode()?;
//! let mut stdout = io::stdout();
//! execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
//! let backend = CrosstermBackend::new(stdout);
//! let mut terminal = Terminal::new(backend)?;
//! stdout().execute(EnterAlternateScreen)?;
//! let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
//!
//! terminal.draw(|f| {
//! let size = f.size();
//! let block = Block::default()
//! .title("Block")
//! .borders(Borders::ALL);
//! f.render_widget(block, size);
//! })?;
//! let mut should_quit = false;
//! while !should_quit {
//! terminal.draw(ui)?;
//! should_quit = handle_events()?;
//! }
//!
//! // Start a thread to discard any input events. Without handling events, the
//! // stdin buffer will fill up, and be read into the shell when the program exits.
//! thread::spawn(|| loop {
//! event::read();
//! });
//!
//! thread::sleep(Duration::from_millis(5000));
//!
//! // restore terminal
//! disable_raw_mode()?;
//! execute!(
//! terminal.backend_mut(),
//! LeaveAlternateScreen,
//! DisableMouseCapture
//! )?;
//! terminal.show_cursor()?;
//!
//! stdout().execute(LeaveAlternateScreen)?;
//! Ok(())
//! }
//!
//! fn handle_events() -> io::Result<bool> {
//! if event::poll(std::time::Duration::from_millis(50))? {
//! if let Event::Key(key) = event::read()? {
//! if key.kind == event::KeyEventKind::Press && key.code == KeyCode::Char('q') {
//! return Ok(true);
//! }
//! }
//! }
//! Ok(false)
//! }
//!
//! fn ui(frame: &mut Frame) {
//! frame.render_widget(
//! Paragraph::new("Hello World!")
//! .block(Block::default().title("Greeting").borders(Borders::ALL)),
//! frame.size(),
//! );
//! }
//! ```
//!
//! Running this example produces the following output:
//!
//! ![docsrs-hello]
//!
//! ## Layout
//!
//! The library comes with a basic yet useful layout management object called `Layout`. As you may
//! see below and in the examples, the library makes heavy use of the builder pattern to provide
//! full customization. And `Layout` is no exception:
//! The library comes with a basic yet useful layout management object called [`Layout`] which
//! allows you to split the available space into multiple areas and then render widgets in each
//! area. This lets you describe a responsive terminal UI by nesting layouts. See the [Layout]
//! section of the [Ratatui Book] for more info.
//!
//! ```rust,no_run
//! use ratatui::{prelude::*, widgets::*};
//!
//! fn ui<B: Backend>(f: &mut Frame<B>) {
//! let chunks = Layout::default()
//! fn ui(frame: &mut Frame) {
//! let main_layout = Layout::default()
//! .direction(Direction::Vertical)
//! .margin(1)
//! .constraints(
//! [
//! Constraint::Percentage(10),
//! Constraint::Percentage(80),
//! Constraint::Percentage(10)
//! ].as_ref()
//! )
//! .split(f.size());
//! let block = Block::default()
//! .title("Block")
//! .borders(Borders::ALL);
//! f.render_widget(block, chunks[0]);
//! let block = Block::default()
//! .title("Block 2")
//! .borders(Borders::ALL);
//! f.render_widget(block, chunks[1]);
//! .constraints(vec![
//! Constraint::Length(1),
//! Constraint::Min(0),
//! Constraint::Length(1),
//! ])
//! .split(frame.size());
//! frame.render_widget(
//! Block::new().borders(Borders::TOP).title("Title Bar"),
//! main_layout[0],
//! );
//! frame.render_widget(
//! Block::new().borders(Borders::TOP).title("Status Bar"),
//! main_layout[2],
//! );
//!
//! let inner_layout = Layout::default()
//! .direction(Direction::Horizontal)
//! .constraints(vec![Constraint::Percentage(50), Constraint::Percentage(50)])
//! .split(main_layout[1]);
//! frame.render_widget(
//! Block::default().borders(Borders::ALL).title("Left"),
//! inner_layout[0],
//! );
//! frame.render_widget(
//! Block::default().borders(Borders::ALL).title("Right"),
//! inner_layout[1],
//! );
//! }
//! ```
//!
//! This let you describe responsive terminal UI by nesting layouts. You should note that by default
//! the computed layout tries to fill the available space completely. So if for any reason you might
//! need a blank space somewhere, try to pass an additional constraint and don't use the
//! corresponding area.
//! Running this example produces the following output:
//!
//! # Features
//! ![docsrs-layout]
//!
//! ## Text and styling
//!
//! The [`Text`], [`Line`] and [`Span`] types are the building blocks of the library and are used in
//! many places. [`Text`] is a list of [`Line`]s and a [`Line`] is a list of [`Span`]s. A [`Span`]
//! is a string with a specific style.
//!
//! The [`style` module] provides types that represent the various styling options. The most
//! important one is [`Style`] which represents the foreground and background colors and the text
//! attributes of a [`Span`]. The [`style` module] also provides a [`Stylize`] trait that allows
//! short-hand syntax to apply a style to widgets and text. See the [Styling Text] section of the
//! [Ratatui Book] for more info.
//!
//! ```rust,no_run
//! use ratatui::{prelude::*, widgets::*};
//!
//! fn ui(frame: &mut Frame) {
//! let areas = Layout::default()
//! .direction(Direction::Vertical)
//! .constraints(vec![
//! Constraint::Length(1),
//! Constraint::Length(1),
//! Constraint::Length(1),
//! Constraint::Length(1),
//! Constraint::Min(0),
//! ])
//! .split(frame.size());
//!
//! let span1 = Span::raw("Hello ");
//! let span2 = Span::styled(
//! "World",
//! Style::new()
//! .fg(Color::Green)
//! .bg(Color::White)
//! .add_modifier(Modifier::BOLD),
//! );
//! let span3 = "!".red().on_light_yellow().italic();
//!
//! let line = Line::from(vec![span1, span2, span3]);
//! let text: Text = Text::from(vec![line]);
//!
//! frame.render_widget(Paragraph::new(text), areas[0]);
//! // or using the short-hand syntax and implicit conversions
//! frame.render_widget(
//! Paragraph::new("Hello World!".red().on_white().bold()),
//! areas[1],
//! );
//!
//! // to style the whole widget instead of just the text
//! frame.render_widget(
//! Paragraph::new("Hello World!").style(Style::new().red().on_white()),
//! areas[2],
//! );
//! // or using the short-hand syntax
//! frame.render_widget(Paragraph::new("Hello World!").blue().on_yellow(), areas[3]);
//! }
//! ```
//!
//! Running this example produces the following output:
//!
//! ![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 Book]: https://ratatui.rs
//! [Installation]: https://ratatui.rs/installation.html
//! [Rendering]: https://ratatui.rs/concepts/rendering/index.html
//! [Application Patterns]: https://ratatui.rs/concepts/application_patterns/index.html
//! [Hello World tutorial]: https://ratatui.rs/tutorial/hello_world.html
//! [Backends]: https://ratatui.rs/concepts/backends/index.html
//! [Widgets]: https://ratatui.rs/how-to/widgets/index.html
//! [Handling Events]: https://ratatui.rs/concepts/event_handling.html
//! [Layout]: https://ratatui.rs/how-to/layout/index.html
//! [Styling Text]: https://ratatui.rs/how-to/render/style-text.html
//! [rust-tui-template]: https://github.com/ratatui-org/rust-tui-template
//! [ratatui-async-template]: https://ratatui-org.github.io/ratatui-async-template/
//! [simple-tui-rs]: https://github.com/pmsanford/simple-tui-rs
//! [Examples]: https://github.com/ratatui-org/ratatui/tree/main/examples
//! [git-cliff]: https://github.com/orhun/git-cliff
//! [Conventional Commits]: https://www.conventionalcommits.org
//! [API Documentation]: https://docs.rs/ratatui
//! [Changelog]: https://github.com/ratatui-org/ratatui/blob/main/CHANGELOG.md
//! [Contributing]: https:://github.com/ratatui-org/ratatui/blob/main/CONTRIBUTING.md
//! [Breaking Changes]: https:://github.com/ratatui-org/ratatui/blob/main/BREAKING-CHANGES.md
//! [docsrs-hello]: https://github.com/ratatui-org/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-hello.png?raw=true
//! [docsrs-layout]: https://github.com/ratatui-org/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-layout.png?raw=true
//! [docsrs-styling]: https://github.com/ratatui-org/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-styling.png?raw=true
//! [`Frame`]: terminal::Frame
//! [`render_widget`]: terminal::Frame::render_widget
//! [`Widget`]: widgets::Widget
//! [`Layout`]: layout::Layout
//! [`backend`]: backend
//! [`calendar`]: widgets::calendar
//! [`CrosstermBackend`]: backend::CrosstermBackend
//! [`TermionBackend`]: backend::TermionBackend
//! [`TermwizBackend`]: backend::TermwizBackend
//! [Crossterm crate]: https://crates.io/crates/crossterm
//! [Serde crate]: https://crates.io/crates/serde
//! [Termion crate]: https://crates.io/crates/termion
//! [Termwiz crate]: https://crates.io/crates/termwiz
//! [Time crate]: https://crates.io/crates/time
//! [`Text`]: text::Text
//! [`Line`]: text::Line
//! [`Span`]: text::Span
//! [`Style`]: style::Style
//! [`style` module]: style
//! [`Stylize`]: style::Stylize
//! [`Backend`]: backend::Backend
//! [`backend` module]: backend
//! [`crossterm::event`]: https://docs.rs/crossterm/latest/crossterm/event/index.html
//! [Ratatui]: https://ratatui.rs
//! [Crossterm]: https://crates.io/crates/crossterm
//! [Termion]: https://crates.io/crates/termion
//! [Termwiz]: https://crates.io/crates/termwiz
//! [Tui-rs crate]: https://crates.io/crates/tui
//! [hello_world.rs]: https://github.com/ratatui-org/ratatui/blob/main/examples/hello_world.rs
//! [Crate Badge]: https://img.shields.io/crates/v/ratatui?logo=rust&style=flat-square
//! [CI Badge]:
//! https://img.shields.io/github/actions/workflow/status/ratatui-org/ratatui/ci.yml?style=flat-square&logo=github
//! [Codecov Badge]:
//! https://img.shields.io/codecov/c/github/ratatui-org/ratatui?logo=codecov&style=flat-square&token=BAQ8SOKEST
//! [Dependencies Badge]: https://deps.rs/repo/github/ratatui-org/ratatui/status.svg?style=flat-square
//! [Discord Badge]:
//! https://img.shields.io/discord/1070692720437383208?label=discord&logo=discord&style=flat-square
//! [Docs Badge]: https://img.shields.io/docsrs/ratatui?logo=rust&style=flat-square
//! [License Badge]: https://img.shields.io/crates/l/ratatui?style=flat-square
//! [Matrix Badge]:
//! https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix
// show the feature flags in the generated documentation
#![cfg_attr(docsrs, feature(doc_auto_cfg))]

View File

@@ -157,7 +157,7 @@ pub mod line {
pub const DOUBLE_CROSS: &str = "";
pub const THICK_CROSS: &str = "";
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub struct Set {
pub vertical: &'static str,
pub horizontal: &'static str,
@@ -229,6 +229,154 @@ pub mod line {
};
}
pub mod border {
use super::line;
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub struct Set {
pub top_left: &'static str,
pub top_right: &'static str,
pub bottom_left: &'static str,
pub bottom_right: &'static str,
pub vertical_left: &'static str,
pub vertical_right: &'static str,
pub horizontal_top: &'static str,
pub horizontal_bottom: &'static str,
}
impl Default for Set {
fn default() -> Self {
PLAIN
}
}
/// Border Set with a single line width
///
/// ```text
/// ┌─────┐
/// │xxxxx│
/// │xxxxx│
/// └─────┘
pub const PLAIN: Set = Set {
top_left: line::NORMAL.top_left,
top_right: line::NORMAL.top_right,
bottom_left: line::NORMAL.bottom_left,
bottom_right: line::NORMAL.bottom_right,
vertical_left: line::NORMAL.vertical,
vertical_right: line::NORMAL.vertical,
horizontal_top: line::NORMAL.horizontal,
horizontal_bottom: line::NORMAL.horizontal,
};
/// Border Set with a single line width and rounded corners
///
/// ```text
/// ╭─────╮
/// │xxxxx│
/// │xxxxx│
/// ╰─────╯
pub const ROUNDED: Set = Set {
top_left: line::ROUNDED.top_left,
top_right: line::ROUNDED.top_right,
bottom_left: line::ROUNDED.bottom_left,
bottom_right: line::ROUNDED.bottom_right,
vertical_left: line::ROUNDED.vertical,
vertical_right: line::ROUNDED.vertical,
horizontal_top: line::ROUNDED.horizontal,
horizontal_bottom: line::ROUNDED.horizontal,
};
/// Border Set with a double line width
///
/// ```text
/// ╔═════╗
/// ║xxxxx║
/// ║xxxxx║
/// ╚═════╝
pub const DOUBLE: Set = Set {
top_left: line::DOUBLE.top_left,
top_right: line::DOUBLE.top_right,
bottom_left: line::DOUBLE.bottom_left,
bottom_right: line::DOUBLE.bottom_right,
vertical_left: line::DOUBLE.vertical,
vertical_right: line::DOUBLE.vertical,
horizontal_top: line::DOUBLE.horizontal,
horizontal_bottom: line::DOUBLE.horizontal,
};
/// Border Set with a thick line width
///
/// ```text
/// ┏━━━━━┓
/// ┃xxxxx┃
/// ┃xxxxx┃
/// ┗━━━━━┛
pub const THICK: Set = Set {
top_left: line::THICK.top_left,
top_right: line::THICK.top_right,
bottom_left: line::THICK.bottom_left,
bottom_right: line::THICK.bottom_right,
vertical_left: line::THICK.vertical,
vertical_right: line::THICK.vertical,
horizontal_top: line::THICK.horizontal,
horizontal_bottom: line::THICK.horizontal,
};
pub const QUADRANT_TOP_LEFT: &str = "";
pub const QUADRANT_TOP_RIGHT: &str = "";
pub const QUADRANT_BOTTOM_LEFT: &str = "";
pub const QUADRANT_BOTTOM_RIGHT: &str = "";
pub const QUADRANT_TOP_HALF: &str = "";
pub const QUADRANT_BOTTOM_HALF: &str = "";
pub const QUADRANT_LEFT_HALF: &str = "";
pub const QUADRANT_RIGHT_HALF: &str = "";
pub const QUADRANT_TOP_LEFT_BOTTOM_LEFT_BOTTOM_RIGHT: &str = "";
pub const QUADRANT_TOP_LEFT_TOP_RIGHT_BOTTOM_LEFT: &str = "";
pub const QUADRANT_TOP_LEFT_TOP_RIGHT_BOTTOM_RIGHT: &str = "";
pub const QUADRANT_TOP_RIGHT_BOTTOM_LEFT_BOTTOM_RIGHT: &str = "";
pub const QUADRANT_TOP_LEFT_BOTTOM_RIGHT: &str = "";
pub const QUADRANT_TOP_RIGHT_BOTTOM_LEFT: &str = "";
pub const QUADRANT_BLOCK: &str = "";
/// Quadrant used for setting a border outside a block by one half cell "pixel".
///
/// ```text
/// ▛▀▀▀▀▀▜
/// ▌xxxxx▐
/// ▌xxxxx▐
/// ▙▄▄▄▄▄▟
/// ```
pub const QUADRANT_OUTSIDE: Set = Set {
top_left: QUADRANT_TOP_LEFT_TOP_RIGHT_BOTTOM_LEFT,
top_right: QUADRANT_TOP_LEFT_TOP_RIGHT_BOTTOM_RIGHT,
bottom_left: QUADRANT_TOP_LEFT_BOTTOM_LEFT_BOTTOM_RIGHT,
bottom_right: QUADRANT_TOP_RIGHT_BOTTOM_LEFT_BOTTOM_RIGHT,
vertical_left: QUADRANT_LEFT_HALF,
vertical_right: QUADRANT_RIGHT_HALF,
horizontal_top: QUADRANT_TOP_HALF,
horizontal_bottom: QUADRANT_BOTTOM_HALF,
};
/// Quadrant used for setting a border inside a block by one half cell "pixel".
///
/// ```text
/// ▗▄▄▄▄▄▖
/// ▐xxxxx▌
/// ▐xxxxx▌
/// ▝▀▀▀▀▀▘
/// ```
pub const QUADRANT_INSIDE: Set = Set {
top_right: QUADRANT_BOTTOM_LEFT,
top_left: QUADRANT_BOTTOM_RIGHT,
bottom_right: QUADRANT_TOP_LEFT,
bottom_left: QUADRANT_TOP_RIGHT,
vertical_left: QUADRANT_RIGHT_HALF,
vertical_right: QUADRANT_LEFT_HALF,
horizontal_top: QUADRANT_BOTTOM_HALF,
horizontal_bottom: QUADRANT_TOP_HALF,
};
}
pub const DOT: &str = "";
pub mod braille {

View File

@@ -81,7 +81,7 @@ pub struct TerminalOptions {
pub viewport: Viewport,
}
/// An interface that Ratatui to interact and draw [`Frame`]s on the user's terminal.
/// An interface to interact and draw [`Frame`]s on the user's terminal.
///
/// This is the main entry point for Ratatui. It is responsible for drawing and maintaining the
/// state of the buffers, cursor and viewport.
@@ -91,9 +91,11 @@ pub struct TerminalOptions {
/// terminal libraries: [Crossterm], [Termion] and [Termwiz]. See the [`backend`] module for more
/// information.
///
/// The terminal has two buffers that are used to draw the application. The first buffer is the
/// current buffer and the second buffer is the previous buffer. The two buffers are compared at
/// the end of each draw pass to output only the changes to the terminal.
/// The `Terminal` struct maintains two buffers: the current and the previous.
/// When the widgets are drawn, the changes are accumulated in the current buffer.
/// At the end of each draw pass, the two buffers are compared, and only the changes
/// between these buffers are written to the terminal, avoiding any redundant operations.
/// After flushing these changes, the buffers are swapped to prepare for the next draw cycle./
///
/// The terminal also has a viewport which is the area of the terminal that is currently visible to
/// the user. It can be either fullscreen, inline or fixed. See [`Viewport`] for more information.
@@ -225,10 +227,11 @@ where
}
/// Get a Frame object which provides a consistent view into the terminal state for rendering.
pub fn get_frame(&mut self) -> Frame<B> {
pub fn get_frame(&mut self) -> Frame {
Frame {
terminal: self,
cursor_position: None,
viewport_area: self.viewport_area,
buffer: self.current_buffer_mut(),
}
}
@@ -321,7 +324,7 @@ where
/// ```
pub fn draw<F>(&mut self, f: F) -> io::Result<CompletedFrame>
where
F: FnOnce(&mut Frame<B>),
F: FnOnce(&mut Frame),
{
// Autoresize - otherwise we get glitches if shrinking or potential desync between widgets
// and the terminal (if growing), which may OOB.
@@ -331,7 +334,7 @@ where
f(&mut frame);
// We can't change the cursor position right away because we have to flush the frame to
// stdout first. But we also can't keep the frame around, since it holds a &mut to
// Terminal. Thus, we're taking the important data out of the Frame and dropping it.
// Buffer. Thus, we're taking the important data out of the Frame and dropping it.
let cursor_position = frame.cursor_position;
// Draw to stdout
@@ -545,10 +548,9 @@ fn compute_inline_size<B: Backend>(
/// This is obtained via the closure argument of [`Terminal::draw`]. It is used to render widgets
/// to the terminal and control the cursor position.
///
/// The changes drawn to the frame are not immediately applied to the terminal. They are only
/// applied after the closure returns. This allows for widgets to be drawn in any order. The
/// changes are then compared to the previous frame and only the necessary updates are applied to
/// the terminal.
/// The changes drawn to the frame are applied only to the current buffer.
/// After the closure returns, the current buffer is compared to the previous
/// buffer and only the changes are applied to the terminal.
///
/// The [`Frame`] is generic over a [`Backend`] implementation which is used to interface with the
/// underlying terminal library. The [`Backend`] trait is implemented for three popular Rust
@@ -562,29 +564,25 @@ fn compute_inline_size<B: Backend>(
/// [`Backend`]: crate::backend::Backend
/// [`Buffer`]: crate::buffer::Buffer
#[derive(Debug, Hash)]
pub struct Frame<'a, B: 'a>
where
B: Backend,
{
/// The terminal that this frame is associated with.
terminal: &'a mut Terminal<B>,
pub struct Frame<'a> {
/// Where should the cursor be after drawing this frame?
///
/// If `None`, the cursor is hidden and its position is controlled by the backend. If `Some((x,
/// y))`, the cursor is shown and placed at `(x, y)` after the call to `Terminal::draw()`.
cursor_position: Option<(u16, u16)>,
/// The area of the viewport
viewport_area: Rect,
/// The buffer that is used to draw the current frame
buffer: &'a mut Buffer,
}
impl<'a, B> Frame<'a, B>
where
B: Backend,
{
impl Frame<'_> {
/// The size of the current frame
///
/// This is guaranteed not to change when rendering.
pub fn size(&self) -> Rect {
self.terminal.viewport_area
self.viewport_area
}
/// Render a [`Widget`] to the current buffer using [`Widget::render`].
@@ -609,7 +607,7 @@ where
where
W: Widget,
{
widget.render(area, self.terminal.current_buffer_mut());
widget.render(area, self.buffer);
}
/// Render a [`StatefulWidget`] to the current buffer using [`StatefulWidget::render`].
@@ -641,7 +639,7 @@ where
where
W: StatefulWidget,
{
widget.render(area, self.terminal.current_buffer_mut(), state);
widget.render(area, self.buffer, state);
}
/// After drawing this frame, make the cursor visible and put it at the specified (x, y)

View File

@@ -60,11 +60,6 @@ pub use masked::Masked;
mod span;
pub use span::Span;
/// We keep this for backward compatibility.
mod spans;
#[allow(deprecated)]
pub use spans::Spans;
#[allow(clippy::module_inception)]
mod text;
pub use text::Text;

View File

@@ -1,7 +1,6 @@
#![allow(deprecated)]
use std::borrow::Cow;
use super::{Span, Spans, Style, StyledGrapheme};
use super::{Span, Style, StyledGrapheme};
use crate::layout::Alignment;
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
@@ -11,6 +10,29 @@ pub struct Line<'a> {
}
impl<'a> Line<'a> {
/// Create a line with the default style.
///
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// Line::raw("test content");
/// Line::raw(String::from("test content"));
/// ```
pub fn raw<T>(content: T) -> Line<'a>
where
T: Into<Cow<'a, str>>,
{
Line {
spans: content
.into()
.lines()
.map(|v| Span::raw(v.to_string()))
.collect(),
alignment: None,
}
}
/// Create a line with a style.
///
/// # Examples
@@ -180,18 +202,12 @@ impl<'a> From<Line<'a>> for String {
}
}
impl<'a> From<Spans<'a>> for Line<'a> {
fn from(value: Spans<'a>) -> Self {
Self::from(value.0)
}
}
#[cfg(test)]
mod tests {
use crate::{
layout::Alignment,
style::{Color, Modifier, Style},
text::{Line, Span, Spans, StyledGrapheme},
text::{Line, Span, StyledGrapheme},
};
#[test]
@@ -266,15 +282,6 @@ mod tests {
assert_eq!(vec![span], line.spans);
}
#[test]
fn test_from_spans() {
let spans = vec![
Span::styled("Hello,", Style::default().fg(Color::Red)),
Span::styled(" world!", Style::default().fg(Color::Green)),
];
assert_eq!(Line::from(Spans::from(spans.clone())), Line::from(spans));
}
#[test]
fn test_into_string() {
let line = Line::from(vec![
@@ -323,4 +330,15 @@ mod tests {
],
);
}
#[test]
fn raw_str() {
let line = Line::raw("test content");
assert_eq!(line.spans, vec![Span::raw("test content")]);
assert_eq!(line.alignment, None);
let line = Line::raw("a\nb");
assert_eq!(line.spans, vec![Span::raw("a"), Span::raw("b")]);
assert_eq!(line.alignment, None);
}
}

View File

@@ -1,223 +0,0 @@
#![allow(deprecated)]
use super::{Span, Style};
use crate::{layout::Alignment, text::Line};
/// A string composed of clusters of graphemes, each with their own style.
///
/// `Spans` has been deprecated in favor of `Line`, and will be removed in the
/// future. All methods that accept Spans have been replaced with methods that
/// accept Into<Line<'a>> (which is implemented on `Spans`) to allow users of
/// this crate to gradually transition to Line.
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
#[deprecated(note = "Use `ratatui::text::Line` instead")]
pub struct Spans<'a>(pub Vec<Span<'a>>);
impl<'a> Spans<'a> {
/// Returns the width of the underlying string.
///
/// ## Examples
///
/// ```rust
/// use ratatui::{prelude::*, text::Spans};
///
/// let spans = Spans::from(vec![
/// Span::styled("My", Style::default().fg(Color::Yellow)),
/// Span::raw(" text"),
/// ]);
/// assert_eq!(7, spans.width());
/// ```
pub fn width(&self) -> usize {
self.0.iter().map(Span::width).sum()
}
/// Patches the style of each Span in an existing Spans, adding modifiers from the given style.
///
/// ## Examples
///
/// ```rust
/// use ratatui::{prelude::*, text::Spans};
///
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
/// let mut raw_spans = Spans::from(vec![
/// Span::raw("My"),
/// Span::raw(" text"),
/// ]);
/// let mut styled_spans = Spans::from(vec![
/// Span::styled("My", style),
/// Span::styled(" text", style),
/// ]);
///
/// assert_ne!(raw_spans, styled_spans);
///
/// raw_spans.patch_style(style);
/// assert_eq!(raw_spans, styled_spans);
/// ```
pub fn patch_style(&mut self, style: Style) {
for span in &mut self.0 {
span.patch_style(style);
}
}
/// Resets the style of each Span in the Spans.
/// Equivalent to calling `patch_style(Style::reset())`.
///
/// ## Examples
///
/// ```rust
/// use ratatui::{prelude::*, text::Spans};
///
/// let mut spans = Spans::from(vec![
/// Span::styled("My", Style::default().fg(Color::Yellow)),
/// Span::styled(" text", Style::default().add_modifier(Modifier::BOLD)),
/// ]);
///
/// spans.reset_style();
/// assert_eq!(Style::reset(), spans.0[0].style);
/// assert_eq!(Style::reset(), spans.0[1].style);
/// ```
pub fn reset_style(&mut self) {
for span in &mut self.0 {
span.reset_style();
}
}
/// Sets the target alignment for this line of text.
/// Defaults to: [`None`], meaning the alignment is determined by the rendering widget.
///
/// ## Examples
///
/// ```rust
/// use ratatui::{prelude::*, text::Spans};
///
/// let mut line = Spans::from("Hi, what's up?").alignment(Alignment::Right);
/// assert_eq!(Some(Alignment::Right), line.alignment)
/// ```
pub fn alignment(self, alignment: Alignment) -> Line<'a> {
let line = Line::from(self);
line.alignment(alignment)
}
}
impl<'a> From<String> for Spans<'a> {
fn from(s: String) -> Spans<'a> {
Spans(vec![Span::from(s)])
}
}
impl<'a> From<&'a str> for Spans<'a> {
fn from(s: &'a str) -> Spans<'a> {
Spans(vec![Span::from(s)])
}
}
impl<'a> From<Vec<Span<'a>>> for Spans<'a> {
fn from(spans: Vec<Span<'a>>) -> Spans<'a> {
Spans(spans)
}
}
impl<'a> From<Span<'a>> for Spans<'a> {
fn from(span: Span<'a>) -> Spans<'a> {
Spans(vec![span])
}
}
impl<'a> From<Spans<'a>> for String {
fn from(line: Spans<'a>) -> String {
line.0.iter().fold(String::new(), |mut acc, s| {
acc.push_str(s.content.as_ref());
acc
})
}
}
#[cfg(test)]
mod tests {
use crate::{
style::{Color, Modifier, Style},
text::{Span, Spans},
};
#[test]
fn test_width() {
let spans = Spans::from(vec![
Span::styled("My", Style::default().fg(Color::Yellow)),
Span::raw(" text"),
]);
assert_eq!(7, spans.width());
let empty_spans = Spans::default();
assert_eq!(0, empty_spans.width());
}
#[test]
fn test_patch_style() {
let style = Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::ITALIC);
let mut raw_spans = Spans::from(vec![Span::raw("My"), Span::raw(" text")]);
let styled_spans = Spans::from(vec![
Span::styled("My", style),
Span::styled(" text", style),
]);
assert_ne!(raw_spans, styled_spans);
raw_spans.patch_style(style);
assert_eq!(raw_spans, styled_spans);
}
#[test]
fn test_reset_style() {
let mut spans = Spans::from(vec![
Span::styled("My", Style::default().fg(Color::Yellow)),
Span::styled(" text", Style::default().add_modifier(Modifier::BOLD)),
]);
spans.reset_style();
assert_eq!(Style::reset(), spans.0[0].style);
assert_eq!(Style::reset(), spans.0[1].style);
}
#[test]
fn test_from_string() {
let s = String::from("Hello, world!");
let spans = Spans::from(s);
assert_eq!(vec![Span::from("Hello, world!")], spans.0);
}
#[test]
fn test_from_str() {
let s = "Hello, world!";
let spans = Spans::from(s);
assert_eq!(vec![Span::from("Hello, world!")], spans.0);
}
#[test]
fn test_from_vec() {
let spans_vec = vec![
Span::styled("Hello,", Style::default().fg(Color::Red)),
Span::styled(" world!", Style::default().fg(Color::Green)),
];
let spans = Spans::from(spans_vec.clone());
assert_eq!(spans_vec, spans.0);
}
#[test]
fn test_from_span() {
let span = Span::styled("Hello, world!", Style::default().fg(Color::Yellow));
let spans = Spans::from(span.clone());
assert_eq!(vec![span], spans.0);
}
#[test]
fn test_into_string() {
let spans = Spans::from(vec![
Span::styled("Hello,", Style::default().fg(Color::Red)),
Span::styled(" world!", Style::default().fg(Color::Green)),
]);
let s: String = spans.into();
assert_eq!("Hello, world!", s);
}
}

View File

@@ -1,7 +1,6 @@
use std::borrow::Cow;
#[allow(deprecated)]
use super::{Line, Span, Spans};
use super::{Line, Span};
use crate::style::Style;
/// A string split over multiple lines where each line is composed of several clusters, each with
@@ -172,30 +171,12 @@ impl<'a> From<Span<'a>> for Text<'a> {
}
}
#[allow(deprecated)]
impl<'a> From<Spans<'a>> for Text<'a> {
fn from(spans: Spans<'a>) -> Text<'a> {
Text {
lines: vec![spans.into()],
}
}
}
impl<'a> From<Line<'a>> for Text<'a> {
fn from(line: Line<'a>) -> Text<'a> {
Text { lines: vec![line] }
}
}
#[allow(deprecated)]
impl<'a> From<Vec<Spans<'a>>> for Text<'a> {
fn from(lines: Vec<Spans<'a>>) -> Text<'a> {
Text {
lines: lines.into_iter().map(|l| l.0.into()).collect(),
}
}
}
impl<'a> From<Vec<Line<'a>>> for Text<'a> {
fn from(lines: Vec<Line<'a>>) -> Text<'a> {
Text { lines }
@@ -332,42 +313,12 @@ mod tests {
);
}
#[test]
#[allow(deprecated)]
fn from_spans() {
let style = Style::new().yellow().italic();
let text = Text::from(Spans::from(vec![
Span::styled("The first line", style),
Span::styled("The second line", style),
]));
assert_eq!(
text.lines,
vec![Line::from(Spans::from(vec![
Span::styled("The first line", style),
Span::styled("The second line", style),
]))]
);
}
#[test]
fn from_line() {
let text = Text::from(Line::from("The first line"));
assert_eq!(text.lines, vec![Line::from("The first line")]);
}
#[test]
#[allow(deprecated)]
fn from_vec_spans() {
let text = Text::from(vec![
Spans::from("The first line"),
Spans::from("The second line"),
]);
assert_eq!(
text.lines,
vec![Line::from("The first line"), Line::from("The second line"),]
);
}
#[test]
fn from_vec_line() {
let text = Text::from(vec![

View File

@@ -283,46 +283,89 @@ impl<'a> BarChart<'a> {
}
}
impl<'a> BarChart<'a> {
/// Check the bars, which fits inside the available space and removes
/// the bars and the groups, which are outside of the available space.
fn remove_invisible_groups_and_bars(&mut self, mut width: u16) {
for group_index in 0..self.data.len() {
let n_bars = self.data[group_index].bars.len() as u16;
let group_width = n_bars * self.bar_width + n_bars.saturating_sub(1) * self.bar_gap;
struct LabelInfo {
group_label_visible: bool,
bar_label_visible: bool,
height: u16,
}
if width > group_width {
width = width.saturating_sub(group_width + self.group_gap + self.bar_gap);
} else {
let max_bars = (width + self.bar_gap) / (self.bar_width + self.bar_gap);
if max_bars == 0 {
self.data.truncate(group_index);
} else {
self.data[group_index].bars.truncate(max_bars as usize);
self.data.truncate(group_index + 1);
impl<'a> BarChart<'a> {
/// Returns the visible bars length in ticks. A cell contains 8 ticks.
/// `available_space` used to calculate how many bars can fit in the space
/// `bar_max_length` is the maximal length a bar can take.
fn group_ticks(&self, available_space: u16, bar_max_length: u16) -> Vec<Vec<u64>> {
let max: u64 = self.maximum_data_value();
self.data
.iter()
.scan(available_space, |space, group| {
if *space == 0 {
return None;
}
break;
}
}
let n_bars = group.bars.len() as u16;
let group_width = n_bars * self.bar_width + n_bars.saturating_sub(1) * self.bar_gap;
let n_bars = if *space > group_width {
*space = space.saturating_sub(group_width + self.group_gap + self.bar_gap);
Some(n_bars)
} else {
let max_bars = (*space + self.bar_gap) / (self.bar_width + self.bar_gap);
if max_bars > 0 {
*space = 0;
Some(max_bars)
} else {
None
}
};
n_bars.map(|n| {
group
.bars
.iter()
.take(n as usize)
.map(|bar| bar.value * u64::from(bar_max_length) * 8 / max)
.collect()
})
})
.collect()
}
/// Get the number of lines needed for the labels.
/// Get label information.
///
/// The number of lines depends on whether we need to print the bar labels and/or the group
/// labels.
/// - If there are no labels, return 0.
/// - If there are only bar labels, return 1.
/// - If there are only group labels, return 1.
/// - If there are both bar and group labels, return 2.
fn label_height(&self) -> u16 {
let has_group_labels = self.data.iter().any(|e| e.label.is_some());
let has_data_labels = self
/// height is the number of lines, which depends on whether we need to print the bar
/// labels and/or the group labels.
/// - If there are no labels, height is 0.
/// - If there are only bar labels, height is 1.
/// - If there are only group labels, height is 1.
/// - If there are both bar and group labels, height is 2.
fn label_info(&self, available_height: u16) -> LabelInfo {
if available_height == 0 {
return LabelInfo {
group_label_visible: false,
bar_label_visible: false,
height: 0,
};
}
let bar_label_visible = self
.data
.iter()
.any(|e| e.bars.iter().any(|e| e.label.is_some()));
// convert true to 1 and false to 0 and add the two values
u16::from(has_group_labels) + u16::from(has_data_labels)
if available_height == 1 && bar_label_visible {
return LabelInfo {
group_label_visible: false,
bar_label_visible: true,
height: 1,
};
}
let group_label_visible = self.data.iter().any(|e| e.label.is_some());
LabelInfo {
group_label_visible,
bar_label_visible,
// convert true to 1 and false to 0 and add the two values
height: u16::from(group_label_visible) + u16::from(bar_label_visible),
}
}
/// renders the block if there is one and updates the area to the inner area
@@ -334,26 +377,36 @@ impl<'a> BarChart<'a> {
}
}
fn render_horizontal_bars(self, buf: &mut Buffer, bars_area: Rect, max: u64) {
// convert the bar values to ratatui::symbols::bar::Set
let groups: Vec<Vec<u16>> = self
fn render_horizontal(self, buf: &mut Buffer, area: Rect) {
// get the longest label
let label_size = self
.data
.iter()
.map(|group| {
group
.bars
.iter()
.map(|bar| (bar.value * u64::from(bars_area.width) / max) as u16)
.collect()
})
.collect();
.flat_map(|group| group.bars.iter().map(|bar| &bar.label))
.flatten() // bar.label is an Option<Line>
.map(|label| label.width())
.max()
.unwrap_or(0) as u16;
// print all visible bars
let label_x = area.x;
let bars_area = {
let margin = if label_size == 0 { 0 } else { 1 };
Rect {
x: area.x + label_size + margin,
width: area.width - label_size - margin,
..area
}
};
let group_ticks = self.group_ticks(bars_area.height, bars_area.width);
// print all visible bars, label and values
let mut bar_y = bars_area.top();
for (group_data, mut group) in groups.into_iter().zip(self.data) {
for (ticks_vec, mut group) in group_ticks.into_iter().zip(self.data) {
let bars = std::mem::take(&mut group.bars);
for (bar_length, bar) in group_data.into_iter().zip(bars) {
for (ticks, bar) in ticks_vec.into_iter().zip(bars) {
let bar_length = (ticks / 8) as u16;
let bar_style = self.bar_style.patch(bar.style);
for y in 0..self.bar_width {
@@ -374,6 +427,12 @@ impl<'a> BarChart<'a> {
y: bar_y + (self.bar_width >> 1),
..bars_area
};
// label
if let Some(label) = &bar.label {
buf.set_line(label_x, bar_value_area.top(), label, label_size);
}
bar.render_value_with_different_styles(
buf,
bar_value_area,
@@ -399,26 +458,27 @@ impl<'a> BarChart<'a> {
}
}
fn render_vertical_bars(&self, buf: &mut Buffer, bars_area: Rect, max: u64) {
// convert the bar values to ratatui::symbols::bar::Set
let mut groups: Vec<Vec<u64>> = self
.data
.iter()
.map(|group| {
group
.bars
.iter()
.map(|bar| bar.value * u64::from(bars_area.height) * 8 / max)
.collect()
})
.collect();
fn render_vertical(self, buf: &mut Buffer, area: Rect) {
let label_info = self.label_info(area.height - 1);
let bars_area = Rect {
height: area.height - label_info.height,
..area
};
let group_ticks = self.group_ticks(bars_area.width, bars_area.height);
self.render_vertical_bars(bars_area, buf, &group_ticks);
self.render_labels_and_values(area, buf, label_info, &group_ticks);
}
fn render_vertical_bars(&self, area: Rect, buf: &mut Buffer, group_ticks: &[Vec<u64>]) {
// print all visible bars (without labels and values)
for j in (0..bars_area.height).rev() {
let mut bar_x = bars_area.left();
for (group_data, group) in groups.iter_mut().zip(&self.data) {
for (d, bar) in group_data.iter_mut().zip(&group.bars) {
let symbol = match d {
let mut bar_x = area.left();
for (ticks_vec, group) in group_ticks.iter().zip(&self.data) {
for (ticks, bar) in ticks_vec.iter().zip(&group.bars) {
let mut ticks = *ticks;
for j in (0..area.height).rev() {
let symbol = match ticks {
0 => self.bar_set.empty,
1 => self.bar_set.one_eighth,
2 => self.bar_set.one_quarter,
@@ -433,20 +493,16 @@ impl<'a> BarChart<'a> {
let bar_style = self.bar_style.patch(bar.style);
for x in 0..self.bar_width {
buf.get_mut(bar_x + x, bars_area.top() + j)
buf.get_mut(bar_x + x, area.top() + j)
.set_symbol(symbol)
.set_style(bar_style);
}
if *d > 8 {
*d -= 8;
} else {
*d = 0;
}
bar_x += self.bar_gap + self.bar_width;
ticks = ticks.saturating_sub(8);
}
bar_x += self.group_gap;
bar_x += self.bar_gap + self.bar_width;
}
bar_x += self.group_gap;
}
}
@@ -463,36 +519,42 @@ impl<'a> BarChart<'a> {
.max(1u64)
}
fn render_labels_and_values(self, area: Rect, buf: &mut Buffer, label_height: u16) {
fn render_labels_and_values(
self,
area: Rect,
buf: &mut Buffer,
label_info: LabelInfo,
group_ticks: &[Vec<u64>],
) {
// print labels and values in one go
let mut bar_x = area.left();
let bar_y = area.bottom() - label_height - 1;
for mut group in self.data.into_iter() {
let bar_y = area.bottom() - label_info.height - 1;
for (mut group, ticks_vec) in self.data.into_iter().zip(group_ticks) {
if group.bars.is_empty() {
continue;
}
let bars = std::mem::take(&mut group.bars);
// print group labels under the bars or the previous labels
let label_max_width =
bars.len() as u16 * (self.bar_width + self.bar_gap) - self.bar_gap;
let group_area = Rect {
x: bar_x,
y: area.bottom() - 1,
width: label_max_width,
height: 1,
};
group.render_label(buf, group_area, self.label_style);
if label_info.group_label_visible {
let label_max_width =
ticks_vec.len() as u16 * (self.bar_width + self.bar_gap) - self.bar_gap;
let group_area = Rect {
x: bar_x,
y: area.bottom() - 1,
width: label_max_width,
height: 1,
};
group.render_label(buf, group_area, self.label_style);
}
// print the bar values and numbers
for bar in bars.into_iter() {
bar.render_label_and_value(
buf,
self.bar_width,
bar_x,
bar_y,
self.value_style,
self.label_style,
);
for (mut bar, ticks) in bars.into_iter().zip(ticks_vec) {
if label_info.bar_label_visible {
bar.render_label(buf, self.bar_width, bar_x, bar_y + 1, self.label_style);
}
bar.render_value(buf, self.bar_width, bar_x, bar_y, self.value_style, *ticks);
bar_x += self.bar_gap + self.bar_width;
}
@@ -507,33 +569,13 @@ impl<'a> Widget for BarChart<'a> {
self.render_block(&mut area, buf);
if self.data.is_empty() {
if area.is_empty() || self.data.is_empty() || self.bar_width == 0 {
return;
}
let label_height = self.label_height();
if area.height <= label_height {
return;
}
let max = self.maximum_data_value();
match self.direction {
Direction::Horizontal => {
// remove invisible groups and bars, since we don't need to print them
self.remove_invisible_groups_and_bars(area.height);
self.render_horizontal_bars(buf, area, max);
}
Direction::Vertical => {
// remove invisible groups and bars, since we don't need to print them
self.remove_invisible_groups_and_bars(area.width);
let bars_area = Rect {
height: area.height - label_height,
..area
};
self.render_vertical_bars(buf, bars_area, max);
self.render_labels_and_values(area, buf, label_height);
}
Direction::Horizontal => self.render_horizontal(buf, area),
Direction::Vertical => self.render_vertical(buf, area),
}
}
}
@@ -576,7 +618,7 @@ mod tests {
buffer,
Buffer::with_lines(vec![
"",
"█ █ ",
"1 2 ",
"f b ",
])
);
@@ -598,7 +640,7 @@ mod tests {
Buffer::with_lines(vec![
"╔Block════════╗",
"║ █ ║",
"█ █",
"1 2",
"║f b ║",
"╚═════════════╝",
])
@@ -626,7 +668,7 @@ mod tests {
buffer,
Buffer::with_lines(vec![
" █ █ ",
"█ █",
"1 2",
"f b b ",
])
);
@@ -641,7 +683,7 @@ mod tests {
widget.render(buffer.area, &mut buffer);
let mut expected = Buffer::with_lines(vec![
"",
"█ █ ",
"1 2 ",
"f b ",
]);
for (x, y) in iproduct!([0, 2], [0, 1]) {
@@ -678,7 +720,7 @@ mod tests {
buffer,
Buffer::with_lines(vec![
"",
" ",
"1 2 ",
"f b ",
])
);
@@ -695,7 +737,7 @@ mod tests {
buffer,
Buffer::with_lines(vec![
"",
" ",
"3 ",
"f b b ",
])
);
@@ -722,7 +764,7 @@ mod tests {
buffer,
Buffer::with_lines(vec![
" ",
" ▁ ▂ ▃ ▄ ▅ ▆ ▇ ",
" ▁ ▂ ▃ ▄ ▅ ▆ ▇ 8 ",
"a b c d e f g h i ",
])
);
@@ -755,7 +797,7 @@ mod tests {
widget.render(buffer.area, &mut buffer);
let mut expected = Buffer::with_lines(vec![
"",
"█ █ ",
"1 2 ",
"f b ",
]);
expected.get_mut(0, 2).set_fg(Color::Red);
@@ -772,7 +814,7 @@ mod tests {
widget.render(buffer.area, &mut buffer);
let mut expected = Buffer::with_lines(vec![
"",
"█ █ ",
"1 2 ",
"f b ",
]);
for (x, y) in iproduct!(0..15, 0..3) {
@@ -781,166 +823,6 @@ mod tests {
assert_buffer_eq!(buffer, expected);
}
#[test]
fn does_not_render_less_than_two_rows() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 1));
let widget = BarChart::default().data(&[("foo", 1), ("bar", 2)]);
widget.render(buffer.area, &mut buffer);
assert_buffer_eq!(buffer, Buffer::empty(Rect::new(0, 0, 15, 1)));
}
fn create_test_barchart<'a>() -> BarChart<'a> {
BarChart::default()
.group_gap(2)
.data(BarGroup::default().label("G1".into()).bars(&[
Bar::default().value(2),
Bar::default().value(1),
Bar::default().value(2),
]))
.data(BarGroup::default().label("G2".into()).bars(&[
Bar::default().value(1),
Bar::default().value(2),
Bar::default().value(1),
]))
.data(BarGroup::default().label("G3".into()).bars(&[
Bar::default().value(1),
Bar::default().value(2),
Bar::default().value(1),
]))
}
#[test]
fn test_invisible_groups_and_bars_full() {
let chart = create_test_barchart();
// Check that the BarChart is shown in full
{
let mut c = chart.clone();
c.remove_invisible_groups_and_bars(21);
assert_eq!(c.data.len(), 3);
assert_eq!(c.data[2].bars.len(), 3);
}
let mut buffer = Buffer::empty(Rect::new(0, 0, 21, 3));
chart.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines(vec![
"█ █ █ █ ",
"█ █ █ █ █ █ █ █ █",
"G1 G2 G3 ",
]);
assert_buffer_eq!(buffer, expected);
}
#[test]
fn test_invisible_groups_and_bars_missing_last_2_bars() {
// Last 2 bars of G3 should be out of screen. (screen width is 17)
let chart = create_test_barchart();
{
let mut w = chart.clone();
w.remove_invisible_groups_and_bars(17);
assert_eq!(w.data.len(), 3);
assert_eq!(w.data[2].bars.len(), 1);
}
let mut buffer = Buffer::empty(Rect::new(0, 0, 17, 3));
chart.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines(vec![
"█ █ █ ",
"█ █ █ █ █ █ █",
"G1 G2 G",
]);
assert_buffer_eq!(buffer, expected);
}
#[test]
fn test_invisible_groups_and_bars_missing_last_group() {
// G3 should be out of screen. (screen width is 16)
let chart = create_test_barchart();
{
let mut w = chart.clone();
w.remove_invisible_groups_and_bars(16);
assert_eq!(w.data.len(), 2);
assert_eq!(w.data[1].bars.len(), 3);
}
let mut buffer = Buffer::empty(Rect::new(0, 0, 16, 3));
chart.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines(vec![
"█ █ █ ",
"█ █ █ █ █ █ ",
"G1 G2 ",
]);
assert_buffer_eq!(buffer, expected);
}
#[test]
fn test_invisible_groups_and_bars_show_only_1_bar() {
let chart = create_test_barchart();
{
let mut w = chart.clone();
w.remove_invisible_groups_and_bars(1);
assert_eq!(w.data.len(), 1);
assert_eq!(w.data[0].bars.len(), 1);
}
let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 3));
chart.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines(vec!["", "", "G"]);
assert_buffer_eq!(buffer, expected);
}
#[test]
fn test_invisible_groups_and_bars_all_bars_outside_visible_area() {
let chart = create_test_barchart();
{
let mut w = chart.clone();
w.remove_invisible_groups_and_bars(0);
assert_eq!(w.data.len(), 0);
}
let mut buffer = Buffer::empty(Rect::new(0, 0, 0, 3));
// Check if the render method panics
chart.render(buffer.area, &mut buffer);
}
#[test]
fn test_label_height() {
{
let barchart = BarChart::default().data(
BarGroup::default()
.label("Group Label".into())
.bars(&[Bar::default().value(2).label("Bar Label".into())]),
);
assert_eq!(barchart.label_height(), 2);
}
{
let barchart = BarChart::default().data(
BarGroup::default()
.label("Group Label".into())
.bars(&[Bar::default().value(2)]),
);
assert_eq!(barchart.label_height(), 1);
}
{
let barchart = BarChart::default().data(
BarGroup::default().bars(&[Bar::default().value(2).label("Bar Label".into())]),
);
assert_eq!(barchart.label_height(), 1);
}
{
let barchart =
BarChart::default().data(BarGroup::default().bars(&[Bar::default().value(2)]));
assert_eq!(barchart.label_height(), 0);
}
}
#[test]
fn can_be_stylized() {
assert_eq!(
@@ -964,7 +846,7 @@ mod tests {
let mut buffer = Buffer::empty(Rect::new(0, 0, 3, 3));
chart.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines(vec!["", "█ █", "G "]);
let expected = Buffer::with_lines(vec!["", "1 2", "G "]);
assert_buffer_eq!(buffer, expected);
}
@@ -1093,6 +975,21 @@ mod tests {
test_horizontal_bars_label_width_greater_than_bar(Some(Color::White))
}
/// Tests horizontal bars label are presents
#[test]
fn test_horizontal_label() {
let chart = BarChart::default()
.direction(Direction::Horizontal)
.bar_gap(0)
.data(&[("Jan", 10), ("Feb", 20), ("Mar", 5)]);
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
chart.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines(vec!["Jan 10█ ", "Feb 20████", "Mar 5 "]);
assert_buffer_eq!(buffer, expected);
}
#[test]
fn test_group_label_style() {
let chart: BarChart<'_> = BarChart::default()
@@ -1122,17 +1019,29 @@ mod tests {
#[test]
fn test_group_label_center() {
let chart: BarChart<'_> = BarChart::default().data(
BarGroup::default()
.label(Line::from(Span::from("G")).alignment(Alignment::Center))
.bars(&[Bar::default().value(2), Bar::default().value(5)]),
);
// test the centered group position when one bar is outside the group
let group = BarGroup::from(&[("a", 1), ("b", 2), ("c", 3), ("c", 4)]);
let chart = BarChart::default()
.data(
group
.clone()
.label(Line::from("G1").alignment(Alignment::Center)),
)
.data(group.label(Line::from("G2").alignment(Alignment::Center)));
let mut buffer = Buffer::empty(Rect::new(0, 0, 3, 3));
let mut buffer = Buffer::empty(Rect::new(0, 0, 13, 5));
chart.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines(vec!["", "▆ █", " G "]);
assert_buffer_eq!(buffer, expected);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
" ▂ █ ▂",
" ▄ █ █ ▄ █",
"▆ 2 3 4 ▆ 2 3",
"a b c c a b c",
" G1 G2 ",
])
);
}
#[test]
@@ -1146,7 +1055,280 @@ mod tests {
let mut buffer = Buffer::empty(Rect::new(0, 0, 3, 3));
chart.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines(vec!["", "", " G"]);
let expected = Buffer::with_lines(vec!["", "5", " G"]);
assert_buffer_eq!(buffer, expected);
}
#[test]
fn test_unicode_as_value() {
let group = BarGroup::default().bars(&[
Bar::default()
.value(123)
.label("B1".into())
.text_value("".into()),
Bar::default()
.value(321)
.label("B2".into())
.text_value("".into()),
Bar::default()
.value(333)
.label("B2".into())
.text_value("".into()),
]);
let chart = BarChart::default().data(group).bar_width(3).bar_gap(1);
let mut buffer = Buffer::empty(Rect::new(0, 0, 11, 5));
chart.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines(vec![
" ▆▆▆ ███",
" ███ ███",
"▃▃▃ ███ ███",
"写█ 写█ 写█",
"B1 B2 B2 ",
]);
assert_buffer_eq!(buffer, expected);
}
#[test]
fn handles_zero_width() {
// this test is to ensure that a BarChart with zero bar / gap width does not panic
let chart = BarChart::default()
.data(&[("A", 1)])
.bar_width(0)
.bar_gap(0);
let mut buffer = Buffer::empty(Rect::new(0, 0, 0, 10));
chart.render(buffer.area, &mut buffer);
assert_buffer_eq!(buffer, Buffer::empty(Rect::new(0, 0, 0, 10)));
}
#[test]
fn single_line() {
let mut group: BarGroup = (&[
("a", 0),
("b", 1),
("c", 2),
("d", 3),
("e", 4),
("f", 5),
("g", 6),
("h", 7),
("i", 8),
])
.into();
group = group.label("Group".into());
let chart = BarChart::default()
.data(group)
.bar_set(symbols::bar::NINE_LEVELS);
let mut buffer = Buffer::empty(Rect::new(0, 0, 17, 1));
chart.render(buffer.area, &mut buffer);
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" ▁ ▂ ▃ ▄ ▅ ▆ ▇ 8"]));
}
#[test]
fn two_lines() {
let mut group: BarGroup = (&[
("a", 0),
("b", 1),
("c", 2),
("d", 3),
("e", 4),
("f", 5),
("g", 6),
("h", 7),
("i", 8),
])
.into();
group = group.label("Group".into());
let chart = BarChart::default()
.data(group)
.bar_set(symbols::bar::NINE_LEVELS);
let mut buffer = Buffer::empty(Rect::new(0, 0, 17, 3));
chart.render(Rect::new(0, 1, buffer.area.width, 2), &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
" ",
" ▁ ▂ ▃ ▄ ▅ ▆ ▇ 8",
"a b c d e f g h i",
])
);
}
#[test]
fn three_lines() {
let mut group: BarGroup = (&[
("a", 0),
("b", 1),
("c", 2),
("d", 3),
("e", 4),
("f", 5),
("g", 6),
("h", 7),
("i", 8),
])
.into();
group = group.label(Line::from("Group").alignment(Alignment::Center));
let chart = BarChart::default()
.data(group)
.bar_set(symbols::bar::NINE_LEVELS);
let mut buffer = Buffer::empty(Rect::new(0, 0, 17, 3));
chart.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
" ▁ ▂ ▃ ▄ ▅ ▆ ▇ 8",
"a b c d e f g h i",
" Group ",
])
);
}
#[test]
fn three_lines_double_width() {
let mut group = BarGroup::from(&[
("a", 0),
("b", 1),
("c", 2),
("d", 3),
("e", 4),
("f", 5),
("g", 6),
("h", 7),
("i", 8),
]);
group = group.label(Line::from("Group").alignment(Alignment::Center));
let chart = BarChart::default()
.data(group)
.bar_width(2)
.bar_set(symbols::bar::NINE_LEVELS);
let mut buffer = Buffer::empty(Rect::new(0, 0, 26, 3));
chart.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
" 1▁ 2▂ 3▃ 4▄ 5▅ 6▆ 7▇ 8█",
"a b c d e f g h i ",
" Group ",
])
);
}
#[test]
fn four_lines() {
let mut group: BarGroup = (&[
("a", 0),
("b", 1),
("c", 2),
("d", 3),
("e", 4),
("f", 5),
("g", 6),
("h", 7),
("i", 8),
])
.into();
group = group.label(Line::from("Group").alignment(Alignment::Center));
let chart = BarChart::default()
.data(group)
.bar_set(symbols::bar::NINE_LEVELS);
let mut buffer = Buffer::empty(Rect::new(0, 0, 17, 4));
chart.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
" ▂ ▄ ▆ █",
" ▂ ▄ ▆ 4 5 6 7 8",
"a b c d e f g h i",
" Group ",
])
);
}
#[test]
fn two_lines_without_bar_labels() {
let group = BarGroup::default()
.label(Line::from("Group").alignment(Alignment::Center))
.bars(&[
Bar::default().value(0),
Bar::default().value(1),
Bar::default().value(2),
Bar::default().value(3),
Bar::default().value(4),
Bar::default().value(5),
Bar::default().value(6),
Bar::default().value(7),
Bar::default().value(8),
]);
let chart = BarChart::default().data(group);
let mut buffer = Buffer::empty(Rect::new(0, 0, 17, 3));
chart.render(Rect::new(0, 1, buffer.area.width, 2), &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
" ",
" ▁ ▂ ▃ ▄ ▅ ▆ ▇ 8",
" Group ",
])
);
}
#[test]
fn one_lines_with_more_bars() {
let bars: Vec<Bar> = (0..30).map(|i| Bar::default().value(i)).collect();
let chart = BarChart::default().data(BarGroup::default().bars(&bars));
let mut buffer = Buffer::empty(Rect::new(0, 0, 59, 1));
chart.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
" ▁ ▁ ▁ ▁ ▂ ▂ ▂ ▃ ▃ ▃ ▃ ▄ ▄ ▄ ▄ ▅ ▅ ▅ ▆ ▆ ▆ ▆ ▇ ▇ ▇ █",
])
);
}
#[test]
fn first_bar_of_the_group_is_half_outside_view() {
let chart = BarChart::default()
.data(&[("a", 1), ("b", 2)])
.data(&[("a", 1), ("b", 2)])
.bar_width(2);
let mut buffer = Buffer::empty(Rect::new(0, 0, 7, 6));
chart.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
" ██ ",
" ██ ",
"▄▄ ██ ",
"██ ██ ",
"1█ 2█ ",
"a b ",
])
);
}
}

View File

@@ -1,3 +1,5 @@
use unicode_width::UnicodeWidthStr;
use crate::{buffer::Buffer, prelude::Rect, style::Style, text::Line};
/// A bar to be shown by the [`BarChart`](crate::widgets::BarChart) widget.
@@ -137,16 +139,15 @@ impl<'a> Bar<'a> {
}
}
pub(super) fn render_label_and_value(
pub(super) fn render_value(
self,
buf: &mut Buffer,
max_width: u16,
x: u16,
y: u16,
default_value_style: Style,
default_label_style: Style,
ticks: u64,
) {
// render the value
if self.value != 0 {
let value_label = if let Some(text) = self.text_value {
text
@@ -154,8 +155,11 @@ impl<'a> Bar<'a> {
self.value.to_string()
};
let width = value_label.len() as u16;
if width < max_width {
let width = value_label.width() as u16;
const TICKS_PER_LINE: u64 = 8;
// if we have enough space or the ticks are greater equal than 1 cell (8)
// then print the value
if width < max_width || (width == max_width && ticks >= TICKS_PER_LINE) {
buf.set_string(
x + (max_width.saturating_sub(value_label.len() as u16) >> 1),
y,
@@ -164,9 +168,17 @@ impl<'a> Bar<'a> {
);
}
}
}
// render the label
if let Some(mut label) = self.label {
pub(super) fn render_label(
&mut self,
buf: &mut Buffer,
max_width: u16,
x: u16,
y: u16,
default_label_style: Style,
) {
if let Some(label) = &mut self.label {
// patch label styles
for span in &mut label.spans {
span.style = default_label_style.patch(span.style);
@@ -174,8 +186,8 @@ impl<'a> Bar<'a> {
buf.set_line(
x + (max_width.saturating_sub(label.width() as u16) >> 1),
y + 1,
&label,
y,
label,
max_width,
);
}

View File

@@ -16,7 +16,7 @@ use crate::{
buffer::Buffer,
layout::{Alignment, Rect},
style::{Style, Styled},
symbols::line,
symbols::border,
widgets::{Borders, Widget},
};
@@ -70,18 +70,46 @@ pub enum BorderType {
/// ┗━━━━━━━┛
/// ```
Thick,
/// A border with a single line on the inside of a half block.
///
/// # Example
///
/// ```plain
/// ▗▄▄▄▄▄▄▄▖
/// ▐ ▌
/// ▐ ▌
/// ▝▀▀▀▀▀▀▀▘
QuadrantInside,
/// A border with a single line on the outside of a half block.
///
/// # Example
///
/// ```plain
/// ▛▀▀▀▀▀▀▀▜
/// ▌ ▐
/// ▌ ▐
/// ▙▄▄▄▄▄▄▄▟
QuadrantOutside,
}
impl BorderType {
/// Convert this `BorderType` into the corresponding [`Set`](line::Set) of lines.
pub const fn line_symbols(border_type: BorderType) -> line::Set {
/// Convert this `BorderType` into the corresponding [`Set`](border::Set) of border symbols.
pub const fn border_symbols(border_type: BorderType) -> border::Set {
match border_type {
BorderType::Plain => line::NORMAL,
BorderType::Rounded => line::ROUNDED,
BorderType::Double => line::DOUBLE,
BorderType::Thick => line::THICK,
BorderType::Plain => border::PLAIN,
BorderType::Rounded => border::ROUNDED,
BorderType::Double => border::DOUBLE,
BorderType::Thick => border::THICK,
BorderType::QuadrantInside => border::QUADRANT_INSIDE,
BorderType::QuadrantOutside => border::QUADRANT_OUTSIDE,
}
}
/// Convert this `BorderType` into the corresponding [`Set`](border::Set) of border symbols.
pub const fn to_border_set(self) -> border::Set {
Self::border_symbols(self)
}
}
/// Defines the padding of a [`Block`].
@@ -213,10 +241,9 @@ pub struct Block<'a> {
borders: Borders,
/// Border style
border_style: Style,
/// Type of the border. The default is plain lines but one can choose to have rounded or
/// doubled lines instead.
border_type: BorderType,
/// The symbols used to render the border. The default is plain lines but one can choose to
/// have rounded or doubled lines instead or a custom set of symbols
border_set: border::Set,
/// Widget style
style: Style,
/// Block padding
@@ -233,7 +260,7 @@ impl<'a> Block<'a> {
titles_position: Position::Top,
borders: Borders::NONE,
border_style: Style::new(),
border_type: BorderType::Plain,
border_set: BorderType::Plain.to_border_set(),
style: Style::new(),
padding: Padding::zero(),
}
@@ -414,9 +441,40 @@ impl<'a> Block<'a> {
/// Sets the symbols used to display the border (e.g. single line, double line, thick or
/// rounded borders).
///
/// Setting this overwrites any custom [`border_set`](Block::border_set) that was set.
///
/// See [`BorderType`] for the full list of available symbols.
///
/// # Examples
///
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// Block::default().title("Block").borders(Borders::ALL).border_type(BorderType::Rounded);
/// // Renders
/// // ╭Block╮
/// // │ │
/// // ╰─────╯
/// ```
pub const fn border_type(mut self, border_type: BorderType) -> Block<'a> {
self.border_type = border_type;
self.border_set = border_type.to_border_set();
self
}
/// Sets the symbols used to display the border as a [`crate::symbols::border::Set`].
///
/// Setting this overwrites any [`border_type`](Block::border_type) that was set.
///
/// # Examples
///
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// Block::default().title("Block").borders(Borders::ALL).border_set(symbols::border::DOUBLE);
/// // Renders
/// // ╔Block╗
/// // ║ ║
/// // ╚═════╝
pub const fn border_set(mut self, border_set: border::Set) -> Block<'a> {
self.border_set = border_set;
self
}
@@ -427,7 +485,7 @@ impl<'a> Block<'a> {
/// Draw a block nested within another block
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// # fn render_nested_block<B: Backend>(frame: &mut Frame<B>) {
/// # fn render_nested_block(frame: &mut Frame) {
/// let outer_block = Block::default().title("Outer").borders(Borders::ALL);
/// let inner_block = Block::default().title("Inner").borders(Borders::ALL);
///
@@ -511,20 +569,20 @@ impl<'a> Block<'a> {
fn render_borders(&self, area: Rect, buf: &mut Buffer) {
buf.set_style(area, self.style);
let symbols = BorderType::line_symbols(self.border_type);
let symbols = self.border_set;
// Sides
if self.borders.intersects(Borders::LEFT) {
for y in area.top()..area.bottom() {
buf.get_mut(area.left(), y)
.set_symbol(symbols.vertical)
.set_symbol(symbols.vertical_left)
.set_style(self.border_style);
}
}
if self.borders.intersects(Borders::TOP) {
for x in area.left()..area.right() {
buf.get_mut(x, area.top())
.set_symbol(symbols.horizontal)
.set_symbol(symbols.horizontal_top)
.set_style(self.border_style);
}
}
@@ -532,7 +590,7 @@ impl<'a> Block<'a> {
let x = area.right() - 1;
for y in area.top()..area.bottom() {
buf.get_mut(x, y)
.set_symbol(symbols.vertical)
.set_symbol(symbols.vertical_right)
.set_style(self.border_style);
}
}
@@ -540,7 +598,7 @@ impl<'a> Block<'a> {
let y = area.bottom() - 1;
for x in area.left()..area.right() {
buf.get_mut(x, y)
.set_symbol(symbols.horizontal)
.set_symbol(symbols.horizontal_bottom)
.set_style(self.border_style);
}
}
@@ -883,7 +941,7 @@ mod tests {
#[test]
fn border_type_can_be_const() {
const _PLAIN: line::Set = BorderType::line_symbols(BorderType::Plain);
const _PLAIN: border::Set = BorderType::border_symbols(BorderType::Plain);
}
#[test]
@@ -927,7 +985,7 @@ mod tests {
titles_position: Position::Top,
borders: Borders::NONE,
border_style: Style::new(),
border_type: BorderType::Plain,
border_set: BorderType::Plain.to_border_set(),
style: Style::new(),
padding: Padding::zero(),
}
@@ -1119,6 +1177,7 @@ mod tests {
])
);
}
#[test]
fn render_rounded_border() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
@@ -1135,6 +1194,7 @@ mod tests {
])
);
}
#[test]
fn render_double_border() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
@@ -1152,6 +1212,40 @@ mod tests {
);
}
#[test]
fn render_quadrant_inside() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::QuadrantInside)
.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
"▗▄▄▄▄▄▄▄▄▄▄▄▄▄▖",
"▐ ▌",
"▝▀▀▀▀▀▀▀▀▀▀▀▀▀▘",
])
);
}
#[test]
fn render_border_quadrant_outside() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::QuadrantOutside)
.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
"▛▀▀▀▀▀▀▀▀▀▀▀▀▀▜",
"▌ ▐",
"▙▄▄▄▄▄▄▄▄▄▄▄▄▄▟",
])
);
}
#[test]
fn render_solid_border() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
@@ -1168,4 +1262,30 @@ mod tests {
])
);
}
#[test]
fn render_custom_border_set() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
Block::default()
.borders(Borders::ALL)
.border_set(border::Set {
top_left: "1",
top_right: "2",
bottom_left: "3",
bottom_right: "4",
vertical_left: "L",
vertical_right: "R",
horizontal_top: "T",
horizontal_bottom: "B",
})
.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
"1TTTTTTTTTTTTT2",
"L R",
"3BBBBBBBBBBBBB4",
])
);
}
}

View File

@@ -10,7 +10,7 @@ use crate::{buffer::Buffer, layout::Rect, widgets::Widget};
/// ```
/// use ratatui::{prelude::*, widgets::*};
///
/// fn draw_on_clear<B: Backend>(f: &mut Frame<B>, area: Rect) {
/// fn draw_on_clear(f: &mut Frame, area: Rect) {
/// let block = Block::default().title("Block").borders(Borders::ALL);
/// f.render_widget(Clear, area); // <- this will clear/reset the area first
/// f.render_widget(block, area); // now render the block widget

View File

@@ -1,3 +1,4 @@
#![deny(missing_docs)]
use crate::{
buffer::Buffer,
layout::Rect,
@@ -7,18 +8,36 @@ use crate::{
widgets::{Block, Widget},
};
/// A widget to display a task progress.
/// A widget to display a progress bar.
///
/// # Examples:
/// 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.
///
/// # Example
///
/// ```
/// use ratatui::{prelude::*, widgets::*};
///
/// Gauge::default()
/// .block(Block::default().borders(Borders::ALL).title("Progress"))
/// .gauge_style(Style::default().fg(Color::White).bg(Color::Black).add_modifier(Modifier::ITALIC))
/// .gauge_style(
/// Style::default()
/// .fg(Color::White)
/// .bg(Color::Black)
/// .add_modifier(Modifier::ITALIC),
/// )
/// .percent(20);
/// ```
///
/// # See also
///
/// - [`LineGauge`] for a thin progress bar
#[derive(Debug, Clone, PartialEq)]
pub struct Gauge<'a> {
block: Option<Block<'a>>,
@@ -43,11 +62,24 @@ impl<'a> Default for Gauge<'a> {
}
impl<'a> Gauge<'a> {
/// Surrounds the `Gauge` with a [`Block`].
///
/// The gauge is rendered in the inner portion of the block once space for borders and padding
/// is reserved. Styles set on the block do **not** affect the bar itself.
pub fn block(mut self, block: Block<'a>) -> Gauge<'a> {
self.block = Some(block);
self
}
/// Sets the bar progression from a percentage.
///
/// # Panics
///
/// This method panics if `percent` is **not** between 0 and 100 inclusively.
///
/// # See also
///
/// See [`Gauge::ratio`] to set from a float.
pub fn percent(mut self, percent: u16) -> Gauge<'a> {
assert!(
percent <= 100,
@@ -57,7 +89,18 @@ impl<'a> Gauge<'a> {
self
}
/// Sets ratio ([0.0, 1.0]) directly.
/// Sets the bar progression from a ratio (float).
///
/// `ratio` is the ratio between filled bar over empty bar (i.e. `3/4` completion is `0.75`).
/// This is more easily seen as a floating point percentage (e.g. 42% = `0.42`).
///
/// # Panics
///
/// This method panics if `ratio` is **not** between 0 and 1 inclusively.
///
/// # See also
///
/// See [`Gauge::percent`] to set from a percentage.
pub fn ratio(mut self, ratio: f64) -> Gauge<'a> {
assert!(
(0.0..=1.0).contains(&ratio),
@@ -67,6 +110,10 @@ impl<'a> Gauge<'a> {
self
}
/// Sets the label to display in the center of the bar.
///
/// For a left-aligned label, see [`LineGauge`].
/// If the label is not defined, it is the percentage filled.
pub fn label<T>(mut self, label: T) -> Gauge<'a>
where
T: Into<Span<'a>>,
@@ -75,16 +122,26 @@ impl<'a> Gauge<'a> {
self
}
/// Sets the widget style.
///
/// This will style the block (if any non-styled) and background of the widget (everything
/// except the bar itself). [`Block`] style set with [`Gauge::block`] takes precedence.
pub fn style(mut self, style: Style) -> Gauge<'a> {
self.style = style;
self
}
/// Sets the style of the bar.
pub fn gauge_style(mut self, style: Style) -> Gauge<'a> {
self.gauge_style = style;
self
}
/// Sets whether to use unicode characters to display the progress bar.
///
/// This enables the use of
/// [unicode block characters](https://en.wikipedia.org/wiki/Block_Elements).
/// This is useful to display a higher precision bar (8 extra fractional parts per cell).
pub fn use_unicode(mut self, unicode: bool) -> Gauge<'a> {
self.use_unicode = unicode;
self
@@ -165,7 +222,16 @@ fn get_unicode_block<'a>(frac: f64) -> &'a str {
}
}
/// A compact widget to display a task progress over a single line.
/// A compact widget to display a progress bar over a single thin line.
///
/// 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.
/// You can also set the symbols used to draw the bar with [`LineGauge::line_set`].
///
/// This can be useful to indicate the progression of a task, like a download.
///
/// # Examples:
///
@@ -174,10 +240,19 @@ fn get_unicode_block<'a>(frac: f64) -> &'a str {
///
/// LineGauge::default()
/// .block(Block::default().borders(Borders::ALL).title("Progress"))
/// .gauge_style(Style::default().fg(Color::White).bg(Color::Black).add_modifier(Modifier::BOLD))
/// .gauge_style(
/// Style::default()
/// .fg(Color::White)
/// .bg(Color::Black)
/// .add_modifier(Modifier::BOLD),
/// )
/// .line_set(symbols::line::THICK)
/// .ratio(0.4);
/// ```
///
/// # See also
///
/// - [`Gauge`] for bigger, higher precision and more configurable progress bar
#[derive(Debug, Default, Clone, PartialEq)]
pub struct LineGauge<'a> {
block: Option<Block<'a>>,
@@ -189,11 +264,20 @@ pub struct LineGauge<'a> {
}
impl<'a> LineGauge<'a> {
/// Surrounds the `LineGauge` with a [`Block`].
pub fn block(mut self, block: Block<'a>) -> Self {
self.block = Some(block);
self
}
/// Sets the bar progression from a ratio (float).
///
/// `ratio` is the ratio between filled bar over empty bar (i.e. `3/4` completion is `0.75`).
/// This is more easily seen as a floating point percentage (e.g. 42% = `0.42`).
///
/// # Panics
///
/// This method panics if `ratio` is **not** between 0 and 1 inclusively.
pub fn ratio(mut self, ratio: f64) -> Self {
assert!(
(0.0..=1.0).contains(&ratio),
@@ -203,11 +287,22 @@ impl<'a> LineGauge<'a> {
self
}
/// Sets the characters to use for the line.
///
/// # See also
///
/// See [`symbols::line::Set`] for more information. Predefined sets are also available, see
/// [`NORMAL`](symbols::line::NORMAL), [`DOUBLE`](symbols::line::DOUBLE) and
/// [`THICK`](symbols::line::THICK).
pub fn line_set(mut self, set: symbols::line::Set) -> Self {
self.line_set = set;
self
}
/// Sets the label to display.
///
/// With `LineGauge`, labels are only on the left, see [`Gauge`] for a centered label.
/// If the label is not defined, it is the percentage filled.
pub fn label<T>(mut self, label: T) -> Self
where
T: Into<Line<'a>>,
@@ -216,11 +311,16 @@ impl<'a> LineGauge<'a> {
self
}
/// Sets the widget style.
///
/// This will style everything except the bar itself, so basically the block (if any) and
/// background.
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
/// Sets the style of the bar.
pub fn gauge_style(mut self, style: Style) -> Self {
self.gauge_style = style;
self

View File

@@ -143,7 +143,7 @@ pub enum ScrollbarOrientation {
/// ```rust
/// use ratatui::{prelude::*, widgets::*};
///
/// # fn render_paragraph_with_scrollbar<B: Backend>(frame: &mut Frame<B>, area: Rect) {
/// # fn render_paragraph_with_scrollbar(frame: &mut Frame, area: Rect) {
///
/// let vertical_scroll = 0; // from app state
///

View File

@@ -304,11 +304,52 @@ impl<'a> Table<'a> {
}
}
/// Creates a custom block around a [`Table`] widget.
///
/// The `block` parameter is of type [`Block`]. This holds the specified block to be
/// created around the [`Table`]
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let table = Table::new(vec![
/// Row::new(vec![
/// Cell::from("Cell1"),
/// Cell::from("Cell2")
/// ]),
/// Row::new(vec![
/// Cell::from("Cell3"),
/// Cell::from("Cell4")
/// ]),
/// ]).block(Block::default().title("Table"));
/// ```
pub fn block(mut self, block: Block<'a>) -> Self {
self.block = Some(block);
self
}
/// Creates a header for a [`Table`] widget.
///
/// The `header` parameter is of type [`Row`] and this holds the cells to be displayed at the
/// top of the [`Table`]
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let table = Table::new(vec![
/// Row::new(vec![
/// Cell::from("Cell1"),
/// Cell::from("Cell2")
/// ])
/// ]).header(
/// Row::new(vec![
/// Cell::from("Header Cell 1"),
/// Cell::from("Header Cell 2")
/// ])
/// );
/// ```
pub fn header(mut self, header: Row<'a>) -> Self {
self.header = Some(header);
self

View File

@@ -1,3 +1,4 @@
#![deny(missing_docs)]
use crate::{
buffer::Buffer,
layout::Rect,
@@ -7,18 +8,22 @@ use crate::{
widgets::{Block, Widget},
};
/// A widget to display available tabs in a multiple panels context.
/// A widget that displays a horizontal set of Tabs with a single tab selected.
///
/// # Examples
/// Each tab title is stored as a [`Line`] which can be individually styled. The selected tab is set
/// using [`Tabs::select`] and styled using [`Tabs::highlight_style`]. The divider can be customized
/// with [`Tabs::divider`].
///
/// # Example
///
/// ```
/// use ratatui::{prelude::*, widgets::*};
///
/// let titles = ["Tab1", "Tab2", "Tab3", "Tab4"].iter().cloned().map(Line::from).collect();
/// Tabs::new(titles)
/// Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"])
/// .block(Block::default().title("Tabs").borders(Borders::ALL))
/// .style(Style::default().fg(Color::White))
/// .highlight_style(Style::default().fg(Color::Yellow))
/// .style(Style::default().white())
/// .highlight_style(Style::default().yellow())
/// .select(2)
/// .divider(symbols::DOT);
/// ```
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
@@ -38,6 +43,24 @@ pub struct Tabs<'a> {
}
impl<'a> Tabs<'a> {
/// Creates new `Tabs` from their titles.
///
/// `titles` can be a [`Vec`] of [`&str`], [`String`] or anything that can be converted into
/// [`Line`]. As such, titles can be styled independently.
///
/// # Examples
///
/// Basic titles.
/// ```
/// # use ratatui::{prelude::*, widgets::Tabs};
/// let tabs = Tabs::new(vec!["Tab 1", "Tab 2"]);
/// ```
///
/// Styled titles
/// ```
/// # use ratatui::{prelude::*, widgets::Tabs};
/// let tabs = Tabs::new(vec!["Tab 1".red(), "Tab 2".blue()]);
/// ```
pub fn new<T>(titles: Vec<T>) -> Tabs<'a>
where
T: Into<Line<'a>>,
@@ -52,26 +75,55 @@ impl<'a> Tabs<'a> {
}
}
/// Surrounds the `Tabs` with a [`Block`].
pub fn block(mut self, block: Block<'a>) -> Tabs<'a> {
self.block = Some(block);
self
}
/// Sets the selected tab.
///
/// The first tab has index 0 (this is also the default index).
/// The selected tab can have a different style with [`Tabs::highlight_style`].
pub fn select(mut self, selected: usize) -> Tabs<'a> {
self.selected = selected;
self
}
/// Sets the style of the tabs.
///
/// This will set the given style on the entire render area.
/// More precise style can be applied to the titles by styling the ones given to [`Tabs::new`].
/// The selected tab can be styled differently using [`Tabs::highlight_style`].
pub fn style(mut self, style: Style) -> Tabs<'a> {
self.style = style;
self
}
/// Sets the style for the highlighted tab.
///
/// Highlighted tab can be selected with [`Tabs::select`].
pub fn highlight_style(mut self, style: Style) -> Tabs<'a> {
self.highlight_style = style;
self
}
/// Sets the string to use as tab divider.
///
/// By default, the divider is a pipe (`|`).
///
/// # Examples
///
/// Use a dot (`•`) as separator.
/// ```
/// # use ratatui::{prelude::*, widgets::Tabs, symbols};
/// let tabs = Tabs::new(vec!["Tab 1", "Tab 2"]).divider(symbols::DOT);
/// ```
/// Use dash (`-`) as separator.
/// ```
/// # use ratatui::{prelude::*, widgets::Tabs, symbols};
/// let tabs = Tabs::new(vec!["Tab 1", "Tab 2"]).divider("-");
/// ```
pub fn divider<T>(mut self, divider: T) -> Tabs<'a>
where
T: Into<Span<'a>>,

View File

@@ -1,5 +1,3 @@
#![allow(deprecated)]
use ratatui::{
backend::TestBackend,
buffer::Buffer,

View File

@@ -1,5 +1,3 @@
#![allow(deprecated)]
use ratatui::{
backend::TestBackend,
buffer::Buffer,

View File

@@ -1,5 +1,3 @@
#![allow(deprecated)]
use ratatui::{
backend::TestBackend, buffer::Buffer, layout::Rect, symbols, text::Line, widgets::Tabs,
Terminal,