Compare commits
38 Commits
v0.23.1-al
...
v0.23.1-al
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad2dc5646d | ||
|
|
6cbdb06fd8 | ||
|
|
27c5637675 | ||
|
|
0c52ff431a | ||
|
|
8d507c43fa | ||
|
|
b61f65bc20 | ||
|
|
3a57e76ed1 | ||
|
|
6c7bef8d11 | ||
|
|
88ae3485c2 | ||
|
|
e5caf170c8 | ||
|
|
089f8ba66a | ||
|
|
fbf1a451c8 | ||
|
|
4541336514 | ||
|
|
346e7b4f4d | ||
|
|
15641c8475 | ||
|
|
2fd85af33c | ||
|
|
401a7a7f71 | ||
|
|
e35e4135c9 | ||
|
|
8ae4403b63 | ||
|
|
11076d0af3 | ||
|
|
9cfb133a98 | ||
|
|
4548a9b7e2 | ||
|
|
61af0d9906 | ||
|
|
c0991cc576 | ||
|
|
301366c4fa | ||
|
|
3bda372847 | ||
|
|
082cbcbc50 | ||
|
|
cbf86da0e7 | ||
|
|
32e461953c | ||
|
|
d67fa2c00d | ||
|
|
c3155a2489 | ||
|
|
c5ea656385 | ||
|
|
be55a5fbcd | ||
|
|
21303f2167 | ||
|
|
c9b8e7cf41 | ||
|
|
5498a889ae | ||
|
|
0fe738500c | ||
|
|
dd9a8df03a |
7
.github/ISSUE_TEMPLATE/config.yml
vendored
7
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Discord Chat
|
||||
url: https://discord.gg/pMCEU9hNEj
|
||||
about: Ask questions about ratatui on Discord
|
||||
- name: Matrix Chat
|
||||
url: https://matrix.to/#/#ratatui:matrix.org
|
||||
about: Ask questions about ratatui on Matrix
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
225
BREAKING-CHANGES.md
Normal file
225
BREAKING-CHANGES.md
Normal file
@@ -0,0 +1,225 @@
|
||||
# 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)
|
||||
|
||||
## Summary
|
||||
|
||||
This is a quick summary of the sections below:
|
||||
|
||||
- [Unreleased (v0.24.0)](#unreleased-0240)
|
||||
- `ScrollbarState`: `position`, `content_length`, and `viewport_content_length` are now `usize`
|
||||
- `BorderType`: `line_symbols` is now `border_symbols` and returns `symbols::border::set`
|
||||
- `Frame<'a, B: Backend>` is now `Frame<'a>`
|
||||
- `Stylize` shorthands for `String` now consume the value and return `Span<'static>`
|
||||
- `Spans` is removed
|
||||
- [v0.23.0](#v0230)
|
||||
- `Scrollbar`: `track_symbol` now takes `Option<&str>`
|
||||
- `Scrollbar`: symbols moved to `symbols` module
|
||||
- MSRV is now 1.67.0
|
||||
- [v0.22.0](#v0220)
|
||||
- serde representation of `Borders` and `Modifiers` has changed
|
||||
- [v0.21.0](#v0210)
|
||||
- MSRV is now 1.65.0
|
||||
- `terminal::ViewPort` is now an enum
|
||||
- `"".as_ref()` must be annotated to implement `Into<Text<'a>>`
|
||||
- `Marker::Block` renders as a block char instead of a bar char
|
||||
- [v0.20.0](#v0200)
|
||||
- MSRV is now 1.63.0
|
||||
- `List` no longer ignores empty strings
|
||||
|
||||
## 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"),
|
||||
];
|
||||
```
|
||||
43
Cargo.toml
43
Cargo.toml
@@ -23,14 +23,8 @@ rust-version = "1.67.0"
|
||||
[badges]
|
||||
|
||||
[dependencies]
|
||||
#! The crate provides a set of optional features that can be enabled in your `cargo.toml` file.
|
||||
#!
|
||||
#! Generally an application will only use one backend, so you should only enable one of the following features:
|
||||
## enables the [`CrosstermBackend`] backend and adds a dependency on the [Crossterm crate].
|
||||
crossterm = { version = "0.27", optional = true }
|
||||
## enables the [`TermionBackend`] backend and adds a dependency on the [Termion crate].
|
||||
termion = { version = "2.0", optional = true }
|
||||
## enables the [`TermwizBackend`] backend and adds a dependency on the [Termwiz crate].
|
||||
termwiz = { version = "0.20.0", optional = true }
|
||||
|
||||
serde = { version = "1", optional = true, features = ["derive"] }
|
||||
@@ -48,18 +42,32 @@ 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]
|
||||
default = ["crossterm"]
|
||||
#! The crate provides a set of optional features that can be enabled in your `cargo.toml` file.
|
||||
#!
|
||||
## By default, we enable the crossterm backend as this is a reasonable choice for most applications
|
||||
## as it is supported on Linux/Mac/Windows systems. We also enable the `underline-color` feature
|
||||
## which allows you to set the underline color of text.
|
||||
default = ["crossterm", "underline-color"]
|
||||
#! Generally an application will only use one backend, so you should only enable one of the following features:
|
||||
## enables the [`CrosstermBackend`] backend and adds a dependency on the [Crossterm crate].
|
||||
crossterm = ["dep:crossterm"]
|
||||
## enables the [`TermionBackend`] backend and adds a dependency on the [Termion crate].
|
||||
termion = ["dep:termion"]
|
||||
## enables the [`TermwizBackend`] backend and adds a dependency on the [Termwiz crate].
|
||||
termwiz = ["dep:termwiz"]
|
||||
|
||||
#! The following optional features are available for all backends:
|
||||
## enables serialization and deserialization of style and color types using the [Serde crate].
|
||||
## This is useful if you want to save themes to a file.
|
||||
@@ -76,6 +84,11 @@ all-widgets = ["widget-calendar"]
|
||||
## enables the [`calendar`] widget module and adds a dependency on the [Time crate].
|
||||
widget-calendar = ["dep:time"]
|
||||
|
||||
#! Underline color is only supported by the [`CrosstermBackend`] backend, and is not supported
|
||||
#! on Windows 7.
|
||||
## enables the backend code that sets the underline color.
|
||||
underline-color = ["dep:crossterm"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
# see https://doc.rust-lang.org/nightly/rustdoc/scraped-examples.html
|
||||
@@ -149,6 +162,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"]
|
||||
|
||||
@@ -9,9 +9,9 @@ ALL_FEATURES = "all-widgets,macros,serde"
|
||||
|
||||
# Windows does not support building termion, so this avoids the build failure by providing two
|
||||
# sets of flags, one for Windows and one for other platforms.
|
||||
# Windows: --features=all-widgets,macros,serde,crossterm,termwiz
|
||||
# Other: --all-features
|
||||
ALL_FEATURES_FLAG = { source = "${CARGO_MAKE_RUST_TARGET_OS}", default_value = "--all-features", mapping = { "windows" = "--features=all-widgets,macros,serde,crossterm,termwiz" } }
|
||||
# Windows: --features=all-widgets,macros,serde,crossterm,termwiz,underline-color
|
||||
# Other: --features=all-widgets,macros,serde,crossterm,termion,termwiz,underline-color
|
||||
ALL_FEATURES_FLAG = { source = "${CARGO_MAKE_RUST_TARGET_OS}", default_value = "--features=all-widgets,macros,serde,crossterm,termion,termwiz", mapping = { "windows" = "--features=all-widgets,macros,serde,crossterm,termwiz" } }
|
||||
|
||||
[tasks.default]
|
||||
alias = "ci"
|
||||
|
||||
500
README.md
500
README.md
@@ -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.
|
||||
|
||||
[](https://crates.io/crates/ratatui)
|
||||
[](./LICENSE) [](https://github.com/ratatui-org/ratatui/actions?query=workflow%3ACI+)
|
||||
[](https://docs.rs/crate/ratatui/)
|
||||
[](https://deps.rs/repo/github/ratatui-org/ratatui)
|
||||
[](https://app.codecov.io/gh/ratatui-org/ratatui)
|
||||
[](https://discord.gg/pMCEU9hNEj)
|
||||
[](https://matrix.to/#/#ratatui:matrix.org)
|
||||
|
||||
<!-- See RELEASE.md for instructions on creating the demo 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 -->
|
||||
|
||||

|
||||
|
||||
<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([
|
||||
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([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([
|
||||
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
|
||||
|
||||
[](https://github.com/ratatui-org/ratatui/graphs/contributors)
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
Special thanks to [**Pavel Fomchenkov**](https://github.com/nawok) for his work in designing **an
|
||||
|
||||
28
RELEASE.md
28
RELEASE.md
@@ -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.
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
# Examples
|
||||
|
||||
These gifs were created using [Charm VHS](https://github.com/charmbracelet/vhs).
|
||||
These gifs were created using [VHS](https://github.com/charmbracelet/vhs). Each example has a
|
||||
corresponding `.tape` file that holds instructions for how to generate the images. Note that the
|
||||
images themselves are stored in a separate git branch to avoid bloating the main repository.
|
||||
|
||||
VHS has a problem rendering some background color transitions, which shows up in several examples
|
||||
below. See <https://github.com/charmbracelet/vhs/issues/344> for more info. These problems don't
|
||||
occur in a terminal.
|
||||
## Demo2
|
||||
|
||||
This is the demo example from the main README and crate page. Source: [demo2](./demo2/).
|
||||
|
||||
```shell
|
||||
cargo run --example=demo2 --features=crossterm
|
||||
```
|
||||
|
||||
![Demo2][demo2.gif]
|
||||
|
||||
## 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
|
||||
@@ -58,7 +66,7 @@ Demonstrates the [`Calendar`](https://docs.rs/ratatui/latest/ratatui/widgets/cal
|
||||
widget. Source: [calendar.rs](./calendar.rs).
|
||||
|
||||
```shell
|
||||
cargo run --example=calendar --features=crossterm widget-calendar
|
||||
cargo run --example=calendar --features="crossterm widget-calendar"
|
||||
```
|
||||
|
||||
![Calendar][calendar.gif]
|
||||
@@ -102,19 +110,20 @@ cargo run --example=colors --features=crossterm
|
||||
|
||||
Demonstrates the available RGB
|
||||
[`Color`](https://docs.rs/ratatui/latest/ratatui/style/enum.Color.html) options. These can be used
|
||||
in any style field. Source: [colors_rgb.rs](./colors_rgb.rs).
|
||||
in any style field. Source: [colors_rgb.rs](./colors_rgb.rs). Uses a half block technique to render
|
||||
two square-ish pixels in the space of a single rectangular terminal cell.
|
||||
|
||||
```shell
|
||||
cargo run --example=colors_rgb --features=crossterm
|
||||
```
|
||||
|
||||
![Colors RGB][colors_rgb.gif]
|
||||
![Colors RGB][colors_rgb.png]
|
||||
|
||||
## Custom Widget
|
||||
|
||||
Demonstrates how to implement the
|
||||
[`Widget`](https://docs.rs/ratatui/latest/ratatui/widgets/trait.Widget.html) trait. Source:
|
||||
[custom_widget.rs](./custom_widget.rs).
|
||||
[`Widget`](https://docs.rs/ratatui/latest/ratatui/widgets/trait.Widget.html) trait. Also shows mouse
|
||||
interaction. Source: [custom_widget.rs](./custom_widget.rs).
|
||||
|
||||
```shell
|
||||
cargo run --example=custom_widget --features=crossterm
|
||||
@@ -127,10 +136,6 @@ cargo run --example=custom_widget --features=crossterm
|
||||
Demonstrates the [`Gauge`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Gauge.html) widget.
|
||||
Source: [gauge.rs](./gauge.rs).
|
||||
|
||||
> [!NOTE] The backgrounds render poorly when we generate this example using VHS. This problem
|
||||
> doesn't generally happen during normal rendering in a terminal. See
|
||||
> [vhs#344](https://github.com/charmbracelet/vhs/issues/344) for more details.
|
||||
|
||||
```shell
|
||||
cargo run --example=gauge --features=crossterm
|
||||
```
|
||||
@@ -139,7 +144,7 @@ cargo run --example=gauge --features=crossterm
|
||||
|
||||
## Inline
|
||||
|
||||
Demonstrates the
|
||||
Demonstrates how to use the
|
||||
[`Inline`](https://docs.rs/ratatui/latest/ratatui/terminal/enum.Viewport.html#variant.Inline)
|
||||
Viewport mode for ratatui apps. Source: [inline.rs](./inline.rs).
|
||||
|
||||
@@ -216,10 +221,6 @@ Demonstrates how to render a widget over the top of previously rendered widgets
|
||||
cargo run --example=popup --features=crossterm
|
||||
```
|
||||
|
||||
> [!NOTE] The background renders poorly after the popup when we generate this example using VHS.
|
||||
> This problem doesn't generally happen during normal rendering in a terminal. See
|
||||
> [vhs#344](https://github.com/charmbracelet/vhs/issues/344) for more details.
|
||||
|
||||
![Popup][popup.gif]
|
||||
|
||||
## Scrollbar
|
||||
@@ -238,10 +239,6 @@ cargo run --example=scrollbar --features=crossterm
|
||||
Demonstrates the [`Sparkline`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Sparkline.html)
|
||||
widget. Source: [sparkline.rs](./sparkline.rs).
|
||||
|
||||
> [!NOTE] The background renders poorly in the second sparkline when we generate this example using
|
||||
> VHS. This problem doesn't generally happen during normal rendering in a terminal. See
|
||||
> [vhs#344](https://github.com/charmbracelet/vhs/issues/344) for more details.
|
||||
|
||||
```shell
|
||||
cargo run --example=sparkline --features=crossterm
|
||||
```
|
||||
@@ -292,15 +289,17 @@ To update these examples in bulk:
|
||||
examples/generate.bash
|
||||
```
|
||||
-->
|
||||
|
||||
[barchart.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/barchart.gif?raw=true
|
||||
[block.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/block.gif?raw=true
|
||||
[calendar.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/calendar.gif?raw=true
|
||||
[canvas.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/canvas.gif?raw=true
|
||||
[chart.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/chart.gif?raw=true
|
||||
[colors.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/colors.gif?raw=true
|
||||
[colors_rgb.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/colors_rgb.gif?raw=true
|
||||
[colors_rgb.png]: https://github.com/ratatui-org/ratatui/blob/images/examples/colors_rgb.png?raw=true
|
||||
[custom_widget.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/custom_widget.gif?raw=true
|
||||
[demo.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/demo.gif?raw=true
|
||||
[demo2.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/demo2.gif?raw=true
|
||||
[gauge.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/gauge.gif?raw=true
|
||||
[hello_world.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/hello_world.gif?raw=true
|
||||
[inline.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/inline.gif?raw=true
|
||||
|
||||
@@ -119,9 +119,7 @@ fn run_app<B: Backend>(
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &app))?;
|
||||
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
@@ -136,10 +134,10 @@ 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())
|
||||
.constraints([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)])
|
||||
.split(f.size());
|
||||
|
||||
let barchart = BarChart::default()
|
||||
@@ -152,7 +150,7 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
.split(chunks[1]);
|
||||
|
||||
draw_bar_with_group_labels(f, app, chunks[0]);
|
||||
@@ -198,10 +196,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 +223,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 +252,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,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/barchart.tape`
|
||||
Output "target/barchart.gif"
|
||||
Set Theme "OceanicMaterial"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 1200
|
||||
Set Height 800
|
||||
Hide
|
||||
|
||||
@@ -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>>;
|
||||
|
||||
@@ -106,18 +105,18 @@ fn ui(frame: &mut Frame) {
|
||||
fn calculate_layout(area: Rect) -> (Rect, Vec<Vec<Rect>>) {
|
||||
let layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(vec![Constraint::Length(1), Constraint::Min(0)])
|
||||
.constraints([Constraint::Length(1), Constraint::Min(0)])
|
||||
.split(area);
|
||||
let title_area = layout[0];
|
||||
let main_areas = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(vec![Constraint::Max(4); 9])
|
||||
.constraints([Constraint::Max(4); 9])
|
||||
.split(layout[1])
|
||||
.iter()
|
||||
.map(|&area| {
|
||||
Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(vec![Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
.split(area)
|
||||
.to_vec()
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/block.tape`
|
||||
Output "target/block.gif"
|
||||
Set Theme "OceanicMaterial"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 1200
|
||||
Set Height 1200
|
||||
Hide
|
||||
|
||||
@@ -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 {
|
||||
@@ -69,14 +69,11 @@ fn split_rows(area: &Rect) -> Rc<[Rect]> {
|
||||
let list_layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(0)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(33),
|
||||
Constraint::Percentage(33),
|
||||
Constraint::Percentage(33),
|
||||
]
|
||||
.as_ref(),
|
||||
);
|
||||
.constraints([
|
||||
Constraint::Percentage(33),
|
||||
Constraint::Percentage(33),
|
||||
Constraint::Percentage(33),
|
||||
]);
|
||||
|
||||
list_layout.split(*area)
|
||||
}
|
||||
@@ -85,15 +82,12 @@ fn split_cols(area: &Rect) -> Rc<[Rect]> {
|
||||
let list_layout = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.margin(0)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
]
|
||||
.as_ref(),
|
||||
);
|
||||
.constraints([
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
]);
|
||||
|
||||
list_layout.split(*area)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/calendar.tape`
|
||||
Output "target/calendar.gif"
|
||||
Set Theme "OceanicMaterial"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 1200
|
||||
Set Height 800
|
||||
Hide
|
||||
|
||||
@@ -1,28 +1,29 @@
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
io::{self, stdout, Stdout},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
event::{self, Event, KeyCode},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
};
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{canvas::*, *},
|
||||
};
|
||||
|
||||
fn main() -> io::Result<()> {
|
||||
App::run()
|
||||
}
|
||||
|
||||
struct App {
|
||||
x: f64,
|
||||
y: f64,
|
||||
ball: Rectangle,
|
||||
ball: Circle,
|
||||
playground: Rect,
|
||||
vx: f64,
|
||||
vy: f64,
|
||||
dir_x: bool,
|
||||
dir_y: bool,
|
||||
tick_count: u64,
|
||||
marker: Marker,
|
||||
}
|
||||
@@ -32,155 +33,166 @@ impl App {
|
||||
App {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
ball: Rectangle {
|
||||
x: 10.0,
|
||||
y: 30.0,
|
||||
width: 10.0,
|
||||
height: 10.0,
|
||||
ball: Circle {
|
||||
x: 20.0,
|
||||
y: 40.0,
|
||||
radius: 10.0,
|
||||
color: Color::Yellow,
|
||||
},
|
||||
playground: Rect::new(10, 10, 100, 100),
|
||||
playground: Rect::new(10, 10, 200, 100),
|
||||
vx: 1.0,
|
||||
vy: 1.0,
|
||||
dir_x: true,
|
||||
dir_y: true,
|
||||
tick_count: 0,
|
||||
marker: Marker::Dot,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_tick(&mut self) {
|
||||
self.tick_count += 1;
|
||||
// only change marker every 4 ticks (1s) to avoid stroboscopic effect
|
||||
if (self.tick_count % 4) == 0 {
|
||||
self.marker = match self.marker {
|
||||
Marker::Dot => Marker::Block,
|
||||
Marker::Block => Marker::Bar,
|
||||
Marker::Bar => Marker::Braille,
|
||||
Marker::Braille => Marker::Dot,
|
||||
};
|
||||
}
|
||||
if self.ball.x < self.playground.left() as f64
|
||||
|| self.ball.x + self.ball.width > self.playground.right() as f64
|
||||
{
|
||||
self.dir_x = !self.dir_x;
|
||||
}
|
||||
if self.ball.y < self.playground.top() as f64
|
||||
|| self.ball.y + self.ball.height > self.playground.bottom() as f64
|
||||
{
|
||||
self.dir_y = !self.dir_y;
|
||||
}
|
||||
|
||||
if self.dir_x {
|
||||
self.ball.x += self.vx;
|
||||
} else {
|
||||
self.ball.x -= self.vx;
|
||||
}
|
||||
|
||||
if self.dir_y {
|
||||
self.ball.y += self.vy;
|
||||
} else {
|
||||
self.ball.y -= self.vy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// create app and run it
|
||||
let tick_rate = Duration::from_millis(250);
|
||||
let app = App::new();
|
||||
let res = run_app(&mut terminal, app, tick_rate);
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> io::Result<()> {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &app))?;
|
||||
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
if event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => {
|
||||
return Ok(());
|
||||
pub fn run() -> io::Result<()> {
|
||||
let mut terminal = init_terminal()?;
|
||||
let mut app = App::new();
|
||||
let mut last_tick = Instant::now();
|
||||
let tick_rate = Duration::from_millis(16);
|
||||
loop {
|
||||
let _ = terminal.draw(|frame| app.ui(frame));
|
||||
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
|
||||
if event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => break,
|
||||
KeyCode::Down => app.y += 1.0,
|
||||
KeyCode::Up => app.y -= 1.0,
|
||||
KeyCode::Right => app.x += 1.0,
|
||||
KeyCode::Left => app.x -= 1.0,
|
||||
_ => {}
|
||||
}
|
||||
KeyCode::Down => {
|
||||
app.y += 1.0;
|
||||
}
|
||||
KeyCode::Up => {
|
||||
app.y -= 1.0;
|
||||
}
|
||||
KeyCode::Right => {
|
||||
app.x += 1.0;
|
||||
}
|
||||
KeyCode::Left => {
|
||||
app.x -= 1.0;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
app.on_tick();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
restore_terminal()
|
||||
}
|
||||
|
||||
fn on_tick(&mut self) {
|
||||
self.tick_count += 1;
|
||||
// only change marker every 180 ticks (3s) to avoid stroboscopic effect
|
||||
if (self.tick_count % 180) == 0 {
|
||||
self.marker = match self.marker {
|
||||
Marker::Dot => Marker::Braille,
|
||||
Marker::Braille => Marker::Block,
|
||||
Marker::Block => Marker::HalfBlock,
|
||||
Marker::HalfBlock => Marker::Bar,
|
||||
Marker::Bar => Marker::Dot,
|
||||
};
|
||||
}
|
||||
// bounce the ball by flipping the velocity vector
|
||||
let ball = &self.ball;
|
||||
let playground = self.playground;
|
||||
if ball.x - ball.radius < playground.left() as f64
|
||||
|| ball.x + ball.radius > playground.right() as f64
|
||||
{
|
||||
self.vx = -self.vx;
|
||||
}
|
||||
if ball.y - ball.radius < playground.top() as f64
|
||||
|| ball.y + ball.radius > playground.bottom() as f64
|
||||
{
|
||||
self.vy = -self.vy;
|
||||
}
|
||||
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
app.on_tick();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
self.ball.x += self.vx;
|
||||
self.ball.y += self.vy;
|
||||
}
|
||||
|
||||
fn ui(&self, frame: &mut Frame) {
|
||||
let main_layout = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
.split(frame.size());
|
||||
|
||||
let right_layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
.split(main_layout[1]);
|
||||
|
||||
frame.render_widget(self.map_canvas(), main_layout[0]);
|
||||
frame.render_widget(self.pong_canvas(), right_layout[0]);
|
||||
frame.render_widget(self.boxes_canvas(right_layout[1]), right_layout[1]);
|
||||
}
|
||||
|
||||
fn map_canvas(&self) -> impl Widget + '_ {
|
||||
Canvas::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("World"))
|
||||
.marker(self.marker)
|
||||
.paint(|ctx| {
|
||||
ctx.draw(&Map {
|
||||
color: Color::Green,
|
||||
resolution: MapResolution::High,
|
||||
});
|
||||
ctx.print(self.x, -self.y, "You are here".yellow());
|
||||
})
|
||||
.x_bounds([-180.0, 180.0])
|
||||
.y_bounds([-90.0, 90.0])
|
||||
}
|
||||
|
||||
fn pong_canvas(&self) -> impl Widget + '_ {
|
||||
Canvas::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("Pong"))
|
||||
.marker(self.marker)
|
||||
.paint(|ctx| {
|
||||
ctx.draw(&self.ball);
|
||||
})
|
||||
.x_bounds([10.0, 210.0])
|
||||
.y_bounds([10.0, 110.0])
|
||||
}
|
||||
|
||||
fn boxes_canvas(&self, area: Rect) -> impl Widget {
|
||||
let (left, right, bottom, top) =
|
||||
(0.0, area.width as f64, 0.0, area.height as f64 * 2.0 - 4.0);
|
||||
Canvas::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("Rects"))
|
||||
.marker(self.marker)
|
||||
.x_bounds([left, right])
|
||||
.y_bounds([bottom, top])
|
||||
.paint(|ctx| {
|
||||
for i in 0..=11 {
|
||||
ctx.draw(&Rectangle {
|
||||
x: (i * i + 3 * i) as f64 / 2.0 + 2.0,
|
||||
y: 2.0,
|
||||
width: i as f64,
|
||||
height: i as f64,
|
||||
color: Color::Red,
|
||||
});
|
||||
ctx.draw(&Rectangle {
|
||||
x: (i * i + 3 * i) as f64 / 2.0 + 2.0,
|
||||
y: 21.0,
|
||||
width: i as f64,
|
||||
height: i as f64,
|
||||
color: Color::Blue,
|
||||
});
|
||||
}
|
||||
for i in 0..100 {
|
||||
if i % 10 != 0 {
|
||||
ctx.print(i as f64 + 1.0, 0.0, format!("{i}", i = i % 10));
|
||||
}
|
||||
if i % 2 == 0 && i % 10 != 0 {
|
||||
ctx.print(0.0, i as f64, format!("{i}", i = i % 10));
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(f.size());
|
||||
let canvas = Canvas::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("World"))
|
||||
.marker(app.marker)
|
||||
.paint(|ctx| {
|
||||
ctx.draw(&Map {
|
||||
color: Color::White,
|
||||
resolution: MapResolution::High,
|
||||
});
|
||||
ctx.print(app.x, -app.y, "You are here".yellow());
|
||||
})
|
||||
.x_bounds([-180.0, 180.0])
|
||||
.y_bounds([-90.0, 90.0]);
|
||||
f.render_widget(canvas, chunks[0]);
|
||||
let canvas = Canvas::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("Pong"))
|
||||
.marker(app.marker)
|
||||
.paint(|ctx| {
|
||||
ctx.draw(&app.ball);
|
||||
})
|
||||
.x_bounds([10.0, 110.0])
|
||||
.y_bounds([10.0, 110.0]);
|
||||
f.render_widget(canvas, chunks[1]);
|
||||
fn init_terminal() -> io::Result<Terminal<CrosstermBackend<Stdout>>> {
|
||||
enable_raw_mode()?;
|
||||
stdout().execute(EnterAlternateScreen)?;
|
||||
Terminal::new(CrosstermBackend::new(stdout()))
|
||||
}
|
||||
|
||||
fn restore_terminal() -> io::Result<()> {
|
||||
disable_raw_mode()?;
|
||||
stdout().execute(LeaveAlternateScreen)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/canvas.tape`
|
||||
Output "target/canvas.gif"
|
||||
Set Theme "OceanicMaterial"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set FontSize 12
|
||||
Set Width 1200
|
||||
Set Height 800
|
||||
Hide
|
||||
Type "cargo run --example=canvas --features=crossterm"
|
||||
Enter
|
||||
Sleep 1s
|
||||
Sleep 2s
|
||||
Show
|
||||
Sleep 5s
|
||||
Sleep 30s
|
||||
|
||||
@@ -125,9 +125,7 @@ fn run_app<B: Backend>(
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &app))?;
|
||||
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
@@ -142,18 +140,15 @@ 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)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Ratio(1, 3),
|
||||
Constraint::Ratio(1, 3),
|
||||
Constraint::Ratio(1, 3),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.constraints([
|
||||
Constraint::Ratio(1, 3),
|
||||
Constraint::Ratio(1, 3),
|
||||
Constraint::Ratio(1, 3),
|
||||
])
|
||||
.split(size);
|
||||
let x_labels = vec![
|
||||
Span::styled(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/chart.tape`
|
||||
Output "target/chart.gif"
|
||||
Set Theme "OceanicMaterial"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 1200
|
||||
Set Height 800
|
||||
Hide
|
||||
|
||||
@@ -41,10 +41,10 @@ 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![
|
||||
.constraints([
|
||||
Constraint::Length(30),
|
||||
Constraint::Length(17),
|
||||
Constraint::Length(2),
|
||||
@@ -75,10 +75,10 @@ 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])
|
||||
.constraints([Constraint::Length(3); 10])
|
||||
.split(area);
|
||||
|
||||
render_fg_named_colors(frame, Color::Reset, layout[0]);
|
||||
@@ -94,20 +94,20 @@ 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);
|
||||
|
||||
let layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(vec![Constraint::Length(1); 2])
|
||||
.constraints([Constraint::Length(1); 2])
|
||||
.split(inner)
|
||||
.iter()
|
||||
.flat_map(|area| {
|
||||
Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(vec![Constraint::Ratio(1, 8); 8])
|
||||
.constraints([Constraint::Ratio(1, 8); 8])
|
||||
.split(*area)
|
||||
.to_vec()
|
||||
})
|
||||
@@ -119,20 +119,20 @@ 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);
|
||||
|
||||
let layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(vec![Constraint::Length(1); 2])
|
||||
.constraints([Constraint::Length(1); 2])
|
||||
.split(inner)
|
||||
.iter()
|
||||
.flat_map(|area| {
|
||||
Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(vec![Constraint::Ratio(1, 8); 8])
|
||||
.constraints([Constraint::Ratio(1, 8); 8])
|
||||
.split(*area)
|
||||
.to_vec()
|
||||
})
|
||||
@@ -144,14 +144,14 @@ 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);
|
||||
|
||||
let layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(vec![
|
||||
.constraints([
|
||||
Constraint::Length(1), // 0 - 15
|
||||
Constraint::Length(1), // blank
|
||||
Constraint::Min(6), // 16 - 123
|
||||
@@ -164,7 +164,7 @@ fn render_indexed_colors<B: Backend>(frame: &mut Frame<B>, area: Rect) {
|
||||
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
||||
let color_layout = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(vec![Constraint::Length(5); 16])
|
||||
.constraints([Constraint::Length(5); 16])
|
||||
.split(layout[0]);
|
||||
for i in 0..16 {
|
||||
let color = Color::Indexed(i);
|
||||
@@ -198,7 +198,7 @@ fn render_indexed_colors<B: Backend>(frame: &mut Frame<B>, area: Rect) {
|
||||
.flat_map(|area| {
|
||||
Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(vec![Constraint::Length(27); 3])
|
||||
.constraints([Constraint::Length(27); 3])
|
||||
.split(*area)
|
||||
.to_vec()
|
||||
})
|
||||
@@ -206,7 +206,7 @@ fn render_indexed_colors<B: Backend>(frame: &mut Frame<B>, area: Rect) {
|
||||
.flat_map(|area| {
|
||||
Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(vec![Constraint::Length(1); 6])
|
||||
.constraints([Constraint::Length(1); 6])
|
||||
.split(area)
|
||||
.to_vec()
|
||||
})
|
||||
@@ -214,7 +214,7 @@ fn render_indexed_colors<B: Backend>(frame: &mut Frame<B>, area: Rect) {
|
||||
.flat_map(|area| {
|
||||
Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(vec![Constraint::Min(4); 6])
|
||||
.constraints([Constraint::Min(4); 6])
|
||||
.split(area)
|
||||
.to_vec()
|
||||
})
|
||||
@@ -243,10 +243,10 @@ 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![
|
||||
.constraints([
|
||||
Constraint::Length(1), // 232 - 243
|
||||
Constraint::Length(1), // 244 - 255
|
||||
])
|
||||
@@ -255,7 +255,7 @@ fn render_indexed_grayscale<B: Backend>(frame: &mut Frame<B>, area: Rect) {
|
||||
.flat_map(|area| {
|
||||
Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(vec![Constraint::Length(6); 12])
|
||||
.constraints([Constraint::Length(6); 12])
|
||||
.split(*area)
|
||||
.to_vec()
|
||||
})
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/colors.tape`
|
||||
Output "target/colors.gif"
|
||||
# The OceanicMaterial theme is a good choice for this example (Obsidian is almost as good) because:
|
||||
# - Black is dark and distinct from the default background
|
||||
# - White is light and distinct from the default foreground
|
||||
# - Normal and bright colors are distinct
|
||||
# - Black and DarkGray are distinct
|
||||
# - White and Gray are distinct
|
||||
Set Theme "OceanicMaterial"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 1200
|
||||
Set Height 1410
|
||||
Hide
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +89,7 @@ impl RgbColors {
|
||||
fn layout(area: Rect) -> Rc<[Rect]> {
|
||||
Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(vec![Constraint::Length(1), Constraint::Min(0)])
|
||||
.constraints([Constraint::Length(1), Constraint::Min(0)])
|
||||
.split(area)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/colors_rgb.tape`
|
||||
Output "target/colors_rgb.gif"
|
||||
# The OceanicMaterial theme is a good choice for this example (Obsidian is almost as good) because:
|
||||
# - Black is dark and distinct from the default background
|
||||
# - White is light and distinct from the default foreground
|
||||
# - Normal and bright colors are distinct
|
||||
# - Black and DarkGray are distinct
|
||||
# - White and Gray are distinct
|
||||
Set Theme "OceanicMaterial"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 1200
|
||||
Set Height 1410
|
||||
Set Height 800
|
||||
Hide
|
||||
Type "cargo run --example=colors_rgb --features=crossterm"
|
||||
Enter
|
||||
Sleep 2s
|
||||
Screenshot "target/colors_rgb.png"
|
||||
Show
|
||||
Sleep 1s
|
||||
|
||||
@@ -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,126 @@ 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([
|
||||
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([
|
||||
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 {
|
||||
if button_states[old_selected_button] != State::Active {
|
||||
button_states[old_selected_button] = State::Normal;
|
||||
}
|
||||
if button_states[*selected_button] != State::Active {
|
||||
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;
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# 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 Theme "Aardvark Blue"
|
||||
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
|
||||
@@ -1,7 +1,7 @@
|
||||
# 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/demo.gif"
|
||||
Set Theme "OceanicMaterial"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 1200
|
||||
Set Height 1200
|
||||
Set PlaybackSpeed 0.5
|
||||
|
||||
@@ -50,9 +50,7 @@ fn run_app<B: Backend>(
|
||||
loop {
|
||||
terminal.draw(|f| ui::draw(f, &mut app))?;
|
||||
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
@@ -32,19 +31,17 @@ fn run_app(
|
||||
terminal: &mut Terminal<TermwizBackend>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> io::Result<()> {
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|f| ui::draw(f, &mut app))?;
|
||||
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
if let Ok(Some(input)) = terminal
|
||||
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
|
||||
if let Some(input) = terminal
|
||||
.backend_mut()
|
||||
.buffered_terminal_mut()
|
||||
.terminal()
|
||||
.poll_input(Some(timeout))
|
||||
.poll_input(Some(timeout))?
|
||||
{
|
||||
match input {
|
||||
InputEvent::Key(key_code) => match key_code.key {
|
||||
|
||||
@@ -5,9 +5,9 @@ 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())
|
||||
.constraints([Constraint::Length(3), Constraint::Min(0)])
|
||||
.split(f.size());
|
||||
let titles = app
|
||||
.tabs
|
||||
@@ -28,38 +28,26 @@ 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(
|
||||
[
|
||||
Constraint::Length(9),
|
||||
Constraint::Min(8),
|
||||
Constraint::Length(7),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.constraints([
|
||||
Constraint::Length(9),
|
||||
Constraint::Min(8),
|
||||
Constraint::Length(7),
|
||||
])
|
||||
.split(area);
|
||||
draw_gauges(f, app, chunks[0]);
|
||||
draw_charts(f, app, chunks[1]);
|
||||
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(
|
||||
[
|
||||
Constraint::Length(2),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(1),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.constraints([
|
||||
Constraint::Length(2),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(1),
|
||||
])
|
||||
.margin(1)
|
||||
.split(area);
|
||||
let block = Block::default().borders(Borders::ALL).title("Graphs");
|
||||
@@ -102,10 +90,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 {
|
||||
@@ -117,11 +102,11 @@ where
|
||||
.split(area);
|
||||
{
|
||||
let chunks = Layout::default()
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
.split(chunks[0]);
|
||||
{
|
||||
let chunks = Layout::default()
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
.direction(Direction::Horizontal)
|
||||
.split(chunks[0]);
|
||||
|
||||
@@ -249,10 +234,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,12 +272,9 @@ 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())
|
||||
.constraints([Constraint::Percentage(30), Constraint::Percentage(70)])
|
||||
.direction(Direction::Horizontal)
|
||||
.split(area);
|
||||
let up_style = Style::default().fg(Color::Green);
|
||||
@@ -379,10 +358,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)])
|
||||
|
||||
43
examples/demo2-social.tape
Normal file
43
examples/demo2-social.tape
Normal 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 "Aardvark Blue"
|
||||
# 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
49
examples/demo2.tape
Normal 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 "Aardvark Blue"
|
||||
# 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
99
examples/demo2/app.rs
Normal 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
34
examples/demo2/colors.rs
Normal 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
17
examples/demo2/main.rs
Normal 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
93
examples/demo2/root.rs
Normal 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
11
examples/demo2/tabs.rs
Normal 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;
|
||||
157
examples/demo2/tabs/about.rs
Normal file
157
examples/demo2/tabs/about.rs
Normal 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;
|
||||
}
|
||||
(_, _) => {}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
143
examples/demo2/tabs/email.rs
Normal file
143
examples/demo2/tabs/email.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
171
examples/demo2/tabs/recipe.rs
Normal file
171
examples/demo2/tabs/recipe.rs
Normal 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)
|
||||
}
|
||||
199
examples/demo2/tabs/traceroute.rs
Normal file
199
examples/demo2/tabs/traceroute.rs
Normal 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([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::HalfBlock)
|
||||
// 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([112.0, 155.0])
|
||||
.y_bounds([-46.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),
|
||||
];
|
||||
141
examples/demo2/tabs/weather.rs
Normal file
141
examples/demo2/tabs/weather.rs
Normal 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
71
examples/demo2/term.rs
Normal 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
136
examples/demo2/theme.rs
Normal 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
126
examples/docsrs.rs
Normal 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([
|
||||
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([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([
|
||||
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
39
examples/docsrs.tape
Normal 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 "Aardvark Blue"
|
||||
# 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"
|
||||
@@ -1,17 +1,32 @@
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
io::{self, stdout, Stdout},
|
||||
rc::Rc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
event::{self, Event, KeyCode, KeyEventKind},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{block::Title, *},
|
||||
};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
App::run()
|
||||
}
|
||||
|
||||
struct App {
|
||||
term: Term,
|
||||
should_quit: bool,
|
||||
state: AppState,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
struct AppState {
|
||||
progress1: u16,
|
||||
progress2: u16,
|
||||
progress3: f64,
|
||||
@@ -19,141 +34,154 @@ struct App {
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn new() -> App {
|
||||
App {
|
||||
progress1: 0,
|
||||
progress2: 0,
|
||||
progress3: 0.45,
|
||||
progress4: 0,
|
||||
fn run() -> Result<()> {
|
||||
// run at ~10 fps minus the time it takes to draw
|
||||
let timeout = Duration::from_secs_f32(1.0 / 10.0);
|
||||
let mut app = Self::start()?;
|
||||
while !app.should_quit {
|
||||
app.update();
|
||||
app.draw()?;
|
||||
app.handle_events(timeout)?;
|
||||
}
|
||||
app.stop()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn on_tick(&mut self) {
|
||||
self.progress1 += 1;
|
||||
if self.progress1 > 100 {
|
||||
self.progress1 = 0;
|
||||
}
|
||||
self.progress2 += 2;
|
||||
if self.progress2 > 100 {
|
||||
self.progress2 = 0;
|
||||
}
|
||||
self.progress3 += 0.001;
|
||||
if self.progress3 > 1.0 {
|
||||
self.progress3 = 0.0;
|
||||
}
|
||||
self.progress4 += 1;
|
||||
if self.progress4 > 100 {
|
||||
self.progress4 = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// create app and run it
|
||||
let tick_rate = Duration::from_millis(250);
|
||||
let app = App::new();
|
||||
let res = run_app(&mut terminal, app, tick_rate);
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{err:?}");
|
||||
fn start() -> Result<Self> {
|
||||
Ok(App {
|
||||
term: Term::start()?,
|
||||
should_quit: false,
|
||||
state: AppState {
|
||||
progress1: 0,
|
||||
progress2: 0,
|
||||
progress3: 0.0,
|
||||
progress4: 0,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
fn stop(&mut self) -> Result<()> {
|
||||
Term::stop()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> io::Result<()> {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &app))?;
|
||||
fn update(&mut self) {
|
||||
self.state.progress1 = (self.state.progress1 + 4).min(100);
|
||||
self.state.progress2 = (self.state.progress2 + 3).min(100);
|
||||
self.state.progress3 = (self.state.progress3 + 0.02).min(1.0);
|
||||
self.state.progress4 = (self.state.progress4 + 1).min(100);
|
||||
}
|
||||
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
if crossterm::event::poll(timeout)? {
|
||||
fn draw(&mut self) -> Result<()> {
|
||||
self.term.draw(|frame| {
|
||||
let state = self.state;
|
||||
let layout = Self::equal_layout(frame);
|
||||
Self::render_gauge1(state.progress1, frame, layout[0]);
|
||||
Self::render_gauge2(state.progress2, frame, layout[1]);
|
||||
Self::render_gauge3(state.progress3, frame, layout[2]);
|
||||
Self::render_gauge4(state.progress4, frame, layout[3]);
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_events(&mut self, timeout: Duration) -> io::Result<()> {
|
||||
if event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
return Ok(());
|
||||
if key.kind == KeyEventKind::Press {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
self.should_quit = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
app.on_tick();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn equal_layout(frame: &Frame) -> Rc<[Rect]> {
|
||||
Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
])
|
||||
.split(frame.size())
|
||||
}
|
||||
|
||||
fn render_gauge1(progress: u16, frame: &mut Frame, area: Rect) {
|
||||
let title = Self::title_block("Gauge with percentage progress");
|
||||
let gauge = Gauge::default()
|
||||
.block(title)
|
||||
.gauge_style(Style::new().light_red())
|
||||
.percent(progress);
|
||||
frame.render_widget(gauge, area);
|
||||
}
|
||||
|
||||
fn render_gauge2(progress: u16, frame: &mut Frame, area: Rect) {
|
||||
let title = Self::title_block("Gauge with percentage progress and custom label");
|
||||
let label = format!("{}/100", progress);
|
||||
let gauge = Gauge::default()
|
||||
.block(title)
|
||||
.gauge_style(Style::new().blue().on_light_blue())
|
||||
.percent(progress)
|
||||
.label(label);
|
||||
frame.render_widget(gauge, area);
|
||||
}
|
||||
|
||||
fn render_gauge3(progress: f64, frame: &mut Frame, area: Rect) {
|
||||
let title =
|
||||
Self::title_block("Gauge with ratio progress, custom label with style, and unicode");
|
||||
let label = Span::styled(
|
||||
format!("{:.2}%", progress * 100.0),
|
||||
Style::new().red().italic().bold(),
|
||||
);
|
||||
let gauge = Gauge::default()
|
||||
.block(title)
|
||||
.gauge_style(Style::default().fg(Color::Yellow))
|
||||
.ratio(progress)
|
||||
.label(label)
|
||||
.use_unicode(true);
|
||||
frame.render_widget(gauge, area);
|
||||
}
|
||||
|
||||
fn render_gauge4(progress: u16, frame: &mut Frame, area: Rect) {
|
||||
let title = Self::title_block("Gauge with percentage progress and label");
|
||||
let label = format!("{}/100", progress);
|
||||
let gauge = Gauge::default()
|
||||
.block(title)
|
||||
.gauge_style(Style::new().green().italic())
|
||||
.percent(progress)
|
||||
.label(label);
|
||||
frame.render_widget(gauge, area);
|
||||
}
|
||||
|
||||
fn title_block(title: &str) -> Block {
|
||||
let title = Title::from(title).alignment(Alignment::Center);
|
||||
Block::default().title(title).borders(Borders::TOP)
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(f.size());
|
||||
|
||||
let gauge = Gauge::default()
|
||||
.block(Block::default().title("Gauge1").borders(Borders::ALL))
|
||||
.gauge_style(Style::default().fg(Color::Yellow))
|
||||
.percent(app.progress1);
|
||||
f.render_widget(gauge, chunks[0]);
|
||||
|
||||
let label = format!("{}/100", app.progress2);
|
||||
let gauge = Gauge::default()
|
||||
.block(Block::default().title("Gauge2").borders(Borders::ALL))
|
||||
.gauge_style(Style::default().fg(Color::Magenta).bg(Color::Green))
|
||||
.percent(app.progress2)
|
||||
.label(label);
|
||||
f.render_widget(gauge, chunks[1]);
|
||||
|
||||
let label = Span::styled(
|
||||
format!("{:.2}%", app.progress3 * 100.0),
|
||||
Style::default()
|
||||
.fg(Color::Red)
|
||||
.add_modifier(Modifier::ITALIC | Modifier::BOLD),
|
||||
);
|
||||
let gauge = Gauge::default()
|
||||
.block(Block::default().title("Gauge3").borders(Borders::ALL))
|
||||
.gauge_style(Style::default().fg(Color::Yellow))
|
||||
.ratio(app.progress3)
|
||||
.label(label)
|
||||
.use_unicode(true);
|
||||
f.render_widget(gauge, chunks[2]);
|
||||
|
||||
let label = format!("{}/100", app.progress4);
|
||||
let gauge = Gauge::default()
|
||||
.block(Block::default().title("Gauge4").borders(Borders::ALL))
|
||||
.gauge_style(
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::ITALIC),
|
||||
)
|
||||
.percent(app.progress4)
|
||||
.label(label);
|
||||
f.render_widget(gauge, chunks[3]);
|
||||
struct Term {
|
||||
terminal: Terminal<CrosstermBackend<Stdout>>,
|
||||
}
|
||||
|
||||
impl Term {
|
||||
pub fn start() -> io::Result<Term> {
|
||||
stdout().execute(EnterAlternateScreen)?;
|
||||
enable_raw_mode()?;
|
||||
let terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
|
||||
Ok(Self { terminal })
|
||||
}
|
||||
|
||||
pub fn stop() -> io::Result<()> {
|
||||
disable_raw_mode()?;
|
||||
stdout().execute(LeaveAlternateScreen)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: impl FnOnce(&mut Frame)) -> Result<()> {
|
||||
self.terminal.draw(frame)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/gauge.tape`
|
||||
Output "target/gauge.gif"
|
||||
Set Theme "OceanicMaterial"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 1200
|
||||
Set Height 600
|
||||
Set Height 550
|
||||
Hide
|
||||
Type "cargo run --example=gauge --features=crossterm"
|
||||
Enter
|
||||
|
||||
@@ -5,7 +5,11 @@
|
||||
# - cargo: https://doc.rust-lang.org/cargo/getting-started/installation.html
|
||||
# - gh: https://github.com/cli/cli
|
||||
# - git: https://git-scm.com/
|
||||
# - vhs: https://github.com/charmbracelet/vhs
|
||||
# - vhs: https://github.com/charmbracelet/vhs - currently this needs to be installed from the
|
||||
# main branch, as the latest release doesn't support the theme we use or the Screenshot
|
||||
# command. Install using `go install github.com/charmbracelet/vhs@main``
|
||||
# - go: https://golang.org/doc/install
|
||||
# - ttyd: https://github.com/tsl0922/ttyd
|
||||
|
||||
# Exit on error. Append "|| true" if you expect an error.
|
||||
set -o errexit
|
||||
@@ -21,11 +25,10 @@ set -o pipefail
|
||||
# ensure that running each example doesn't have to wait for the build
|
||||
cargo build --examples --features=crossterm,all-widgets
|
||||
|
||||
for tape in examples/*.tape
|
||||
do
|
||||
gif=${tape/examples\/}
|
||||
for tape in examples/*.tape; do
|
||||
gif=${tape/examples\//}
|
||||
gif=${gif/.tape/.gif}
|
||||
vhs $tape --quiet
|
||||
~/go/bin/vhs $tape --quiet
|
||||
# this can be pasted into the examples README.md
|
||||
echo "[${gif}]: https://github.com/ratatui-org/ratatui/blob/images/examples/${gif}?raw=true"
|
||||
done
|
||||
@@ -35,4 +38,4 @@ cp target/*.gif examples/
|
||||
git add examples/*.gif
|
||||
git commit -m 'docs(examples): update images'
|
||||
gh pr create
|
||||
git sw main
|
||||
git sw -
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/hello_world.tape`
|
||||
Output "target/hello_world.gif"
|
||||
Set Theme "OceanicMaterial"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 1200
|
||||
Set Height 200
|
||||
Hide
|
||||
|
||||
@@ -98,9 +98,7 @@ fn input_handling(tx: mpsc::Sender<Event>) {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
// poll for tick rate duration, if no events, sent tick event.
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
|
||||
if crossterm::event::poll(timeout).unwrap() {
|
||||
match crossterm::event::read().unwrap() {
|
||||
crossterm::event::Event::Key(key) => tx.send(Event::Input(key)).unwrap(),
|
||||
@@ -216,14 +214,14 @@ 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));
|
||||
f.render_widget(block, size);
|
||||
|
||||
let chunks = Layout::default()
|
||||
.constraints(vec![Constraint::Length(2), Constraint::Length(4)])
|
||||
.constraints([Constraint::Length(2), Constraint::Length(4)])
|
||||
.margin(1)
|
||||
.split(size);
|
||||
|
||||
@@ -237,7 +235,7 @@ fn ui<B: Backend>(f: &mut Frame<B>, downloads: &Downloads) {
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(vec![Constraint::Percentage(20), Constraint::Percentage(80)])
|
||||
.constraints([Constraint::Percentage(20), Constraint::Percentage(80)])
|
||||
.split(chunks[1]);
|
||||
|
||||
// in progress downloads
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/inline.tape`
|
||||
Output "target/inline.gif"
|
||||
Set Theme "OceanicMaterial"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 1200
|
||||
Set Height 600
|
||||
Type "cargo run --example=inline --features=crossterm"
|
||||
|
||||
@@ -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,10 +47,10 @@ 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![
|
||||
.constraints([
|
||||
Length(4), // text
|
||||
Length(50), // examples
|
||||
Min(0), // fills remaining space
|
||||
@@ -71,7 +71,7 @@ fn ui<B: Backend>(frame: &mut Frame<B>) {
|
||||
|
||||
let example_rows = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(vec![
|
||||
.constraints([
|
||||
Length(9),
|
||||
Length(9),
|
||||
Length(9),
|
||||
@@ -85,7 +85,7 @@ fn ui<B: Backend>(frame: &mut Frame<B>) {
|
||||
.flat_map(|area| {
|
||||
Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(vec![
|
||||
.constraints([
|
||||
Length(14),
|
||||
Length(14),
|
||||
Length(14),
|
||||
@@ -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();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/layout.tape`
|
||||
Output "target/layout.gif"
|
||||
Set Theme "OceanicMaterial"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 1200
|
||||
Set Height 1410
|
||||
Hide
|
||||
|
||||
@@ -175,9 +175,7 @@ fn run_app<B: Backend>(
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &mut app))?;
|
||||
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
@@ -198,11 +196,11 @@ 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)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
.split(f.size());
|
||||
|
||||
// Iterate through all elements in the `items` app and append some debug text to it.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/list.tape`
|
||||
Output "target/list.gif"
|
||||
Set Theme "OceanicMaterial"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 1200
|
||||
Set Height 600
|
||||
Hide
|
||||
|
||||
@@ -43,10 +43,10 @@ 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)])
|
||||
.constraints([Constraint::Length(1), Constraint::Min(0)])
|
||||
.split(frame.size());
|
||||
frame.render_widget(
|
||||
Paragraph::new("Note: not all terminals support all modifiers")
|
||||
@@ -55,13 +55,13 @@ fn ui<B: Backend>(frame: &mut Frame<B>) {
|
||||
);
|
||||
let layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(vec![Constraint::Length(1); 50])
|
||||
.constraints([Constraint::Length(1); 50])
|
||||
.split(layout[1])
|
||||
.iter()
|
||||
.flat_map(|area| {
|
||||
Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(vec![Constraint::Percentage(20); 5])
|
||||
.constraints([Constraint::Percentage(20); 5])
|
||||
.split(*area)
|
||||
.to_vec()
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/modifiers.tape`
|
||||
Output "target/modifiers.gif"
|
||||
Set Theme "OceanicMaterial"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 1200
|
||||
Set Height 1460
|
||||
Hide
|
||||
|
||||
@@ -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**")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/panic.tape`
|
||||
Output "target/panic.gif"
|
||||
Set Theme "OceanicMaterial"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 1200
|
||||
Set Height 600
|
||||
Type "cargo run --example=panic --features=crossterm"
|
||||
|
||||
@@ -64,9 +64,7 @@ fn run_app<B: Backend>(
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &app))?;
|
||||
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
@@ -81,7 +79,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.
|
||||
@@ -94,15 +92,12 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.constraints([
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
])
|
||||
.split(size);
|
||||
|
||||
let text = vec![
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/paragraph.tape`
|
||||
Output "target/paragraph.gif"
|
||||
Set Theme "OceanicMaterial"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 1200
|
||||
Set Height 1800
|
||||
Hide
|
||||
|
||||
@@ -61,11 +61,11 @@ 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()
|
||||
.constraints([Constraint::Percentage(20), Constraint::Percentage(80)].as_ref())
|
||||
.constraints([Constraint::Percentage(20), Constraint::Percentage(80)])
|
||||
.split(size);
|
||||
|
||||
let text = if app.show_popup {
|
||||
@@ -96,25 +96,19 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
|
||||
let popup_layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage((100 - percent_y) / 2),
|
||||
Constraint::Percentage(percent_y),
|
||||
Constraint::Percentage((100 - percent_y) / 2),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.constraints([
|
||||
Constraint::Percentage((100 - percent_y) / 2),
|
||||
Constraint::Percentage(percent_y),
|
||||
Constraint::Percentage((100 - percent_y) / 2),
|
||||
])
|
||||
.split(r);
|
||||
|
||||
Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage((100 - percent_x) / 2),
|
||||
Constraint::Percentage(percent_x),
|
||||
Constraint::Percentage((100 - percent_x) / 2),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.constraints([
|
||||
Constraint::Percentage((100 - percent_x) / 2),
|
||||
Constraint::Percentage(percent_x),
|
||||
Constraint::Percentage((100 - percent_x) / 2),
|
||||
])
|
||||
.split(popup_layout[1])[1]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/popup.tape`
|
||||
Output "target/popup.gif"
|
||||
Set Theme "OceanicMaterial"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 1200
|
||||
Set Height 600
|
||||
Hide
|
||||
|
||||
@@ -57,9 +57,7 @@ fn run_app<B: Backend>(
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &mut app))?;
|
||||
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
@@ -94,7 +92,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.
|
||||
@@ -107,16 +105,13 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Min(1),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.constraints([
|
||||
Constraint::Min(1),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
])
|
||||
.split(size);
|
||||
|
||||
let text = vec![
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/scrollbar.tape`
|
||||
Output "target/scrollbar.gif"
|
||||
Set Theme "OceanicMaterial"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 1200
|
||||
Set Height 1200
|
||||
Hide
|
||||
|
||||
@@ -109,9 +109,7 @@ fn run_app<B: Backend>(
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &app))?;
|
||||
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
@@ -126,17 +124,14 @@ 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::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Min(0),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.constraints([
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Min(0),
|
||||
])
|
||||
.split(f.size());
|
||||
let sparkline = Sparkline::default()
|
||||
.block(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/sparkline.tape`
|
||||
Output "target/sparkline.gif"
|
||||
Set Theme "OceanicMaterial"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 1200
|
||||
Set Height 600
|
||||
Hide
|
||||
|
||||
@@ -113,9 +113,9 @@ 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())
|
||||
.constraints([Constraint::Percentage(100)])
|
||||
.split(f.size());
|
||||
|
||||
let selected_style = Style::default().add_modifier(Modifier::REVERSED);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/table.tape`
|
||||
Output "target/table.gif"
|
||||
Set Theme "OceanicMaterial"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 1200
|
||||
Set Height 600
|
||||
Hide
|
||||
|
||||
@@ -78,11 +78,11 @@ 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)
|
||||
.constraints([Constraint::Length(3), Constraint::Min(0)].as_ref())
|
||||
.constraints([Constraint::Length(3), Constraint::Min(0)])
|
||||
.split(size);
|
||||
|
||||
let block = Block::default().on_white().black();
|
||||
@@ -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),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/tabs.tape`
|
||||
Output "target/tabs.gif"
|
||||
Set Theme "OceanicMaterial"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 1200
|
||||
Set Height 300
|
||||
Hide
|
||||
|
||||
@@ -171,17 +171,14 @@ 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(
|
||||
[
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(3),
|
||||
Constraint::Min(1),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.constraints([
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(3),
|
||||
Constraint::Min(1),
|
||||
])
|
||||
.split(f.size());
|
||||
|
||||
let (msg, style) = match app.input_mode {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/user_input.tape`
|
||||
Output "target/user_input.gif"
|
||||
Set Theme "OceanicMaterial"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 1200
|
||||
Set Height 600
|
||||
Hide
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,12 +4,14 @@
|
||||
//! [Crossterm]: https://crates.io/crates/crossterm
|
||||
use std::io::{self, Write};
|
||||
|
||||
#[cfg(feature = "underline-color")]
|
||||
use crossterm::style::SetUnderlineColor;
|
||||
use crossterm::{
|
||||
cursor::{Hide, MoveTo, Show},
|
||||
execute, queue,
|
||||
style::{
|
||||
Attribute as CAttribute, Color as CColor, Print, SetAttribute, SetBackgroundColor,
|
||||
SetForegroundColor, SetUnderlineColor,
|
||||
SetForegroundColor,
|
||||
},
|
||||
terminal::{self, Clear},
|
||||
};
|
||||
@@ -124,6 +126,7 @@ where
|
||||
{
|
||||
let mut fg = Color::Reset;
|
||||
let mut bg = Color::Reset;
|
||||
#[cfg(feature = "underline-color")]
|
||||
let mut underline_color = Color::Reset;
|
||||
let mut modifier = Modifier::empty();
|
||||
let mut last_pos: Option<(u16, u16)> = None;
|
||||
@@ -151,6 +154,7 @@ where
|
||||
queue!(self.writer, SetBackgroundColor(color))?;
|
||||
bg = cell.bg;
|
||||
}
|
||||
#[cfg(feature = "underline-color")]
|
||||
if cell.underline_color != underline_color {
|
||||
let color = CColor::from(cell.underline_color);
|
||||
queue!(self.writer, SetUnderlineColor(color))?;
|
||||
@@ -160,13 +164,21 @@ where
|
||||
queue!(self.writer, Print(&cell.symbol))?;
|
||||
}
|
||||
|
||||
queue!(
|
||||
#[cfg(feature = "underline-color")]
|
||||
return queue!(
|
||||
self.writer,
|
||||
SetForegroundColor(CColor::Reset),
|
||||
SetBackgroundColor(CColor::Reset),
|
||||
SetUnderlineColor(CColor::Reset),
|
||||
SetAttribute(CAttribute::Reset)
|
||||
)
|
||||
SetAttribute(CAttribute::Reset),
|
||||
);
|
||||
#[cfg(not(feature = "underline-color"))]
|
||||
return queue!(
|
||||
self.writer,
|
||||
SetForegroundColor(CColor::Reset),
|
||||
SetBackgroundColor(CColor::Reset),
|
||||
SetAttribute(CAttribute::Reset),
|
||||
);
|
||||
}
|
||||
|
||||
fn hide_cursor(&mut self) -> io::Result<()> {
|
||||
|
||||
@@ -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
|
||||
@@ -20,7 +19,7 @@ pub struct Cell {
|
||||
pub symbol: String,
|
||||
pub fg: Color,
|
||||
pub bg: Color,
|
||||
#[cfg(feature = "crossterm")]
|
||||
#[cfg(feature = "underline-color")]
|
||||
pub underline_color: Color,
|
||||
pub modifier: Modifier,
|
||||
pub skip: bool,
|
||||
@@ -56,7 +55,7 @@ impl Cell {
|
||||
if let Some(c) = style.bg {
|
||||
self.bg = c;
|
||||
}
|
||||
#[cfg(feature = "crossterm")]
|
||||
#[cfg(feature = "underline-color")]
|
||||
if let Some(c) = style.underline_color {
|
||||
self.underline_color = c;
|
||||
}
|
||||
@@ -65,7 +64,7 @@ impl Cell {
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(feature = "crossterm")]
|
||||
#[cfg(feature = "underline-color")]
|
||||
pub fn style(&self) -> Style {
|
||||
Style::default()
|
||||
.fg(self.fg)
|
||||
@@ -74,7 +73,7 @@ impl Cell {
|
||||
.add_modifier(self.modifier)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "crossterm"))]
|
||||
#[cfg(not(feature = "underline-color"))]
|
||||
pub fn style(&self) -> Style {
|
||||
Style::default()
|
||||
.fg(self.fg)
|
||||
@@ -96,7 +95,7 @@ impl Cell {
|
||||
self.symbol.push(' ');
|
||||
self.fg = Color::Reset;
|
||||
self.bg = Color::Reset;
|
||||
#[cfg(feature = "crossterm")]
|
||||
#[cfg(feature = "underline-color")]
|
||||
{
|
||||
self.underline_color = Color::Reset;
|
||||
}
|
||||
@@ -111,7 +110,7 @@ impl Default for Cell {
|
||||
symbol: " ".into(),
|
||||
fg: Color::Reset,
|
||||
bg: Color::Reset,
|
||||
#[cfg(feature = "crossterm")]
|
||||
#[cfg(feature = "underline-color")]
|
||||
underline_color: Color::Reset,
|
||||
modifier: Modifier::empty(),
|
||||
skip: false,
|
||||
@@ -139,7 +138,7 @@ impl Default for Cell {
|
||||
/// symbol: String::from("r"),
|
||||
/// fg: Color::Red,
|
||||
/// bg: Color::White,
|
||||
/// #[cfg(feature = "crossterm")]
|
||||
/// #[cfg(feature = "underline-color")]
|
||||
/// underline_color: Color::Reset,
|
||||
/// modifier: Modifier::empty(),
|
||||
/// skip: false
|
||||
@@ -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;
|
||||
@@ -584,7 +560,7 @@ impl Debug for Buffer {
|
||||
overwritten.push((x, &c.symbol));
|
||||
}
|
||||
skip = std::cmp::max(skip, c.symbol.width()).saturating_sub(1);
|
||||
#[cfg(feature = "crossterm")]
|
||||
#[cfg(feature = "underline-color")]
|
||||
{
|
||||
let style = (c.fg, c.bg, c.underline_color, c.modifier);
|
||||
if last_style != Some(style) {
|
||||
@@ -592,7 +568,7 @@ impl Debug for Buffer {
|
||||
styles.push((x, y, c.fg, c.bg, c.underline_color, c.modifier));
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "crossterm"))]
|
||||
#[cfg(not(feature = "underline-color"))]
|
||||
{
|
||||
let style = (c.fg, c.bg, c.modifier);
|
||||
if last_style != Some(style) {
|
||||
@@ -610,12 +586,12 @@ impl Debug for Buffer {
|
||||
}
|
||||
f.write_str(" ],\n styles: [\n")?;
|
||||
for s in styles {
|
||||
#[cfg(feature = "crossterm")]
|
||||
#[cfg(feature = "underline-color")]
|
||||
f.write_fmt(format_args!(
|
||||
" x: {}, y: {}, fg: {:?}, bg: {:?}, underline: {:?}, modifier: {:?},\n",
|
||||
s.0, s.1, s.2, s.3, s.4, s.5
|
||||
))?;
|
||||
#[cfg(not(feature = "crossterm"))]
|
||||
#[cfg(not(feature = "underline-color"))]
|
||||
f.write_fmt(format_args!(
|
||||
" x: {}, y: {}, fg: {:?}, bg: {:?}, modifier: {:?},\n",
|
||||
s.0, s.1, s.2, s.3, s.4
|
||||
@@ -649,7 +625,7 @@ mod tests {
|
||||
.bg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
);
|
||||
#[cfg(feature = "crossterm")]
|
||||
#[cfg(feature = "underline-color")]
|
||||
assert_eq!(
|
||||
format!("{buf:?}"),
|
||||
indoc::indoc!(
|
||||
@@ -667,7 +643,7 @@ mod tests {
|
||||
}"
|
||||
)
|
||||
);
|
||||
#[cfg(not(feature = "crossterm"))]
|
||||
#[cfg(not(feature = "underline-color"))]
|
||||
assert_eq!(
|
||||
format!("{buf:?}"),
|
||||
indoc::indoc!(
|
||||
|
||||
294
src/layout.rs
294
src/layout.rs
@@ -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,10 +195,10 @@ 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)])
|
||||
/// .constraints([Constraint::Length(5), Constraint::Min(0)])
|
||||
/// .split(Rect::new(0, 0, 10, 10));
|
||||
/// frame.render_widget(Paragraph::new("foo"), layout[0]);
|
||||
/// frame.render_widget(Paragraph::new("bar"), layout[1]);
|
||||
@@ -388,7 +276,7 @@ impl Layout {
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let layout = Layout::default()
|
||||
/// .constraints(vec![
|
||||
/// .constraints([
|
||||
/// Constraint::Percentage(20),
|
||||
/// Constraint::Ratio(1, 5),
|
||||
/// Constraint::Length(2),
|
||||
@@ -419,7 +307,7 @@ impl Layout {
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let layout = Layout::default()
|
||||
/// .constraints(vec![Constraint::Min(0)])
|
||||
/// .constraints([Constraint::Min(0)])
|
||||
/// .margin(2)
|
||||
/// .split(Rect::new(0, 0, 10, 10));
|
||||
/// assert_eq!(layout[..], [Rect::new(2, 2, 6, 6)]);
|
||||
@@ -439,7 +327,7 @@ impl Layout {
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let layout = Layout::default()
|
||||
/// .constraints(vec![Constraint::Min(0)])
|
||||
/// .constraints([Constraint::Min(0)])
|
||||
/// .horizontal_margin(2)
|
||||
/// .split(Rect::new(0, 0, 10, 10));
|
||||
/// assert_eq!(layout[..], [Rect::new(2, 0, 6, 10)]);
|
||||
@@ -456,7 +344,7 @@ impl Layout {
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let layout = Layout::default()
|
||||
/// .constraints(vec![Constraint::Min(0)])
|
||||
/// .constraints([Constraint::Min(0)])
|
||||
/// .vertical_margin(2)
|
||||
/// .split(Rect::new(0, 0, 10, 10));
|
||||
/// assert_eq!(layout[..], [Rect::new(0, 2, 10, 6)]);
|
||||
@@ -474,13 +362,13 @@ impl Layout {
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let layout = Layout::default()
|
||||
/// .direction(Direction::Horizontal)
|
||||
/// .constraints(vec![Constraint::Length(5), Constraint::Min(0)])
|
||||
/// .constraints([Constraint::Length(5), Constraint::Min(0)])
|
||||
/// .split(Rect::new(0, 0, 10, 10));
|
||||
/// assert_eq!(layout[..], [Rect::new(0, 0, 5, 10), Rect::new(5, 0, 5, 10)]);
|
||||
///
|
||||
/// let layout = Layout::default()
|
||||
/// .direction(Direction::Vertical)
|
||||
/// .constraints(vec![Constraint::Length(5), Constraint::Min(0)])
|
||||
/// .constraints([Constraint::Length(5), Constraint::Min(0)])
|
||||
/// .split(Rect::new(0, 0, 10, 10));
|
||||
/// assert_eq!(layout[..], [Rect::new(0, 0, 10, 5), Rect::new(0, 5, 10, 5)]);
|
||||
/// ```
|
||||
@@ -509,13 +397,13 @@ impl Layout {
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let layout = Layout::default()
|
||||
/// .direction(Direction::Vertical)
|
||||
/// .constraints(vec![Constraint::Length(5), Constraint::Min(0)])
|
||||
/// .constraints([Constraint::Length(5), Constraint::Min(0)])
|
||||
/// .split(Rect::new(2, 2, 10, 10));
|
||||
/// assert_eq!(layout[..], [Rect::new(2, 2, 10, 5), Rect::new(2, 7, 10, 5)]);
|
||||
///
|
||||
/// let layout = Layout::default()
|
||||
/// .direction(Direction::Horizontal)
|
||||
/// .constraints(vec![Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)])
|
||||
/// .constraints([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)])
|
||||
/// .split(Rect::new(0, 0, 9, 2));
|
||||
/// assert_eq!(layout[..], [Rect::new(0, 0, 3, 2), Rect::new(3, 0, 6, 2)]);
|
||||
/// ```
|
||||
@@ -717,14 +605,11 @@ mod tests {
|
||||
|
||||
Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(10),
|
||||
Constraint::Max(5),
|
||||
Constraint::Min(1),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.constraints([
|
||||
Constraint::Percentage(10),
|
||||
Constraint::Max(5),
|
||||
Constraint::Min(1),
|
||||
])
|
||||
.split(target);
|
||||
assert!(!Layout::init_cache(15));
|
||||
LAYOUT_CACHE.with(|c| {
|
||||
@@ -805,79 +690,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 +806,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 +843,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();
|
||||
@@ -1484,7 +1236,7 @@ mod tests {
|
||||
#[test]
|
||||
fn edge_cases() {
|
||||
let layout = Layout::default()
|
||||
.constraints(vec![
|
||||
.constraints([
|
||||
Constraint::Percentage(50),
|
||||
Constraint::Percentage(50),
|
||||
Constraint::Min(0),
|
||||
@@ -1500,7 +1252,7 @@ mod tests {
|
||||
);
|
||||
|
||||
let layout = Layout::default()
|
||||
.constraints(vec![
|
||||
.constraints([
|
||||
Constraint::Max(1),
|
||||
Constraint::Percentage(99),
|
||||
Constraint::Min(0),
|
||||
@@ -1518,7 +1270,7 @@ mod tests {
|
||||
// minimal bug from
|
||||
// https://github.com/ratatui-org/ratatui/pull/404#issuecomment-1681850644
|
||||
let layout = Layout::default()
|
||||
.constraints(vec![Min(1), Length(0), Min(1)])
|
||||
.constraints([Min(1), Length(0), Min(1)])
|
||||
.direction(Direction::Horizontal)
|
||||
.split(Rect::new(0, 0, 1, 1));
|
||||
assert_eq!(
|
||||
@@ -1531,7 +1283,7 @@ mod tests {
|
||||
);
|
||||
|
||||
let layout = Layout::default()
|
||||
.constraints(vec![Length(3), Min(4), Length(1), Min(4)])
|
||||
.constraints([Length(3), Min(4), Length(1), Min(4)])
|
||||
.direction(Direction::Horizontal)
|
||||
.split(Rect::new(0, 0, 7, 1));
|
||||
assert_eq!(
|
||||
|
||||
291
src/layout/rect.rs
Normal file
291
src/layout/rect.rs
Normal 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));
|
||||
}
|
||||
}
|
||||
436
src/lib.rs
436
src/lib.rs
@@ -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).
|
||||
//! 
|
||||
//!
|
||||
//! 
|
||||
//! <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([
|
||||
//! 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([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([
|
||||
//! 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))]
|
||||
|
||||
23
src/style.rs
23
src/style.rs
@@ -140,7 +140,7 @@ impl fmt::Debug for Modifier {
|
||||
/// let styles = [
|
||||
/// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC),
|
||||
/// Style::default().bg(Color::Red).add_modifier(Modifier::UNDERLINED),
|
||||
/// #[cfg(feature = "crossterm")]
|
||||
/// #[cfg(feature = "underline-color")]
|
||||
/// Style::default().underline_color(Color::Green),
|
||||
/// Style::default().fg(Color::Yellow).remove_modifier(Modifier::ITALIC),
|
||||
/// ];
|
||||
@@ -152,7 +152,7 @@ impl fmt::Debug for Modifier {
|
||||
/// Style {
|
||||
/// fg: Some(Color::Yellow),
|
||||
/// bg: Some(Color::Red),
|
||||
/// #[cfg(feature = "crossterm")]
|
||||
/// #[cfg(feature = "underline-color")]
|
||||
/// underline_color: Some(Color::Green),
|
||||
/// add_modifier: Modifier::BOLD | Modifier::UNDERLINED,
|
||||
/// sub_modifier: Modifier::empty(),
|
||||
@@ -179,7 +179,7 @@ impl fmt::Debug for Modifier {
|
||||
/// Style {
|
||||
/// fg: Some(Color::Yellow),
|
||||
/// bg: Some(Color::Reset),
|
||||
/// #[cfg(feature = "crossterm")]
|
||||
/// #[cfg(feature = "underline-color")]
|
||||
/// underline_color: Some(Color::Reset),
|
||||
/// add_modifier: Modifier::empty(),
|
||||
/// sub_modifier: Modifier::empty(),
|
||||
@@ -192,7 +192,7 @@ impl fmt::Debug for Modifier {
|
||||
pub struct Style {
|
||||
pub fg: Option<Color>,
|
||||
pub bg: Option<Color>,
|
||||
#[cfg(feature = "crossterm")]
|
||||
#[cfg(feature = "underline-color")]
|
||||
pub underline_color: Option<Color>,
|
||||
pub add_modifier: Modifier,
|
||||
pub sub_modifier: Modifier,
|
||||
@@ -220,7 +220,7 @@ impl Style {
|
||||
Style {
|
||||
fg: None,
|
||||
bg: None,
|
||||
#[cfg(feature = "crossterm")]
|
||||
#[cfg(feature = "underline-color")]
|
||||
underline_color: None,
|
||||
add_modifier: Modifier::empty(),
|
||||
sub_modifier: Modifier::empty(),
|
||||
@@ -232,7 +232,7 @@ impl Style {
|
||||
Style {
|
||||
fg: Some(Color::Reset),
|
||||
bg: Some(Color::Reset),
|
||||
#[cfg(feature = "crossterm")]
|
||||
#[cfg(feature = "underline-color")]
|
||||
underline_color: Some(Color::Reset),
|
||||
add_modifier: Modifier::empty(),
|
||||
sub_modifier: Modifier::all(),
|
||||
@@ -272,9 +272,12 @@ impl Style {
|
||||
/// Changes the underline color. The text must be underlined with a modifier for this to work.
|
||||
///
|
||||
/// This uses a non-standard ANSI escape sequence. It is supported by most terminal emulators,
|
||||
/// but is only implemented in the crossterm backend.
|
||||
/// but is only implemented in the crossterm backend and enabled by the `underline-color`
|
||||
/// feature flag.
|
||||
///
|
||||
/// See [Wikipedia](https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters) code `58` and `59` for more information.
|
||||
/// See
|
||||
/// [Wikipedia](https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters)
|
||||
/// code `58` and `59` for more information.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
@@ -284,7 +287,7 @@ impl Style {
|
||||
/// let diff = Style::default().underline_color(Color::Red).add_modifier(Modifier::UNDERLINED);
|
||||
/// assert_eq!(style.patch(diff), Style::default().underline_color(Color::Red).add_modifier(Modifier::UNDERLINED));
|
||||
/// ```
|
||||
#[cfg(feature = "crossterm")]
|
||||
#[cfg(feature = "underline-color")]
|
||||
pub const fn underline_color(mut self, color: Color) -> Style {
|
||||
self.underline_color = Some(color);
|
||||
self
|
||||
@@ -347,7 +350,7 @@ impl Style {
|
||||
self.fg = other.fg.or(self.fg);
|
||||
self.bg = other.bg.or(self.bg);
|
||||
|
||||
#[cfg(feature = "crossterm")]
|
||||
#[cfg(feature = "underline-color")]
|
||||
{
|
||||
self.underline_color = other.underline_color.or(self.underline_color);
|
||||
}
|
||||
|
||||
160
src/symbols.rs
160
src/symbols.rs
@@ -54,6 +54,12 @@ pub mod block {
|
||||
};
|
||||
}
|
||||
|
||||
pub mod half_block {
|
||||
pub const UPPER: char = '▀';
|
||||
pub const LOWER: char = '▄';
|
||||
pub const FULL: char = '█';
|
||||
}
|
||||
|
||||
pub mod bar {
|
||||
pub const FULL: &str = "█";
|
||||
pub const SEVEN_EIGHTHS: &str = "▇";
|
||||
@@ -157,7 +163,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 +235,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 {
|
||||
@@ -260,6 +414,10 @@ pub enum Marker {
|
||||
/// Braille Patterns. If your terminal does not support this, you will see unicode replacement
|
||||
/// characters (<28>) instead of Braille dots.
|
||||
Braille,
|
||||
/// Use the unicode block and half block characters ("█", "▄", and "▀") to represent points in
|
||||
/// a grid that is double the resolution of the terminal. Because each terminal cell is
|
||||
/// generally about twice as tall as it is wide, this allows for a square grid of pixels.
|
||||
HalfBlock,
|
||||
}
|
||||
|
||||
pub mod scrollbar {
|
||||
|
||||
@@ -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,46 +548,31 @@ 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
|
||||
/// terminal libraries: [Crossterm], [Termion] and [Termwiz]. See the [`backend`] module for more
|
||||
/// information.
|
||||
///
|
||||
/// [Crossterm]: https://crates.io/crates/crossterm
|
||||
/// [Termion]: https://crates.io/crates/termion
|
||||
/// [Termwiz]: https://crates.io/crates/termwiz
|
||||
/// [`backend`]: crate::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 +597,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 +629,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)
|
||||
@@ -653,6 +641,11 @@ where
|
||||
pub fn set_cursor(&mut self, x: u16, y: u16) {
|
||||
self.cursor_position = Some((x, y));
|
||||
}
|
||||
|
||||
/// Gets the buffer that this `Frame` draws into as a mutable reference.
|
||||
pub fn buffer_mut(&mut self) -> &mut Buffer {
|
||||
self.buffer
|
||||
}
|
||||
}
|
||||
|
||||
/// `CompletedFrame` represents the state of the terminal after all changes performed in the last
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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![
|
||||
|
||||
@@ -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 ",
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@ mod points;
|
||||
mod rectangle;
|
||||
mod world;
|
||||
|
||||
use std::fmt::Debug;
|
||||
use std::{fmt::Debug, iter::zip};
|
||||
|
||||
use itertools::Itertools;
|
||||
|
||||
pub use self::{
|
||||
circle::Circle,
|
||||
@@ -36,36 +38,78 @@ pub struct Label<'a> {
|
||||
line: TextLine<'a>,
|
||||
}
|
||||
|
||||
/// A single layer of the canvas.
|
||||
///
|
||||
/// This allows the canvas to be drawn in multiple layers. This is useful if you want to draw
|
||||
/// multiple shapes on the canvas in specific order.
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
struct Layer {
|
||||
// a string of characters representing the grid. This will be wrapped to the width of the grid
|
||||
// when rendering
|
||||
string: String,
|
||||
colors: Vec<Color>,
|
||||
// colors for foreground and background
|
||||
colors: Vec<(Color, Color)>,
|
||||
}
|
||||
|
||||
/// A grid of cells that can be painted on.
|
||||
///
|
||||
/// The grid represents a particular screen region measured in rows and columns. The underlying
|
||||
/// resolution of the grid might exceed the number of rows and columns. For example, a grid of
|
||||
/// Braille patterns will have a resolution of 2x4 dots per cell. This means that a grid of 10x10
|
||||
/// cells will have a resolution of 20x40 dots.
|
||||
trait Grid: Debug {
|
||||
/// Get the width of the grid in number of terminal columns
|
||||
fn width(&self) -> u16;
|
||||
/// Get the height of the grid in number of terminal rows
|
||||
fn height(&self) -> u16;
|
||||
/// Get the resolution of the grid in number of dots. This doesn't have to be the same as the
|
||||
/// number of rows and columns of the grid. For example, a grid of Braille patterns will have a
|
||||
/// resolution of 2x4 dots per cell. This means that a grid of 10x10 cells will have a
|
||||
/// resolution of 20x40 dots.
|
||||
fn resolution(&self) -> (f64, f64);
|
||||
/// Paint a point of the grid. The point is expressed in number of dots starting at the origin
|
||||
/// of the grid in the top left corner. Note that this is not the same as the (x, y) coordinates
|
||||
/// of the canvas.
|
||||
fn paint(&mut self, x: usize, y: usize, color: Color);
|
||||
/// Save the current state of the grid as a layer to be rendered
|
||||
fn save(&self) -> Layer;
|
||||
/// Reset the grid to its initial state
|
||||
fn reset(&mut self);
|
||||
}
|
||||
|
||||
/// The BrailleGrid is a grid made up of cells each containing a Braille pattern.
|
||||
///
|
||||
/// This makes it possible to draw shapes with a resolution of 2x4 dots per cell. This is useful
|
||||
/// when you want to draw shapes with a high resolution. Font support for Braille patterns is
|
||||
/// required to see the dots. If your terminal or font does not support this unicode block, you
|
||||
/// will see unicode replacement characters (<28>) instead of braille dots.
|
||||
///
|
||||
/// This grid type only supports a single foreground color for each 2x4 dots cell. There is no way
|
||||
/// to set the individual color of each dot in the braille pattern.
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
struct BrailleGrid {
|
||||
/// width of the grid in number of terminal columns
|
||||
width: u16,
|
||||
/// height of the grid in number of terminal rows
|
||||
height: u16,
|
||||
cells: Vec<u16>,
|
||||
/// represents the unicode braille patterns. Will take a value between 0x2800 and 0x28FF
|
||||
/// this is converted to a utf16 string when converting to a layer. See
|
||||
/// <https://en.wikipedia.org/wiki/Braille_Patterns> for more info.
|
||||
utf16_code_points: Vec<u16>,
|
||||
/// The color of each cell only supports foreground colors for now as there's no way to
|
||||
/// individually set the background color of each dot in the braille pattern.
|
||||
colors: Vec<Color>,
|
||||
}
|
||||
|
||||
impl BrailleGrid {
|
||||
/// Create a new BrailleGrid with the given width and height measured in terminal columns and
|
||||
/// rows respectively.
|
||||
fn new(width: u16, height: u16) -> BrailleGrid {
|
||||
let length = usize::from(width * height);
|
||||
BrailleGrid {
|
||||
width,
|
||||
height,
|
||||
cells: vec![symbols::braille::BLANK; length],
|
||||
utf16_code_points: vec![symbols::braille::BLANK; length],
|
||||
colors: vec![Color::Reset; length],
|
||||
}
|
||||
}
|
||||
@@ -81,31 +125,26 @@ impl Grid for BrailleGrid {
|
||||
}
|
||||
|
||||
fn resolution(&self) -> (f64, f64) {
|
||||
(
|
||||
f64::from(self.width) * 2.0 - 1.0,
|
||||
f64::from(self.height) * 4.0 - 1.0,
|
||||
)
|
||||
(f64::from(self.width) * 2.0, f64::from(self.height) * 4.0)
|
||||
}
|
||||
|
||||
fn save(&self) -> Layer {
|
||||
Layer {
|
||||
string: String::from_utf16(&self.cells).unwrap(),
|
||||
colors: self.colors.clone(),
|
||||
}
|
||||
let string = String::from_utf16(&self.utf16_code_points).unwrap();
|
||||
// the background color is always reset for braille patterns
|
||||
let colors = self.colors.iter().map(|c| (*c, Color::Reset)).collect();
|
||||
Layer { string, colors }
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
for c in &mut self.cells {
|
||||
*c = symbols::braille::BLANK;
|
||||
}
|
||||
for c in &mut self.colors {
|
||||
*c = Color::Reset;
|
||||
}
|
||||
self.utf16_code_points.fill(symbols::braille::BLANK);
|
||||
self.colors.fill(Color::Reset);
|
||||
}
|
||||
|
||||
fn paint(&mut self, x: usize, y: usize, color: Color) {
|
||||
let index = y / 4 * self.width as usize + x / 2;
|
||||
if let Some(c) = self.cells.get_mut(index) {
|
||||
// using get_mut here because we are indexing the vector with usize values
|
||||
// and we want to make sure we don't panic if the index is out of bounds
|
||||
if let Some(c) = self.utf16_code_points.get_mut(index) {
|
||||
*c |= symbols::braille::DOTS[y % 4][x % 2];
|
||||
}
|
||||
if let Some(c) = self.colors.get_mut(index) {
|
||||
@@ -114,16 +153,27 @@ impl Grid for BrailleGrid {
|
||||
}
|
||||
}
|
||||
|
||||
/// The CharGrid is a grid made up of cells each containing a single character.
|
||||
///
|
||||
/// This makes it possible to draw shapes with a resolution of 1x1 dots per cell. This is useful
|
||||
/// when you want to draw shapes with a low resolution.
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
struct CharGrid {
|
||||
/// width of the grid in number of terminal columns
|
||||
width: u16,
|
||||
/// height of the grid in number of terminal rows
|
||||
height: u16,
|
||||
/// represents a single character for each cell
|
||||
cells: Vec<char>,
|
||||
/// The color of each cell
|
||||
colors: Vec<Color>,
|
||||
/// The character to use for every cell - e.g. a block, dot, etc.
|
||||
cell_char: char,
|
||||
}
|
||||
|
||||
impl CharGrid {
|
||||
/// Create a new CharGrid with the given width and height measured in terminal columns and
|
||||
/// rows respectively.
|
||||
fn new(width: u16, height: u16, cell_char: char) -> CharGrid {
|
||||
let length = usize::from(width * height);
|
||||
CharGrid {
|
||||
@@ -146,27 +196,25 @@ impl Grid for CharGrid {
|
||||
}
|
||||
|
||||
fn resolution(&self) -> (f64, f64) {
|
||||
(f64::from(self.width) - 1.0, f64::from(self.height) - 1.0)
|
||||
(f64::from(self.width), f64::from(self.height))
|
||||
}
|
||||
|
||||
fn save(&self) -> Layer {
|
||||
Layer {
|
||||
string: self.cells.iter().collect(),
|
||||
colors: self.colors.clone(),
|
||||
colors: self.colors.iter().map(|c| (*c, Color::Reset)).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
for c in &mut self.cells {
|
||||
*c = ' ';
|
||||
}
|
||||
for c in &mut self.colors {
|
||||
*c = Color::Reset;
|
||||
}
|
||||
self.cells.fill(' ');
|
||||
self.colors.fill(Color::Reset);
|
||||
}
|
||||
|
||||
fn paint(&mut self, x: usize, y: usize, color: Color) {
|
||||
let index = y * self.width as usize + x;
|
||||
// using get_mut here because we are indexing the vector with usize values
|
||||
// and we want to make sure we don't panic if the index is out of bounds
|
||||
if let Some(c) = self.cells.get_mut(index) {
|
||||
*c = self.cell_char;
|
||||
}
|
||||
@@ -176,6 +224,128 @@ impl Grid for CharGrid {
|
||||
}
|
||||
}
|
||||
|
||||
/// The HalfBlockGrid is a grid made up of cells each containing a half block character.
|
||||
///
|
||||
/// In terminals, each character is usually twice as tall as it is wide. Unicode has a couple of
|
||||
/// vertical half block characters, the upper half block '▀' and lower half block '▄' which take up
|
||||
/// half the height of a normal character but the full width. Together with an empty space ' ' and a
|
||||
/// full block '█', we can effectively double the resolution of a single cell. In addition, because
|
||||
/// each character can have a foreground and background color, we can control the color of the upper
|
||||
/// and lower half of each cell. This allows us to draw shapes with a resolution of 1x2 "pixels" per
|
||||
/// cell.
|
||||
///
|
||||
/// This allows for more flexibility than the BrailleGrid which only supports a single
|
||||
/// foreground color for each 2x4 dots cell, and the CharGrid which only supports a single
|
||||
/// character for each cell.
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
struct HalfBlockGrid {
|
||||
/// width of the grid in number of terminal columns
|
||||
width: u16,
|
||||
/// height of the grid in number of terminal rows
|
||||
height: u16,
|
||||
/// represents a single color for each "pixel" arranged in column, row order
|
||||
pixels: Vec<Vec<Color>>,
|
||||
}
|
||||
|
||||
impl HalfBlockGrid {
|
||||
/// Create a new HalfBlockGrid with the given width and height measured in terminal columns and
|
||||
/// rows respectively.
|
||||
fn new(width: u16, height: u16) -> HalfBlockGrid {
|
||||
HalfBlockGrid {
|
||||
width,
|
||||
height,
|
||||
pixels: vec![vec![Color::Reset; width as usize]; height as usize * 2],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Grid for HalfBlockGrid {
|
||||
fn width(&self) -> u16 {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> u16 {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn resolution(&self) -> (f64, f64) {
|
||||
(f64::from(self.width), f64::from(self.height) * 2.0)
|
||||
}
|
||||
|
||||
fn save(&self) -> Layer {
|
||||
// Given that we store the pixels in a grid, and that we want to use 2 pixels arranged
|
||||
// vertically to form a single terminal cell, which can be either empty, upper half block,
|
||||
// lower half block or full block, we need examine the pixels in vertical pairs to decide
|
||||
// what character to print in each cell. So these are the 4 states we use to represent each
|
||||
// cell:
|
||||
//
|
||||
// 1. upper: reset, lower: reset => ' ' fg: reset / bg: reset
|
||||
// 2. upper: reset, lower: color => '▄' fg: lower color / bg: reset
|
||||
// 3. upper: color, lower: reset => '▀' fg: upper color / bg: reset
|
||||
// 4. upper: color, lower: color => '▀' fg: upper color / bg: lower color
|
||||
//
|
||||
// Note that because the foreground reset color (i.e. default foreground color) is usually
|
||||
// not the same as the background reset color (i.e. default background color), we need to
|
||||
// swap around the colors for that state (2 reset/color).
|
||||
//
|
||||
// When the upper and lower colors are the same, we could continue to use an upper half
|
||||
// block, but we choose to use a full block instead. This allows us to write unit tests that
|
||||
// treat the cell as a single character instead of two half block characters.
|
||||
|
||||
// first we join each adjacent row together to get an iterator that contains vertical pairs
|
||||
// of pixels, with the lower row being the first element in the pair
|
||||
let vertical_color_pairs = self
|
||||
.pixels
|
||||
.iter()
|
||||
.tuples()
|
||||
.flat_map(|(upper_row, lower_row)| zip(upper_row, lower_row));
|
||||
|
||||
// then we work out what character to print for each pair of pixels
|
||||
let string = vertical_color_pairs
|
||||
.clone()
|
||||
.map(|(upper, lower)| match (upper, lower) {
|
||||
(Color::Reset, Color::Reset) => ' ',
|
||||
(Color::Reset, _) => symbols::half_block::LOWER,
|
||||
(_, Color::Reset) => symbols::half_block::UPPER,
|
||||
(&lower, &upper) => {
|
||||
if lower == upper {
|
||||
symbols::half_block::FULL
|
||||
} else {
|
||||
symbols::half_block::UPPER
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// then we convert these each vertical pair of pixels into a foreground and background color
|
||||
let colors = vertical_color_pairs
|
||||
.map(|(upper, lower)| {
|
||||
let (fg, bg) = match (upper, lower) {
|
||||
(Color::Reset, Color::Reset) => (Color::Reset, Color::Reset),
|
||||
(Color::Reset, &lower) => (lower, Color::Reset),
|
||||
(&upper, Color::Reset) => (upper, Color::Reset),
|
||||
(&upper, &lower) => (upper, lower),
|
||||
};
|
||||
(fg, bg)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Layer { string, colors }
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.pixels.fill(vec![Color::Reset; self.width as usize]);
|
||||
}
|
||||
|
||||
fn paint(&mut self, x: usize, y: usize, color: Color) {
|
||||
self.pixels[y][x] = color;
|
||||
}
|
||||
}
|
||||
|
||||
/// Painter is an abstraction over the [`Context`] that allows to draw shapes on the grid.
|
||||
///
|
||||
/// It is used by the [`Shape`] trait to draw shapes on the grid. It can be useful to think of this
|
||||
/// as similar to the [`Buffer`] struct that is used to draw widgets on the terminal.
|
||||
#[derive(Debug)]
|
||||
pub struct Painter<'a, 'b> {
|
||||
context: &'a mut Context<'b>,
|
||||
@@ -185,6 +355,17 @@ pub struct Painter<'a, 'b> {
|
||||
impl<'a, 'b> Painter<'a, 'b> {
|
||||
/// Convert the (x, y) coordinates to location of a point on the grid
|
||||
///
|
||||
/// (x, y) coordinates are expressed in the coordinate system of the canvas. The origin is in
|
||||
/// the lower left corner of the canvas (unlike most other coordinates in Ratatui where the
|
||||
/// origin is the upper left corner). The x and y bounds of the canvas define the specific area
|
||||
/// of some coordinate system that will be drawn on the canvas. The resolution of the grid is
|
||||
/// used to convert the (x, y) coordinates to the location of a point on the grid.
|
||||
///
|
||||
/// The grid coordinates are expressed in the coordinate system of the grid. The origin is in
|
||||
/// the top left corner of the grid. The x and y bounds of the grid are always [0, width - 1]
|
||||
/// and [0, height - 1] respectively. The resolution of the grid is used to convert the (x, y)
|
||||
/// coordinates to the location of a point on the grid.
|
||||
///
|
||||
/// # Examples:
|
||||
/// ```
|
||||
/// use ratatui::{prelude::*, widgets::canvas::*};
|
||||
@@ -215,8 +396,8 @@ impl<'a, 'b> Painter<'a, 'b> {
|
||||
if width == 0.0 || height == 0.0 {
|
||||
return None;
|
||||
}
|
||||
let x = ((x - left) * self.resolution.0 / width) as usize;
|
||||
let y = ((top - y) * self.resolution.1 / height) as usize;
|
||||
let x = ((x - left) * (self.resolution.0 - 1.0) / width) as usize;
|
||||
let y = ((top - y) * (self.resolution.1 - 1.0) / height) as usize;
|
||||
Some((x, y))
|
||||
}
|
||||
|
||||
@@ -246,6 +427,11 @@ impl<'a, 'b> From<&'a mut Context<'b>> for Painter<'a, 'b> {
|
||||
}
|
||||
|
||||
/// Holds the state of the Canvas when painting to it.
|
||||
///
|
||||
/// This is used by the [`Canvas`] widget to draw shapes on the grid. It can be useful to think of
|
||||
/// this as similar to the [`Frame`] struct that is used to draw widgets on the terminal.
|
||||
///
|
||||
/// [`Frame`]: crate::prelude::Frame
|
||||
#[derive(Debug)]
|
||||
pub struct Context<'a> {
|
||||
x_bounds: [f64; 2],
|
||||
@@ -257,6 +443,22 @@ pub struct Context<'a> {
|
||||
}
|
||||
|
||||
impl<'a> Context<'a> {
|
||||
/// Create a new Context with the given width and height measured in terminal columns and rows
|
||||
/// respectively. The x and y bounds define the specific area of some coordinate system that
|
||||
/// will be drawn on the canvas. The marker defines the type of points used to draw the shapes.
|
||||
///
|
||||
/// Applications should not use this directly but rather use the [`Canvas`] widget. This will be
|
||||
/// created by the [`Canvas::paint`] moethod and passed to the closure that is used to draw on
|
||||
/// the canvas.
|
||||
///
|
||||
/// The x and y bounds should be specified as left/right and bottom/top respectively. For
|
||||
/// example, if you want to draw a map of the world, you might want to use the following bounds:
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::{prelude::*, widgets::canvas::*};
|
||||
///
|
||||
/// let ctx = Context::new(100, 100, [-180.0, 180.0], [-90.0, 90.0], symbols::Marker::Braille);
|
||||
/// ```
|
||||
pub fn new(
|
||||
width: u16,
|
||||
height: u16,
|
||||
@@ -272,6 +474,7 @@ impl<'a> Context<'a> {
|
||||
symbols::Marker::Block => Box::new(CharGrid::new(width, height, block)),
|
||||
symbols::Marker::Bar => Box::new(CharGrid::new(width, height, bar)),
|
||||
symbols::Marker::Braille => Box::new(BrailleGrid::new(width, height)),
|
||||
symbols::Marker::HalfBlock => Box::new(HalfBlockGrid::new(width, height)),
|
||||
};
|
||||
Context {
|
||||
x_bounds,
|
||||
@@ -293,14 +496,16 @@ impl<'a> Context<'a> {
|
||||
shape.draw(&mut painter);
|
||||
}
|
||||
|
||||
/// Go one layer above in the canvas.
|
||||
/// Save the existing state of the grid as a layer to be rendered and reset the grid to its
|
||||
/// initial state for the next layer.
|
||||
pub fn layer(&mut self) {
|
||||
self.layers.push(self.grid.save());
|
||||
self.grid.reset();
|
||||
self.dirty = false;
|
||||
}
|
||||
|
||||
/// Print a string on the canvas at the given position
|
||||
/// Print a string on the canvas at the given position. Note that the text is always printed
|
||||
/// on top of the canvas and is not affected by the layers.
|
||||
pub fn print<T>(&mut self, x: f64, y: f64, line: T)
|
||||
where
|
||||
T: Into<TextLine<'a>>,
|
||||
@@ -330,6 +535,22 @@ impl<'a> Context<'a> {
|
||||
///
|
||||
/// See [Unicode Braille Patterns](https://en.wikipedia.org/wiki/Braille_Patterns) for more info.
|
||||
///
|
||||
/// The HalfBlock marker is useful when you want to draw shapes with a higher resolution than a
|
||||
/// CharGrid but lower than a BrailleGrid. This grid type supports a foreground and background color
|
||||
/// for each terminal cell. This allows for more flexibility than the BrailleGrid which only
|
||||
/// supports a single foreground color for each 2x4 dots cell.
|
||||
///
|
||||
/// The Canvas widget is used by calling the [`Canvas::paint`] method and passing a closure that
|
||||
/// will be used to draw on the canvas. The closure will be passed a [`Context`] object that can be
|
||||
/// used to draw shapes on the canvas.
|
||||
///
|
||||
/// The [`Context`] object provides a [`Context::draw`] method that can be used to draw shapes on
|
||||
/// the canvas. The [`Context::layer`] method can be used to save the current state of the canvas
|
||||
/// and start a new layer. This is useful if you want to draw multiple shapes on the canvas in
|
||||
/// specific order. The [`Context`] object also provides a [`Context::print`] method that can be
|
||||
/// used to print text on the canvas. Note that the text is always printed on top of the canvas and
|
||||
/// is not affected by the layers.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
@@ -371,7 +592,7 @@ where
|
||||
block: Option<Block<'a>>,
|
||||
x_bounds: [f64; 2],
|
||||
y_bounds: [f64; 2],
|
||||
painter: Option<F>,
|
||||
paint_func: Option<F>,
|
||||
background_color: Color,
|
||||
marker: symbols::Marker,
|
||||
}
|
||||
@@ -385,7 +606,7 @@ where
|
||||
block: None,
|
||||
x_bounds: [0.0, 0.0],
|
||||
y_bounds: [0.0, 0.0],
|
||||
painter: None,
|
||||
paint_func: None,
|
||||
background_color: Color::Reset,
|
||||
marker: symbols::Marker::Braille,
|
||||
}
|
||||
@@ -396,6 +617,7 @@ impl<'a, F> Canvas<'a, F>
|
||||
where
|
||||
F: Fn(&mut Context),
|
||||
{
|
||||
/// Set the block that will be rendered around the canvas
|
||||
pub fn block(mut self, block: Block<'a>) -> Canvas<'a, F> {
|
||||
self.block = Some(block);
|
||||
self
|
||||
@@ -420,10 +642,11 @@ where
|
||||
|
||||
/// Store the closure that will be used to draw to the Canvas
|
||||
pub fn paint(mut self, f: F) -> Canvas<'a, F> {
|
||||
self.painter = Some(f);
|
||||
self.paint_func = Some(f);
|
||||
self
|
||||
}
|
||||
|
||||
/// Change the background color of the canvas
|
||||
pub fn background_color(mut self, color: Color) -> Canvas<'a, F> {
|
||||
self.background_color = color;
|
||||
self
|
||||
@@ -433,12 +656,18 @@ where
|
||||
/// as they provide a more fine grained result but you might want to use the simple dot or
|
||||
/// block instead if the targeted terminal does not support those symbols.
|
||||
///
|
||||
/// The HalfBlock marker is useful when you want to draw shapes with a higher resolution than a
|
||||
/// CharGrid but lower than a BrailleGrid. This grid type supports a foreground and background
|
||||
/// color for each terminal cell. This allows for more flexibility than the BrailleGrid which
|
||||
/// only supports a single foreground color for each 2x4 dots cell.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::{prelude::*, widgets::canvas::*};
|
||||
///
|
||||
/// Canvas::default().marker(symbols::Marker::Braille).paint(|ctx| {});
|
||||
/// Canvas::default().marker(symbols::Marker::HalfBlock).paint(|ctx| {});
|
||||
/// Canvas::default().marker(symbols::Marker::Dot).paint(|ctx| {});
|
||||
/// Canvas::default().marker(symbols::Marker::Block).paint(|ctx| {});
|
||||
/// ```
|
||||
@@ -466,7 +695,7 @@ where
|
||||
|
||||
let width = canvas_area.width as usize;
|
||||
|
||||
let Some(ref painter) = self.painter else {
|
||||
let Some(ref painter) = self.paint_func else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -484,17 +713,19 @@ where
|
||||
|
||||
// Retrieve painted points for each layer
|
||||
for layer in ctx.layers {
|
||||
for (i, (ch, color)) in layer
|
||||
.string
|
||||
.chars()
|
||||
.zip(layer.colors.into_iter())
|
||||
.enumerate()
|
||||
{
|
||||
for (index, (ch, colors)) in layer.string.chars().zip(layer.colors).enumerate() {
|
||||
if ch != ' ' && ch != '\u{2800}' {
|
||||
let (x, y) = (i % width, i / width);
|
||||
buf.get_mut(x as u16 + canvas_area.left(), y as u16 + canvas_area.top())
|
||||
.set_char(ch)
|
||||
.set_fg(color);
|
||||
let (x, y) = (
|
||||
(index % width) as u16 + canvas_area.left(),
|
||||
(index / width) as u16 + canvas_area.top(),
|
||||
);
|
||||
let cell = buf.get_mut(x, y).set_char(ch);
|
||||
if colors.0 != Color::Reset {
|
||||
cell.set_fg(colors.0);
|
||||
}
|
||||
if colors.1 != Color::Reset {
|
||||
cell.set_bg(colors.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +94,41 @@ mod tests {
|
||||
assert_buffer_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn draw_half_block_lines() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 10));
|
||||
let canvas = Canvas::default()
|
||||
.marker(Marker::HalfBlock)
|
||||
.x_bounds([0.0, 10.0])
|
||||
.y_bounds([0.0, 10.0])
|
||||
.paint(|context| {
|
||||
context.draw(&Rectangle {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
width: 10.0,
|
||||
height: 10.0,
|
||||
color: Color::Red,
|
||||
});
|
||||
});
|
||||
canvas.render(buffer.area, &mut buffer);
|
||||
let mut expected = Buffer::with_lines(vec![
|
||||
"█▀▀▀▀▀▀▀▀█",
|
||||
"█ █",
|
||||
"█ █",
|
||||
"█ █",
|
||||
"█ █",
|
||||
"█ █",
|
||||
"█ █",
|
||||
"█ █",
|
||||
"█ █",
|
||||
"█▄▄▄▄▄▄▄▄█",
|
||||
]);
|
||||
expected.set_style(buffer.area, Style::new().red().on_red());
|
||||
expected.set_style(buffer.area.inner(&Margin::new(1, 0)), Style::reset().red());
|
||||
expected.set_style(buffer.area.inner(&Margin::new(1, 1)), Style::reset());
|
||||
assert_buffer_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn draw_braille_lines() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 10));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -128,14 +185,14 @@ impl<'a> Widget for Gauge<'a> {
|
||||
// render the filled area (left to end)
|
||||
for x in gauge_area.left()..end {
|
||||
let cell = buf.get_mut(x, y);
|
||||
if self.use_unicode {
|
||||
// Use full block for the filled part of the gauge and spaces for the part that is
|
||||
// covered by the label. Note that the background and foreground colors are swapped
|
||||
// for the label part, otherwise the gauge will be inverted
|
||||
if x < label_col || x > label_col + clamped_label_width || y != label_row {
|
||||
cell.set_symbol(symbols::block::FULL)
|
||||
.set_fg(self.gauge_style.fg.unwrap_or(Color::Reset))
|
||||
.set_bg(self.gauge_style.bg.unwrap_or(Color::Reset));
|
||||
} else {
|
||||
// spaces are needed to apply the background styling.
|
||||
// note that the background and foreground colors are swapped
|
||||
// otherwise the gauge will be inverted
|
||||
cell.set_symbol(" ")
|
||||
.set_fg(self.gauge_style.bg.unwrap_or(Color::Reset))
|
||||
.set_bg(self.gauge_style.fg.unwrap_or(Color::Reset));
|
||||
@@ -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
|
||||
@@ -266,7 +366,7 @@ impl<'a> Widget for LineGauge<'a> {
|
||||
.set_style(Style {
|
||||
fg: self.gauge_style.fg,
|
||||
bg: None,
|
||||
#[cfg(feature = "crossterm")]
|
||||
#[cfg(feature = "underline-color")]
|
||||
underline_color: self.gauge_style.underline_color,
|
||||
add_modifier: self.gauge_style.add_modifier,
|
||||
sub_modifier: self.gauge_style.sub_modifier,
|
||||
@@ -278,7 +378,7 @@ impl<'a> Widget for LineGauge<'a> {
|
||||
.set_style(Style {
|
||||
fg: self.gauge_style.bg,
|
||||
bg: None,
|
||||
#[cfg(feature = "crossterm")]
|
||||
#[cfg(feature = "underline-color")]
|
||||
underline_color: self.gauge_style.underline_color,
|
||||
add_modifier: self.gauge_style.add_modifier,
|
||||
sub_modifier: self.gauge_style.sub_modifier,
|
||||
|
||||
@@ -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
|
||||
///
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>>,
|
||||
|
||||
@@ -2,7 +2,7 @@ use ratatui::{
|
||||
backend::TestBackend,
|
||||
buffer::Buffer,
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
style::{Color, Modifier, Style, Stylize},
|
||||
symbols,
|
||||
text::Span,
|
||||
widgets::{Block, Borders, Gauge, LineGauge},
|
||||
@@ -18,7 +18,7 @@ fn widgets_gauge_renders() {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(2)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
.split(f.size());
|
||||
|
||||
let gauge = Gauge::default()
|
||||
@@ -47,32 +47,12 @@ fn widgets_gauge_renders() {
|
||||
" ",
|
||||
" ",
|
||||
]);
|
||||
expected.set_style(Rect::new(3, 3, 34, 1), Style::new().red().on_blue());
|
||||
|
||||
for i in 3..17 {
|
||||
expected
|
||||
.get_mut(i, 3)
|
||||
.set_bg(Color::Blue)
|
||||
.set_fg(Color::Red);
|
||||
}
|
||||
for i in 17..37 {
|
||||
expected
|
||||
.get_mut(i, 3)
|
||||
.set_bg(Color::Blue)
|
||||
.set_fg(Color::Red);
|
||||
}
|
||||
|
||||
for i in 3..20 {
|
||||
expected
|
||||
.get_mut(i, 6)
|
||||
.set_bg(Color::Blue)
|
||||
.set_fg(Color::Red);
|
||||
}
|
||||
for i in 20..37 {
|
||||
expected
|
||||
.get_mut(i, 6)
|
||||
.set_bg(Color::Blue)
|
||||
.set_fg(Color::Red);
|
||||
}
|
||||
expected.set_style(Rect::new(3, 6, 15, 1), Style::new().red().on_blue());
|
||||
// Note that filled part of the gauge only covers the 5 and the 1, not the % symbol
|
||||
expected.set_style(Rect::new(18, 6, 2, 1), Style::new().blue().on_red());
|
||||
expected.set_style(Rect::new(20, 6, 17, 1), Style::new().red().on_blue());
|
||||
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
}
|
||||
@@ -87,7 +67,7 @@ fn widgets_gauge_renders_no_unicode() {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(2)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
.split(f.size());
|
||||
|
||||
let gauge = Gauge::default()
|
||||
@@ -106,10 +86,10 @@ fn widgets_gauge_renders_no_unicode() {
|
||||
" ",
|
||||
" ",
|
||||
" ┌Percentage────────────────────────┐ ",
|
||||
" │ 43% │ ",
|
||||
" │███████████████43% │ ",
|
||||
" └──────────────────────────────────┘ ",
|
||||
" ┌Ratio─────────────────────────────┐ ",
|
||||
" │ 21% │ ",
|
||||
" │███████ 21% │ ",
|
||||
" └──────────────────────────────────┘ ",
|
||||
" ",
|
||||
" ",
|
||||
@@ -143,9 +123,9 @@ fn widgets_gauge_applies_styles() {
|
||||
.unwrap();
|
||||
let mut expected = Buffer::with_lines(vec![
|
||||
"┌Test──────┐",
|
||||
"│ │",
|
||||
"│ 43% │",
|
||||
"│ │",
|
||||
"│████ │",
|
||||
"│███43% │",
|
||||
"│████ │",
|
||||
"└──────────┘",
|
||||
]);
|
||||
// title
|
||||
@@ -156,13 +136,10 @@ fn widgets_gauge_applies_styles() {
|
||||
Style::default().fg(Color::Blue).bg(Color::Red),
|
||||
);
|
||||
// filled area
|
||||
for y in 1..4 {
|
||||
expected.set_style(
|
||||
Rect::new(1, y, 4, 1),
|
||||
// filled style is invert of gauge_style
|
||||
Style::default().fg(Color::Red).bg(Color::Blue),
|
||||
);
|
||||
}
|
||||
expected.set_style(
|
||||
Rect::new(1, 1, 4, 3),
|
||||
Style::default().fg(Color::Blue).bg(Color::Red),
|
||||
);
|
||||
// label (foreground and modifier from label style)
|
||||
expected.set_style(
|
||||
Rect::new(4, 2, 1, 1),
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#![allow(deprecated)]
|
||||
|
||||
use ratatui::{
|
||||
backend::TestBackend,
|
||||
buffer::Buffer,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#![allow(deprecated)]
|
||||
|
||||
use ratatui::{
|
||||
backend::TestBackend,
|
||||
buffer::Buffer,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#![allow(deprecated)]
|
||||
|
||||
use ratatui::{
|
||||
backend::TestBackend, buffer::Buffer, layout::Rect, symbols, text::Line, widgets::Tabs,
|
||||
Terminal,
|
||||
|
||||
Reference in New Issue
Block a user