Compare commits
23 Commits
v0.23.1-al
...
v0.23.1-al
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a57e76ed1 | ||
|
|
6c7bef8d11 | ||
|
|
88ae3485c2 | ||
|
|
e5caf170c8 | ||
|
|
089f8ba66a | ||
|
|
fbf1a451c8 | ||
|
|
4541336514 | ||
|
|
346e7b4f4d | ||
|
|
15641c8475 | ||
|
|
2fd85af33c | ||
|
|
401a7a7f71 | ||
|
|
e35e4135c9 | ||
|
|
8ae4403b63 | ||
|
|
11076d0af3 | ||
|
|
9cfb133a98 | ||
|
|
4548a9b7e2 | ||
|
|
61af0d9906 | ||
|
|
c0991cc576 | ||
|
|
301366c4fa | ||
|
|
3bda372847 | ||
|
|
082cbcbc50 | ||
|
|
cbf86da0e7 | ||
|
|
32e461953c |
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
|
||||
|
||||
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"),
|
||||
];
|
||||
```
|
||||
@@ -155,6 +155,11 @@ 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"]
|
||||
|
||||
507
README.md
507
README.md
@@ -1,142 +1,347 @@
|
||||

|
||||
<!--
|
||||
Permalink to https://github.com/ratatui-org/ratatui/blob/images/examples/demo2.gif
|
||||
See RELEASE.md for instructions on creating the demo gif
|
||||
--->
|
||||
<details>
|
||||
<summary>Table of Contents</summary>
|
||||
|
||||
- [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)
|
||||
- [Acknowledgments](#acknowledgments)
|
||||
- [License](#license)
|
||||
|
||||
</details>
|
||||
|
||||
<!-- cargo-rdme start -->
|
||||
|
||||

|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://crates.io/crates/ratatui)
|
||||
[](./LICENSE) [](https://github.com/ratatui-org/ratatui/actions?query=workflow%3ACI+)
|
||||
[](https://docs.rs/crate/ratatui/)<br>
|
||||
[](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)<br>
|
||||
[Documentation](https://docs.rs/ratatui)
|
||||
· [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)
|
||||
[![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>
|
||||
|
||||
<img align="left" src="https://avatars.githubusercontent.com/u/125200832?s=128&v=4">
|
||||
|
||||
# Ratatui
|
||||
|
||||
`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.
|
||||
|
||||
<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
|
||||
widgets](#third-party-libraries-bootstrapping-templates-and-widgets)
|
||||
* [Apps](#apps)
|
||||
* [Alternatives](#alternatives)
|
||||
* [Contributors](#contributors)
|
||||
* [Acknowledgments](#acknowledgments)
|
||||
* [License](#license)
|
||||
|
||||
</details>
|
||||
[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
|
||||
@@ -159,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
|
||||
@@ -195,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
|
||||
@@ -220,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
|
||||
@@ -257,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
|
||||
|
||||
18
RELEASE.md
18
RELEASE.md
@@ -20,8 +20,26 @@ actions](.github/workflows/cd.yml) and triggered by pushing a tag.
|
||||
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.
|
||||
|
||||
@@ -58,7 +58,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]
|
||||
|
||||
@@ -136,10 +136,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 +152,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 +198,7 @@ fn create_groups<'a>(app: &'a App, combine_values_and_labels: bool) -> Vec<BarGr
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn draw_bar_with_group_labels<B>(f: &mut Frame<B>, app: &App, area: Rect)
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
fn draw_bar_with_group_labels(f: &mut Frame, app: &App, area: Rect) {
|
||||
let groups = create_groups(app, false);
|
||||
|
||||
let mut barchart = BarChart::default()
|
||||
@@ -228,10 +225,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_horizontal_bars<B>(f: &mut Frame<B>, app: &App, area: Rect)
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
fn draw_horizontal_bars(f: &mut Frame, app: &App, area: Rect) {
|
||||
let groups = create_groups(app, true);
|
||||
|
||||
let mut barchart = BarChart::default()
|
||||
@@ -260,10 +254,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_legend<B>(f: &mut Frame<B>, area: Rect)
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
fn draw_legend(f: &mut Frame, area: Rect) {
|
||||
let text = vec![
|
||||
Line::from(Span::styled(
|
||||
TOTAL_REVENUE,
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
@@ -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,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(())
|
||||
}
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
# To run this script, install vhs and run `vhs ./examples/canvas.tape`
|
||||
Output "target/canvas.gif"
|
||||
Set Theme "OceanicMaterial"
|
||||
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
|
||||
|
||||
@@ -142,18 +142,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(
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
@@ -89,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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,20 @@
|
||||
# To run this script, install vhs and run `vhs ./examples/custom_widget.tape`
|
||||
Output "target/custom_widget.gif"
|
||||
Set Theme "OceanicMaterial"
|
||||
Set Width 1200
|
||||
Set Height 200
|
||||
Set Width 760
|
||||
Set Height 260
|
||||
Hide
|
||||
Type "cargo run --example=custom_widget --features=crossterm"
|
||||
Enter
|
||||
Sleep 1s
|
||||
Sleep 2s
|
||||
Show
|
||||
Sleep 5s
|
||||
Sleep 1s
|
||||
Set TypingSpeed 0.7s
|
||||
Right
|
||||
Right
|
||||
Space
|
||||
Space
|
||||
Left
|
||||
Space
|
||||
Left
|
||||
Space
|
||||
@@ -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)])
|
||||
|
||||
@@ -30,7 +30,7 @@ impl Widget for TracerouteTab {
|
||||
Block::new().style(THEME.content).render(area, buf);
|
||||
let area = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(vec![Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)])
|
||||
.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);
|
||||
@@ -111,11 +111,11 @@ fn render_map(selected_row: usize, area: Rect, buf: &mut Buffer) {
|
||||
.padding(Padding::new(1, 0, 1, 0))
|
||||
.style(theme.style),
|
||||
)
|
||||
.marker(Marker::Dot)
|
||||
.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([113.0, 154.0])
|
||||
.y_bounds([-42.0, -11.0])
|
||||
.x_bounds([112.0, 155.0])
|
||||
.y_bounds([-46.0, -11.0])
|
||||
.paint(|context| {
|
||||
context.draw(&map);
|
||||
if let Some(path) = path {
|
||||
|
||||
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 "OceanicMaterial"
|
||||
# The reason for this strange size is that the social preview image for this
|
||||
# demo is 1280x64 with 80 pixels of padding on each side. We want a version
|
||||
# without the padding for README.md, etc.
|
||||
Set Width 640
|
||||
Set Height 160
|
||||
Set Padding 0
|
||||
Hide
|
||||
Type "cargo run --example docsrs --features crossterm"
|
||||
Enter
|
||||
Sleep 2s
|
||||
Show
|
||||
Sleep 1s
|
||||
Screenshot "target/docsrs-hello.png"
|
||||
Sleep 1s
|
||||
Hide
|
||||
Type "q"
|
||||
Type "cargo run --example docsrs --features crossterm -- layout"
|
||||
Enter
|
||||
Sleep 2s
|
||||
Show
|
||||
Sleep 1s
|
||||
Screenshot "target/docsrs-layout.png"
|
||||
Sleep 1s
|
||||
Hide
|
||||
Type "q"
|
||||
Type "cargo run --example docsrs --features crossterm -- styling"
|
||||
Enter
|
||||
Sleep 2s
|
||||
Show
|
||||
Sleep 1s
|
||||
Screenshot "target/docsrs-styling.png"
|
||||
Sleep 1s
|
||||
Hide
|
||||
Type "q"
|
||||
@@ -103,18 +103,15 @@ 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::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(f.size());
|
||||
|
||||
let gauge = Gauge::default()
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -216,14 +216,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 +237,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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -198,11 +198,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.
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
@@ -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**")
|
||||
|
||||
@@ -81,7 +81,7 @@ fn run_app<B: Backend>(
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
fn ui(f: &mut Frame, app: &App) {
|
||||
let size = f.size();
|
||||
|
||||
// Words made "loooong" to demonstrate line breaking.
|
||||
@@ -94,15 +94,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![
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ fn run_app<B: Backend>(
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
|
||||
fn ui(f: &mut Frame, app: &mut App) {
|
||||
let size = f.size();
|
||||
|
||||
// Words made "loooong" to demonstrate line breaking.
|
||||
@@ -107,16 +107,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![
|
||||
|
||||
@@ -126,17 +126,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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
297
src/layout.rs
297
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,116 +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.saturating_mul(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 {
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -310,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]);
|
||||
@@ -391,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),
|
||||
@@ -422,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)]);
|
||||
@@ -442,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)]);
|
||||
@@ -459,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)]);
|
||||
@@ -477,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)]);
|
||||
/// ```
|
||||
@@ -512,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)]);
|
||||
/// ```
|
||||
@@ -720,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| {
|
||||
@@ -808,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!(
|
||||
@@ -997,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);
|
||||
@@ -1078,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();
|
||||
@@ -1487,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),
|
||||
@@ -1503,7 +1252,7 @@ mod tests {
|
||||
);
|
||||
|
||||
let layout = Layout::default()
|
||||
.constraints(vec![
|
||||
.constraints([
|
||||
Constraint::Max(1),
|
||||
Constraint::Percentage(99),
|
||||
Constraint::Min(0),
|
||||
@@ -1521,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!(
|
||||
@@ -1534,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));
|
||||
}
|
||||
}
|
||||
437
src/lib.rs
437
src/lib.rs
@@ -1,180 +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).
|
||||
//!
|
||||
//! 
|
||||
// this is a permalink to https://github.com/ratatui-org/ratatui/blob/images/examples/demo2.gif
|
||||
//!
|
||||
//! # Get started
|
||||
//! <div align="center">
|
||||
//!
|
||||
//! ## Adding `ratatui` as a dependency
|
||||
//! [![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)
|
||||
//!
|
||||
//! Add the following to your `Cargo.toml`:
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! crossterm = "0.27"
|
||||
//! ratatui = "0.23"
|
||||
//! </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 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))]
|
||||
|
||||
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
|
||||
|
||||
@@ -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,7 +377,7 @@ impl<'a> BarChart<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_horizontal_bars(self, buf: &mut Buffer, bars_area: Rect, max: u64) {
|
||||
fn render_horizontal(self, buf: &mut Buffer, area: Rect) {
|
||||
// get the longest label
|
||||
let label_size = self
|
||||
.data
|
||||
@@ -345,35 +388,25 @@ impl<'a> BarChart<'a> {
|
||||
.max()
|
||||
.unwrap_or(0) as u16;
|
||||
|
||||
let label_x = bars_area.x;
|
||||
let label_x = area.x;
|
||||
let bars_area = {
|
||||
let margin = if label_size == 0 { 0 } else { 1 };
|
||||
Rect {
|
||||
x: bars_area.x + label_size + margin,
|
||||
width: bars_area.width - label_size - margin,
|
||||
..bars_area
|
||||
x: area.x + label_size + margin,
|
||||
width: area.width - label_size - margin,
|
||||
..area
|
||||
}
|
||||
};
|
||||
|
||||
// convert the bar values to ratatui::symbols::bar::Set
|
||||
let groups: Vec<Vec<u16>> = self
|
||||
.data
|
||||
.iter()
|
||||
.map(|group| {
|
||||
group
|
||||
.bars
|
||||
.iter()
|
||||
.map(|bar| (bar.value * u64::from(bars_area.width) / max) as u16)
|
||||
.collect()
|
||||
})
|
||||
.collect();
|
||||
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 {
|
||||
@@ -425,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,
|
||||
@@ -459,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -489,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;
|
||||
}
|
||||
@@ -532,39 +568,14 @@ impl<'a> Widget for BarChart<'a> {
|
||||
buf.set_style(area, self.style);
|
||||
|
||||
self.render_block(&mut area, buf);
|
||||
if area.area() == 0 {
|
||||
return;
|
||||
}
|
||||
if self.data.is_empty() {
|
||||
return;
|
||||
}
|
||||
if self.bar_width == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let label_height = self.label_height();
|
||||
if area.height <= label_height {
|
||||
if area.is_empty() || self.data.is_empty() || self.bar_width == 0 {
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -607,7 +618,7 @@ mod tests {
|
||||
buffer,
|
||||
Buffer::with_lines(vec![
|
||||
" █ ",
|
||||
"█ █ ",
|
||||
"1 2 ",
|
||||
"f b ",
|
||||
])
|
||||
);
|
||||
@@ -629,7 +640,7 @@ mod tests {
|
||||
Buffer::with_lines(vec![
|
||||
"╔Block════════╗",
|
||||
"║ █ ║",
|
||||
"║█ █ ║",
|
||||
"║1 2 ║",
|
||||
"║f b ║",
|
||||
"╚═════════════╝",
|
||||
])
|
||||
@@ -657,7 +668,7 @@ mod tests {
|
||||
buffer,
|
||||
Buffer::with_lines(vec![
|
||||
" █ █ ",
|
||||
"█ █ █ ",
|
||||
"1 2 █ ",
|
||||
"f b b ",
|
||||
])
|
||||
);
|
||||
@@ -672,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]) {
|
||||
@@ -709,7 +720,7 @@ mod tests {
|
||||
buffer,
|
||||
Buffer::with_lines(vec![
|
||||
" █ ",
|
||||
"█ █ ",
|
||||
"1 2 ",
|
||||
"f b ",
|
||||
])
|
||||
);
|
||||
@@ -726,7 +737,7 @@ mod tests {
|
||||
buffer,
|
||||
Buffer::with_lines(vec![
|
||||
" █ ",
|
||||
" ▄ █ ",
|
||||
" ▄ 3 ",
|
||||
"f b b ",
|
||||
])
|
||||
);
|
||||
@@ -753,7 +764,7 @@ mod tests {
|
||||
buffer,
|
||||
Buffer::with_lines(vec![
|
||||
" ",
|
||||
" ▁ ▂ ▃ ▄ ▅ ▆ ▇ █ ",
|
||||
" ▁ ▂ ▃ ▄ ▅ ▆ ▇ 8 ",
|
||||
"a b c d e f g h i ",
|
||||
])
|
||||
);
|
||||
@@ -786,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);
|
||||
@@ -803,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) {
|
||||
@@ -812,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!(
|
||||
@@ -995,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);
|
||||
}
|
||||
|
||||
@@ -1168,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]
|
||||
@@ -1192,7 +1055,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![" █", "▆ 5", " G"]);
|
||||
assert_buffer_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
@@ -1238,4 +1101,234 @@ mod tests {
|
||||
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 ",
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,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
|
||||
@@ -157,7 +156,10 @@ impl<'a> Bar<'a> {
|
||||
};
|
||||
|
||||
let width = value_label.width() as u16;
|
||||
if width < max_width {
|
||||
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,
|
||||
@@ -166,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);
|
||||
@@ -176,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
|
||||
|
||||
@@ -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
|
||||
///
|
||||
|
||||
@@ -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>>,
|
||||
|
||||
@@ -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()
|
||||
@@ -87,7 +87,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()
|
||||
|
||||
Reference in New Issue
Block a user