Compare commits

..

3 Commits

Author SHA1 Message Date
Josh McKinney
84243c7ce8 feat: add layout helper methods 2024-01-03 20:30:30 -08:00
Josh McKinney
6a50f2085e feat: add layout helper methods 2024-01-03 20:27:36 -08:00
Josh McKinney
beaa2bf58d feat(layout): add split_array method
Layout::split_array() makes it easy to store areas in individual values.
E.g.:

```rust
let [top, main] =
    Layout::vertical([Constraint::Length(1), Constraint::Min(0)])
    .split_array(area);
```
2024-01-03 20:27:36 -08:00
95 changed files with 4073 additions and 10467 deletions

View File

@@ -11,11 +11,11 @@ set -o pipefail
# Turn on traces, useful while debugging but commented out by default
# set -o xtrace
last_release="$(git tag --sort=committerdate | grep -P "v0+\.\d+\.\d+$" | tail -1)"
last_release="$(git tag --sort=committerdate | grep -E "v0\.\d+\.\d+$" | tail -1)"
echo "🐭 Last release: ${last_release}"
# detect breaking changes
if [ -n "$(git log --oneline ${last_release}..HEAD | grep '!:')" ]; then
if git log --oneline ${last_release}..HEAD | grep -q '!:' || true; then
echo "🐭 Breaking changes detected since ${last_release}"
git log --oneline ${last_release}..HEAD | grep '!:'
# increment the minor version
@@ -33,7 +33,7 @@ echo "🐭 Next release: ${next_release}"
suffix="alpha"
last_tag="$(git tag --sort=committerdate | tail -1)"
if [[ "${last_tag}" = "${next_release}-${suffix}"* ]]; then
if [[ "${last_tag}" = "${next-release}-${suffix}"* ]]; then
echo "🐭 Last alpha release: ${last_tag}"
# increment the alpha version
# e.g. v0.22.1-alpha.12 -> v0.22.1-alpha.13

View File

@@ -44,24 +44,6 @@ jobs:
header: pr-title-lint-error
delete: true
check-signed:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
# Check commit signature and add comment if needed
- name: Check signed commits in PR
uses: 1Password/check-signed-commits-action@v1
with:
comment: |
Thank you for opening this pull request!
We require commits to be signed and it looks like this PR contains unsigned commits.
Get help in the [CONTRIBUTING.md](https://github.com/ratatui-org/ratatui/blob/main/CONTRIBUTING.md#sign-your-commits)
or on [Github doc](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits).
check-breaking-change-label:
runs-on: ubuntu-latest
env:

View File

@@ -151,7 +151,7 @@ jobs:
toolchain: ${{ matrix.toolchain }}
- name: Install cargo-make
uses: taiki-e/install-action@cargo-make
- name: Install cargo-nextest
- name: Install cargo-make
uses: taiki-e/install-action@nextest
- name: Test ${{ matrix.backend }}
run: cargo make test-backend ${{ matrix.backend }}

View File

@@ -11,12 +11,8 @@ github with a [breaking change] label.
This is a quick summary of the sections below:
- [v0.26.0 (unreleased)](#v0260-unreleased)
- `patch_style` & `reset_style` now consume and return `Self`
- Removed deprecated `Block::title_on_bottom`
- `Line` now has an extra `style` field which applies the style to the entire line
- `Block` style methods cannot be created in a const context
- `Tabs::new()` now accepts `IntoIterator<Item: Into<Line<'a>>>`
- `Table::new` now accepts `IntoIterator<Item: Into<Row<'a>>>`.
- [v0.25.0](#v0250)
- Removed `Axis::title_style` and `Buffer::set_background`
- `List::new()` now accepts `IntoIterator<Item = Into<ListItem<'a>>>`
@@ -48,77 +44,6 @@ This is a quick summary of the sections below:
## v0.26.0 (unreleased)
### `Table::new()` now accepts `IntoIterator<Item: Into<Row<'a>>>` ([#774])
[#774]: https://github.com/ratatui-org/ratatui/pull/774
Previously, `Table::new()` accepted `IntoIterator<Item=Row<'a>>`. The argument change to
`IntoIterator<Item: Into<Row<'a>>>`, This allows more flexible types from calling scopes, though it
can some break type inference in the calling scope for empty containers.
This can be resolved either by providing an explicit type (e.g. `Vec::<Row>::new()`), or by using
`Table::default()`.
```diff
- let table = Table::new(vec![], widths);
// becomes
+ let table = Table::default().widths(widths);
```
### `Tabs::new()` now accepts `IntoIterator<Item: Into<Line<'a>>>` ([#776])
[#776]: https://github.com/ratatui-org/ratatui/pull/776
Previously, `Tabs::new()` accepted `Vec<T>` where `T: Into<Line<'a>>`. This allows more flexible
types from calling scopes, though it can break some type inference in the calling scope.
This typically occurs when collecting an iterator prior to calling `Tabs::new`, and can be resolved
by removing the call to `.collect()`.
```diff
- let tabs = Tabs::new((0.3).map(|i| format!("{i}")).collect());
// becomes
+ let tabs = Tabs::new((0.3).map(|i| format!("{i}")));
```
### Table::default() now sets segment_size to None and column_spacing to ([#751])
[#751]: https://github.com/ratatui-org/ratatui/pull/751
The default() implementation of Table now sets the column_spacing field to 1 and the segment_size
field to SegmentSize::None. This will affect the rendering of a small amount of apps.
To use the previous default values, call `table.segment_size(Default::default())` and
`table.column_spacing(0)`.
### `patch_style` & `reset_style` now consumes and returns `Self` ([#754])
[#754]: https://github.com/ratatui-org/ratatui/pull/754
Previously, `patch_style` and `reset_style` in `Text`, `Line` and `Span` were using a mutable
reference to `Self`. To be more consistent with the rest of `ratatui`, which is using fluent
setters, these now take ownership of `Self` and return it.
The following example shows how to migrate for `Line`, but the same applies for `Text` and `Span`.
```diff
- let mut line = Line::from("foobar");
- line.patch_style(style);
// becomes
+ let line = Line::new("foobar").patch_style(style);
```
### Remove deprecated `Block::title_on_bottom` ([#757])
[#757]: https://github.com/ratatui-org/ratatui/pull/757
`Block::title_on_bottom` was deprecated in v0.22. Use `Block::title` and `Title::position` instead.
```diff
- block.title("foobar").title_on_bottom();
+ block.title(Title::from("foobar").position(Position::Bottom));
```
### `Block` style methods cannot be used in a const context ([#720])
[#720]: https://github.com/ratatui-org/ratatui/pull/720
@@ -144,7 +69,7 @@ the `Span::style` field.
let line = Line {
spans: vec!["".into()],
alignment: Alignment::Left,
+ ..Default::default()
+ ..Default::default()
};
// or
@@ -162,7 +87,7 @@ the `Span::style` field.
These items were deprecated since 0.10.
- You should use styling capabilities of [`text::Line`] given as argument of [`Axis::title`]
instead of `Axis::title_style`
instead of `Axis::title_style`
- You should use styling capabilities of [`Buffer::set_style`] instead of `Buffer::set_background`
[`text::Line`]: https://docs.rs/ratatui/latest/ratatui/text/struct.Line.html
@@ -352,7 +277,7 @@ new module locations. E.g.:
```diff
- use ratatui::{widgets::scrollbar::{Scrollbar, Set}};
// becomes
+ use ratatui::{widgets::Scrollbar, symbols::scrollbar::Set}
+ use ratatui::{widgets::Scrollbar, symbols::scrollbar::Set}
```
### MSRV updated to 1.67 ([#361])
@@ -373,7 +298,7 @@ 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])
### MSRV is 1.65.0 ([#171])
[#171]: https://github.com/ratatui-org/ratatui/issues/171
@@ -384,7 +309,7 @@ The minimum supported rust version is now 1.65.0.
[#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.
and `ViewPort` was changed from a struct to an enum.
```diff
let terminal = Terminal::with_options(backend, TerminalOptions {
@@ -401,7 +326,7 @@ let terminal = Terminal::with_options(backend, TerminalOptions {
[#168]: https://github.com/ratatui-org/ratatui/issues/168
A new type `Masked` was introduced that implements `From<Text<'a>>`. This causes any code that did
previously did not need to use type annotations to fail to compile. To fix this, annotate or call
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.:
```diff

View File

@@ -171,12 +171,6 @@ struct Foo {}
- Code items should be between backticks
i.e. ``[`Block`]``, **NOT** ``[Block]``
### Deprecation notice
We generally want to wait at least two versions before removing deprecated items so users have
time to update. However, if a deprecation is blocking for us to implement a new feature we may
*consider* removing it in a one version notice.
### Use of unsafe for optimization purposes
We don't currently use any unsafe code in Ratatui, and would like to keep it that way. However there

View File

@@ -24,7 +24,7 @@ rust-version = "1.70.0"
[dependencies]
crossterm = { version = "0.27", optional = true }
termion = { version = "3.0", optional = true }
termion = { version = "2.0", optional = true }
termwiz = { version = "0.20.0", optional = true }
serde = { version = "1", optional = true, features = ["derive"] }
@@ -50,15 +50,10 @@ cargo-husky = { version = "1.5.0", default-features = false, features = [
] }
color-eyre = "0.6.2"
criterion = { version = "0.5.1", features = ["html_reports"] }
derive_builder = "0.12.0"
fakeit = "1.1"
font8x8 = "0.3.1"
palette = "0.7.3"
pretty_assertions = "1.4.0"
rand = "0.8.5"
rand_chacha = "0.3.1"
rstest = "0.18.2"
serde_json = "1.0.109"
[features]
#! The crate provides a set of optional features that can be enabled in your `cargo.toml` file.
@@ -212,16 +207,6 @@ name = "layout"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "constraints"
required-features = ["crossterm"]
doc-scrape-examples = false
[[example]]
name = "flex"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "list"
required-features = ["crossterm"]
@@ -282,7 +267,3 @@ doc-scrape-examples = true
name = "inline"
required-features = ["crossterm"]
doc-scrape-examples = true
[[test]]
name = "state_serde"
required-features = ["serde"]

View File

@@ -21,25 +21,33 @@
<!-- cargo-rdme start -->
![Demo](https://github.com/ratatui-org/ratatui/blob/1d39444e3dea6f309cf9035be2417ac711c1abc9/examples/demo2-destroy.gif?raw=true)
![Demo](https://raw.githubusercontent.com/ratatui-org/ratatui/b33c878808c4c40591d7a2d9f9d94d6fee95a96f/examples/demo2.gif)
<div align="center">
[![Crate Badge]][Crate] [![Docs Badge]][API Docs] [![CI Badge]][CI Workflow] [![License
Badge]](./LICENSE)<br>
[![Codecov Badge]][Codecov] [![Deps.rs Badge]][Deps.rs] [![Discord Badge]][Discord Server]
[![Matrix Badge]][Matrix]<br>
[![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>
[Ratatui Website] · [API Docs] · [Examples] · [Changelog] · [Breaking Changes]<br>
[Contributing] · [Report a bug] · [Request a Feature] · [Create a Pull Request]
[Documentation](https://docs.rs/ratatui)
· [Ratatui Website](https://ratatui.rs)
· [Examples](https://github.com/ratatui-org/ratatui/tree/main/examples)
· [Report a bug](https://github.com/ratatui-org/ratatui/issues/new?labels=bug&projects=&template=bug_report.md)
· [Request a Feature](https://github.com/ratatui-org/ratatui/issues/new?labels=enhancement&projects=&template=feature_request.md)
· [Send a Pull Request](https://github.com/ratatui-org/ratatui/compare)
</div>
# Ratatui
[Ratatui][Ratatui Website] 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.
[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
@@ -58,26 +66,28 @@ section of the [Ratatui Website] for more details on how to use other 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 Website]
for more info.
automatically redrawn on the next frame. See the [Rendering] section of the [Ratatui Website] for
more info.
## Other documentation
- [Ratatui Website] - explains the library's concepts and provides step-by-step tutorials
- [API Docs] - the full API documentation for the library on docs.rs.
- [Examples] - a collection of examples that demonstrate how to use the library.
- [Contributing] - Please read this if you are interested in contributing to the project.
- [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
the [Examples] directory. For more guidance on different ways to structure your application see
the [Application Patterns] and [Hello World tutorial] sections in the [Ratatui Website] and the
various [Examples]. There are also several starter templates available in the [templates]
repository.
[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 Website] and the various
[Examples]. There are also several starter templates available:
- [template]
- [async-template] (book and template)
Every application built with `ratatui` needs to implement the following steps:
@@ -107,8 +117,8 @@ module] and the [Backends] section of the [Ratatui Website] for more info.
The drawing logic is delegated to a closure that takes a [`Frame`] instance as argument. The
[`Frame`] provides the size of the area to draw to and allows the app to render any [`Widget`]
using the provided [`render_widget`] method. See the [Widgets] section of the [Ratatui Website]
for more info.
using the provided [`render_widget`] method. See the [Widgets] section of the [Ratatui Website] for
more info.
### Handling events
@@ -121,11 +131,10 @@ Website] for more info. For example, if you are using [Crossterm], you can use t
```rust
use std::io::{self, stdout};
use crossterm::{
event::{self, Event, KeyCode},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}
};
use ratatui::{prelude::*, widgets::*};
@@ -151,7 +160,7 @@ fn handle_events() -> io::Result<bool> {
if key.kind == event::KeyEventKind::Press && key.code == KeyCode::Char('q') {
return Ok(true);
}
}
}
}
Ok(false)
}
@@ -186,7 +195,7 @@ fn ui(frame: &mut Frame) {
Constraint::Length(1),
Constraint::Min(0),
Constraint::Length(1),
],
]
)
.split(frame.size());
frame.render_widget(
@@ -200,7 +209,7 @@ fn ui(frame: &mut Frame) {
let inner_layout = Layout::new(
Direction::Horizontal,
[Constraint::Percentage(50), Constraint::Percentage(50)],
[Constraint::Percentage(50), Constraint::Percentage(50)]
)
.split(main_layout[1]);
frame.render_widget(
@@ -242,7 +251,7 @@ fn ui(frame: &mut Frame) {
Constraint::Length(1),
Constraint::Length(1),
Constraint::Min(0),
],
]
)
.split(frame.size());
@@ -290,14 +299,12 @@ Running this example produces the following output:
[Handling Events]: https://ratatui.rs/concepts/event-handling/
[Layout]: https://ratatui.rs/how-to/layout/
[Styling Text]: https://ratatui.rs/how-to/render/style-text/
[templates]: https://github.com/ratatui-org/templates/
[Examples]: https://github.com/ratatui-org/ratatui/tree/main/examples/README.md
[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
[Create a Pull Request]: https://github.com/ratatui-org/ratatui/compare
[template]: https://github.com/ratatui-org/template
[async-template]: https://ratatui-org.github.io/async-template
[Examples]: https://github.com/ratatui-org/ratatui/tree/main/examples
[git-cliff]: https://git-cliff.org
[Conventional Commits]: https://www.conventionalcommits.org
[API Docs]: https://docs.rs/ratatui
[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
@@ -317,28 +324,24 @@ Running this example produces the following output:
[`Backend`]: backend::Backend
[`backend` module]: backend
[`crossterm::event`]: https://docs.rs/crossterm/latest/crossterm/event/index.html
[Crate]: https://crates.io/crates/ratatui
[Ratatui]: https://ratatui.rs
[Crossterm]: https://crates.io/crates/crossterm
[Termion]: https://crates.io/crates/termion
[Termwiz]: https://crates.io/crates/termwiz
[tui-rs]: 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
[License Badge]: https://img.shields.io/crates/l/ratatui?style=flat-square
[CI Badge]:
https://img.shields.io/github/actions/workflow/status/ratatui-org/ratatui/ci.yml?style=flat-square&logo=github
[CI Workflow]: https://github.com/ratatui-org/ratatui/actions/workflows/ci.yml
[Codecov Badge]:
https://img.shields.io/codecov/c/github/ratatui-org/ratatui?logo=codecov&style=flat-square&token=BAQ8SOKEST
[Codecov]: https://app.codecov.io/gh/ratatui-org/ratatui
[Deps.rs Badge]: https://deps.rs/repo/github/ratatui-org/ratatui/status.svg?style=flat-square
[Deps.rs]: https://deps.rs/repo/github/ratatui-org/ratatui
[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
[Discord Server]: https://discord.gg/pMCEU9hNEj
[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
[Matrix]: https://matrix.to/#/#ratatui:matrix.org
<!-- cargo-rdme end -->
@@ -387,8 +390,9 @@ The library comes with the following
- [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] folder. Run each example
with cargo (e.g. to run the gauge example `cargo run --example gauge`), and quit by pressing `q`.
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
pressing `q`.
You can also run all examples by running `cargo make run-examples` (requires `cargo-make` that can
be installed with `cargo install cargo-make`).
@@ -399,8 +403,8 @@ be installed with `cargo install cargo-make`).
`ratatui::text::Text`
- [color-to-tui](https://github.com/uttarayan21/color-to-tui) — Parse hex colors to
`ratatui::style::Color`
- [templates](https://github.com/ratatui-org/templates) — Starter templates for
bootstrapping a Rust TUI application with Ratatui & crossterm
- [rust-tui-template](https://github.com/ratatui-org/rust-tui-template) — A template for
bootstrapping a Rust TUI application with Tui-rs & crossterm
- [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

View File

@@ -30,13 +30,6 @@ body = """
{{commit.body | indent(prefix=" ") }}
````
{%- endif %}
{%- for footer in commit.footers %}
{%- if footer.token != "Signed-off-by" and footer.token != "Co-authored-by" %}
{{ footer.token | indent(prefix=" ") }}{{ footer.separator }}
{{ footer.value | indent(prefix=" ") }}
{%- endif %}
{%- endfor %}
{% endmacro -%}
{% for group, commits in commits | group_by(attribute="group") %}

View File

@@ -1,27 +1,8 @@
# Examples
This folder contains unreleased code. View the [examples for the latest release
(0.25.0)](https://github.com/ratatui-org/ratatui/tree/v0.25.0/examples) instead.
> [!WARNING]
>
> There are backwards incompatible changes in these examples, as they are designed to compile
> against the `main` branch.
>
> There are a few workaround for this problem:
>
> - View the examples as they were when the latest version was release by selecting the tag that
> matches that version. E.g. <https://github.com/ratatui-org/ratatui/tree/v0.25.0/examples>. There
> is a combo box at the top of this page which allows you to select any previous tagged version.
> - To view the code locally, checkout the tag using `git switch --detach v0.25.0`.
> - Use the latest [alpha version of Ratatui]. These are released weekly on Saturdays.
> - Compile your code against the main branch either locally by adding e.g. `path = "../ratatui"` to
> the dependency, or remotely by adding `git = "https://github.com/ratatui-org/ratatui"`
>
> For a list of unreleased breaking changes, see [BREAKING-CHANGES.md].
>
> We don't keep the CHANGELOG updated with unreleased changes, check the git commit history or run
> `git-cliff -u` against a cloned version of this repository.
These gifs were created using [VHS](https://github.com/charmbracelet/vhs). Each example has a
corresponding `.tape` file that holds instructions for how to generate the images. Note that the
images themselves are stored in a separate git branch to avoid bloating the main repository.
## Demo2
@@ -315,18 +296,11 @@ cargo run --example=user_input --features=crossterm
![User Input][user_input.gif]
## How to update these examples
These gifs were created using [VHS](https://github.com/charmbracelet/vhs). Each example has a
corresponding `.tape` file that holds instructions for how to generate the images. Note that the
images themselves are stored in a separate `images` git branch to avoid bloating the main
repository.
<!--
links to images to make it easier to update in bulk
These are generated with `vhs publish examples/xxx.gif`
Links to images to make them easier to update in bulk. Use the following script to update and upload
the examples to the images branch. (Requires push access to the branch).
To update these examples in bulk:
```shell
examples/generate.bash
```
@@ -356,6 +330,3 @@ examples/generate.bash
[table.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/table.gif?raw=true
[tabs.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/tabs.gif?raw=true
[user_input.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/user_input.gif?raw=true
[alpha version of Ratatui]: https://crates.io/crates/ratatui/versions
[BREAKING-CHANGES.md]: https://github.com/ratatui-org/ratatui/blob/main/BREAKING-CHANGES.md

View File

@@ -134,11 +134,11 @@ fn run_app<B: Backend>(
}
}
fn ui(frame: &mut Frame, app: &App) {
let vertical = Layout::vertical([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)]);
let horizontal = Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]);
let [top, bottom] = frame.size().split(&vertical);
let [left, right] = bottom.split(&horizontal);
fn ui(f: &mut Frame, app: &App) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)])
.split(f.size());
let barchart = BarChart::default()
.block(Block::default().title("Data1").borders(Borders::ALL))
@@ -146,10 +146,15 @@ fn ui(frame: &mut Frame, app: &App) {
.bar_width(9)
.bar_style(Style::default().fg(Color::Yellow))
.value_style(Style::default().fg(Color::Black).bg(Color::Yellow));
f.render_widget(barchart, chunks[0]);
frame.render_widget(barchart, top);
draw_bar_with_group_labels(frame, app, left);
draw_horizontal_bars(frame, app, right);
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(chunks[1]);
draw_bar_with_group_labels(f, app, chunks[0]);
draw_horizontal_bars(f, app, chunks[1]);
}
fn create_groups<'a>(app: &'a App, combine_values_and_labels: bool) -> Vec<BarGroup<'a>> {

View File

@@ -103,14 +103,20 @@ fn ui(frame: &mut Frame) {
///
/// Returns a tuple of the title area and the main areas.
fn calculate_layout(area: Rect) -> (Rect, Vec<Vec<Rect>>) {
let main_layout = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
let block_layout = &Layout::vertical([Constraint::Max(4); 9]);
let [title_area, main_area] = area.split(&main_layout);
let main_areas = block_layout
.split(main_area)
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(1), Constraint::Min(0)])
.split(area);
let title_area = layout[0];
let main_areas = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Max(4); 9])
.split(layout[1])
.iter()
.map(|&area| {
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(area)
.to_vec()
})

View File

@@ -1,4 +1,4 @@
use std::{error::Error, io};
use std::{error::Error, io, rc::Rc};
use crossterm::{
event::{self, Event, KeyCode},
@@ -55,19 +55,43 @@ fn draw(f: &mut Frame) {
let list = make_dates(start.year());
let rows = Layout::vertical([Constraint::Ratio(1, 3); 3]).split(calarea);
let cols = rows.iter().flat_map(|row| {
Layout::horizontal([Constraint::Ratio(1, 4); 4])
.split(*row)
.to_vec()
});
for col in cols {
for chunk in split_rows(&calarea)
.iter()
.flat_map(|row| split_cols(row).to_vec())
{
let cal = cals::get_cal(start.month(), start.year(), &list);
f.render_widget(cal, col);
f.render_widget(cal, chunk);
start = start.replace_month(start.month().next()).unwrap();
}
}
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),
]);
list_layout.split(*area)
}
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),
]);
list_layout.split(*area)
}
fn make_dates(current_year: i32) -> CalendarEventStore {
let mut list = CalendarEventStore::today(
Style::default()

View File

@@ -107,15 +107,19 @@ impl App {
}
fn ui(&self, frame: &mut Frame) {
let horizontal =
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]);
let vertical = Layout::vertical([Constraint::Percentage(50), Constraint::Percentage(50)]);
let [map, right] = frame.size().split(&horizontal);
let [pong, boxes] = right.split(&vertical);
let main_layout = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(frame.size());
frame.render_widget(self.map_canvas(), map);
frame.render_widget(self.pong_canvas(), pong);
frame.render_widget(self.boxes_canvas(boxes), boxes);
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 + '_ {

View File

@@ -132,17 +132,27 @@ fn run_app<B: Backend>(
}
}
fn ui(frame: &mut Frame, app: &App) {
let area = frame.size();
fn ui(f: &mut Frame, app: &App) {
let size = f.size();
let vertical_chunks = Layout::new(
Direction::Vertical,
[Constraint::Percentage(40), Constraint::Percentage(60)],
)
.split(size);
let vertical = Layout::vertical([Constraint::Percentage(40), Constraint::Percentage(60)]);
let horizontal = Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]);
let [chart1, bottom] = area.split(&vertical);
let [line_chart, scatter] = bottom.split(&horizontal);
// top chart
render_chart1(f, vertical_chunks[0], app);
render_chart1(frame, chart1, app);
render_line_chart(frame, line_chart);
render_scatter(frame, scatter);
let horizontal_chunks = Layout::new(
Direction::Horizontal,
[Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)],
)
.split(vertical_chunks[1]);
// bottom left
render_line_chart(f, horizontal_chunks[0]);
// bottom right
render_scatter(f, horizontal_chunks[1]);
}
fn render_chart1(f: &mut Frame, area: Rect, app: &App) {
@@ -196,7 +206,7 @@ fn render_chart1(f: &mut Frame, area: Rect, app: &App) {
fn render_line_chart(f: &mut Frame, area: Rect) {
let datasets = vec![Dataset::default()
.name("Line from only 2 points".italic())
.name("Line from only 2 points")
.marker(symbols::Marker::Braille)
.style(Style::default().fg(Color::Yellow))
.graph_type(GraphType::Line)
@@ -241,7 +251,7 @@ fn render_scatter(f: &mut Frame, area: Rect) {
.style(Style::new().yellow())
.data(&HEAVY_PAYLOAD_DATA),
Dataset::default()
.name("Medium".underlined())
.name("Medium")
.marker(Marker::Braille)
.graph_type(GraphType::Scatter)
.style(Style::new().magenta())

View File

@@ -42,12 +42,14 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
}
fn ui(frame: &mut Frame) {
let layout = Layout::vertical([
Constraint::Length(30),
Constraint::Length(17),
Constraint::Length(2),
])
.split(frame.size());
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(30),
Constraint::Length(17),
Constraint::Length(2),
])
.split(frame.size());
render_named_colors(frame, layout[0]);
render_indexed_colors(frame, layout[1]);
@@ -74,7 +76,10 @@ const NAMED_COLORS: [Color; 16] = [
];
fn render_named_colors(frame: &mut Frame, area: Rect) {
let layout = Layout::vertical([Constraint::Length(3); 10]).split(area);
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(3); 10])
.split(area);
render_fg_named_colors(frame, Color::Reset, layout[0]);
render_fg_named_colors(frame, Color::Black, layout[1]);
@@ -94,11 +99,15 @@ fn render_fg_named_colors(frame: &mut Frame, bg: Color, area: Rect) {
let inner = block.inner(area);
frame.render_widget(block, area);
let layout = Layout::vertical([Constraint::Length(1); 2])
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(1); 2])
.split(inner)
.iter()
.flat_map(|area| {
Layout::horizontal([Constraint::Ratio(1, 8); 8])
Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Ratio(1, 8); 8])
.split(*area)
.to_vec()
})
@@ -115,11 +124,15 @@ fn render_bg_named_colors(frame: &mut Frame, fg: Color, area: Rect) {
let inner = block.inner(area);
frame.render_widget(block, area);
let layout = Layout::vertical([Constraint::Length(1); 2])
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(1); 2])
.split(inner)
.iter()
.flat_map(|area| {
Layout::horizontal([Constraint::Ratio(1, 8); 8])
Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Ratio(1, 8); 8])
.split(*area)
.to_vec()
})
@@ -136,18 +149,23 @@ fn render_indexed_colors(frame: &mut Frame, area: Rect) {
let inner = block.inner(area);
frame.render_widget(block, area);
let layout = Layout::vertical([
Constraint::Length(1), // 0 - 15
Constraint::Length(1), // blank
Constraint::Min(6), // 16 - 123
Constraint::Length(1), // blank
Constraint::Min(6), // 124 - 231
Constraint::Length(1), // blank
])
.split(inner);
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1), // 0 - 15
Constraint::Length(1), // blank
Constraint::Min(6), // 16 - 123
Constraint::Length(1), // blank
Constraint::Min(6), // 124 - 231
Constraint::Length(1), // blank
])
.split(inner);
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
let color_layout = Layout::horizontal([Constraint::Length(5); 16]).split(layout[0]);
let color_layout = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Length(5); 16])
.split(layout[0]);
for i in 0..16 {
let color = Color::Indexed(i);
let color_index = format!("{i:0>2}");
@@ -178,19 +196,25 @@ fn render_indexed_colors(frame: &mut Frame, area: Rect) {
.iter()
// two rows of 3 columns
.flat_map(|area| {
Layout::horizontal([Constraint::Length(27); 3])
Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Length(27); 3])
.split(*area)
.to_vec()
})
// each with 6 rows
.flat_map(|area| {
Layout::vertical([Constraint::Length(1); 6])
Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(1); 6])
.split(area)
.to_vec()
})
// each with 6 columns
.flat_map(|area| {
Layout::horizontal([Constraint::Min(4); 6])
Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Min(4); 6])
.split(area)
.to_vec()
})
@@ -220,18 +244,22 @@ fn title_block(title: String) -> Block<'static> {
}
fn render_indexed_grayscale(frame: &mut Frame, area: Rect) {
let layout = Layout::vertical([
Constraint::Length(1), // 232 - 243
Constraint::Length(1), // 244 - 255
])
.split(area)
.iter()
.flat_map(|area| {
Layout::horizontal([Constraint::Length(6); 12])
.split(*area)
.to_vec()
})
.collect_vec();
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1), // 232 - 243
Constraint::Length(1), // 244 - 255
])
.split(area)
.iter()
.flat_map(|area| {
Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Length(6); 12])
.split(*area)
.to_vec()
})
.collect_vec();
for i in 232..=255 {
let color = Color::Indexed(i);

View File

@@ -158,14 +158,18 @@ impl<'a> AppWidget<'a> {
impl Widget for AppWidget<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
let vertical = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
let horizontal = Layout::horizontal([Constraint::Min(0), Constraint::Length(8)]);
let [top, colors] = area.split(&vertical);
let [title, fps] = top.split(&horizontal);
let main_layout = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(1), Constraint::Min(0)])
.split(area);
let title_layout = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Min(0), Constraint::Length(8)])
.split(main_layout[0]);
self.title.render(title, buf);
self.fps_widget.render(fps, buf);
self.rgb_colors_widget.render(colors, buf);
self.title.render(title_layout[0], buf);
self.fps_widget.render(title_layout[1], buf);
self.rgb_colors_widget.render(main_layout[1], buf);
}
}

View File

@@ -1,454 +0,0 @@
use std::io::{self, stdout};
use color_eyre::{config::HookBuilder, Result};
use crossterm::{
event::{self, Event, KeyCode},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
};
use ratatui::{layout::Constraint::*, prelude::*, style::palette::tailwind, widgets::*};
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
const SPACER_HEIGHT: u16 = 0;
const ILLUSTRATION_HEIGHT: u16 = 4;
const EXAMPLE_HEIGHT: u16 = ILLUSTRATION_HEIGHT + SPACER_HEIGHT;
// priority 1
const FIXED_COLOR: Color = tailwind::RED.c900;
// priority 2
const MIN_COLOR: Color = tailwind::BLUE.c900;
const MAX_COLOR: Color = tailwind::BLUE.c800;
// priority 3
const LENGTH_COLOR: Color = tailwind::SLATE.c700;
const PERCENTAGE_COLOR: Color = tailwind::SLATE.c800;
const RATIO_COLOR: Color = tailwind::SLATE.c900;
// priority 4
const PROPORTIONAL_COLOR: Color = tailwind::SLATE.c950;
#[derive(Default, Clone, Copy)]
struct App {
selected_tab: SelectedTab,
scroll_offset: u16,
max_scroll_offset: u16,
state: AppState,
}
/// Tabs for the different examples
///
/// The order of the variants is the order in which they are displayed.
#[derive(Default, Debug, Copy, Clone, Display, FromRepr, EnumIter, PartialEq, Eq)]
enum SelectedTab {
#[default]
Fixed,
Min,
Max,
Length,
Percentage,
Ratio,
Proportional,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
enum AppState {
#[default]
Running,
Quit,
}
fn main() -> Result<()> {
init_error_hooks()?;
let terminal = init_terminal()?;
// increase the cache size to avoid flickering for indeterminate layouts
Layout::init_cache(100);
App::default().run(terminal)?;
restore_terminal()?;
Ok(())
}
impl App {
fn run(&mut self, mut terminal: Terminal<impl Backend>) -> Result<()> {
self.update_max_scroll_offset();
while self.is_running() {
self.draw(&mut terminal)?;
self.handle_events()?;
}
Ok(())
}
fn update_max_scroll_offset(&mut self) {
self.max_scroll_offset = (self.selected_tab.get_example_count() - 1) * EXAMPLE_HEIGHT;
}
fn is_running(&self) -> bool {
self.state == AppState::Running
}
fn draw(self, terminal: &mut Terminal<impl Backend>) -> io::Result<()> {
terminal.draw(|frame| frame.render_widget(self, frame.size()))?;
Ok(())
}
fn handle_events(&mut self) -> Result<()> {
if let Event::Key(key) = event::read()? {
use KeyCode::*;
match key.code {
Char('q') | Esc => self.quit(),
Char('l') | Right => self.next(),
Char('h') | Left => self.previous(),
Char('j') | Down => self.down(),
Char('k') | Up => self.up(),
Char('g') | Home => self.top(),
Char('G') | End => self.bottom(),
_ => (),
}
}
Ok(())
}
fn quit(&mut self) {
self.state = AppState::Quit;
}
fn next(&mut self) {
self.selected_tab = self.selected_tab.next();
self.update_max_scroll_offset();
self.scroll_offset = 0;
}
fn previous(&mut self) {
self.selected_tab = self.selected_tab.previous();
self.update_max_scroll_offset();
self.scroll_offset = 0;
}
fn up(&mut self) {
self.scroll_offset = self.scroll_offset.saturating_sub(1)
}
fn down(&mut self) {
self.scroll_offset = self
.scroll_offset
.saturating_add(1)
.min(self.max_scroll_offset)
}
fn top(&mut self) {
self.scroll_offset = 0;
}
fn bottom(&mut self) {
self.scroll_offset = self.max_scroll_offset;
}
}
impl Widget for App {
fn render(self, area: Rect, buf: &mut Buffer) {
let [tabs, axis, demo] = area.split(&Layout::vertical([
Constraint::Fixed(3),
Constraint::Fixed(3),
Proportional(0),
]));
self.render_tabs(tabs, buf);
self.render_axis(axis, buf);
self.render_demo(demo, buf);
}
}
impl App {
fn render_tabs(&self, area: Rect, buf: &mut Buffer) {
let titles = SelectedTab::iter().map(SelectedTab::to_tab_title);
let block = Block::new()
.title("Constraints ".bold())
.title(" Use h l or ◄ ► to change tab and j k or ▲ ▼ to scroll");
Tabs::new(titles)
.block(block)
.highlight_style(Modifier::REVERSED)
.select(self.selected_tab as usize)
.padding("", "")
.divider(" ")
.render(area, buf);
}
fn render_axis(&self, area: Rect, buf: &mut Buffer) {
let width = area.width as usize;
// a bar like `<----- 80 px ----->`
let width_label = format!("{} px", width);
let width_bar = format!(
"<{width_label:-^width$}>",
width = width - width_label.len() / 2
);
Paragraph::new(width_bar.dark_gray())
.alignment(Alignment::Center)
.block(Block::default().padding(Padding {
left: 0,
right: 0,
top: 1,
bottom: 0,
}))
.render(area, buf);
}
/// Render the demo content
///
/// This function renders the demo content into a separate buffer and then splices the buffer
/// into the main buffer. This is done to make it possible to handle scrolling easily.
fn render_demo(&self, area: Rect, buf: &mut Buffer) {
// render demo content into a separate buffer so all examples fit we add an extra
// area.height to make sure the last example is fully visible even when the scroll offset is
// at the max
let height = self.selected_tab.get_example_count() * EXAMPLE_HEIGHT;
let demo_area = Rect::new(0, 0, area.width, height + area.height);
let mut demo_buf = Buffer::empty(demo_area);
let scrollbar_needed = self.scroll_offset != 0 || height > area.height;
let content_area = if scrollbar_needed {
Rect {
width: demo_area.width - 1,
..demo_area
}
} else {
demo_area
};
self.selected_tab.render(content_area, &mut demo_buf);
let visible_content = demo_buf
.content
.into_iter()
.skip((demo_area.width * self.scroll_offset) as usize)
.take(area.area() as usize);
for (i, cell) in visible_content.enumerate() {
let x = i as u16 % area.width;
let y = i as u16 / area.width;
*buf.get_mut(area.x + x, area.y + y) = cell;
}
if scrollbar_needed {
let mut state = ScrollbarState::new(self.max_scroll_offset as usize)
.position(self.scroll_offset as usize);
Scrollbar::new(ScrollbarOrientation::VerticalRight).render(area, buf, &mut state);
}
}
}
impl SelectedTab {
/// Get the previous tab, if there is no previous tab return the current tab.
fn previous(&self) -> Self {
let current_index: usize = *self as usize;
let previous_index = current_index.saturating_sub(1);
Self::from_repr(previous_index).unwrap_or(*self)
}
/// Get the next tab, if there is no next tab return the current tab.
fn next(&self) -> Self {
let current_index = *self as usize;
let next_index = current_index.saturating_add(1);
Self::from_repr(next_index).unwrap_or(*self)
}
fn get_example_count(&self) -> u16 {
use SelectedTab::*;
match self {
Fixed => 4,
Length => 4,
Percentage => 5,
Ratio => 4,
Proportional => 2,
Min => 5,
Max => 5,
}
}
fn to_tab_title(value: SelectedTab) -> Line<'static> {
use SelectedTab::*;
let text = format!(" {value} ");
let color = match value {
Fixed => FIXED_COLOR,
Length => LENGTH_COLOR,
Percentage => PERCENTAGE_COLOR,
Ratio => RATIO_COLOR,
Proportional => PROPORTIONAL_COLOR,
Min => MIN_COLOR,
Max => MAX_COLOR,
};
text.fg(tailwind::SLATE.c200).bg(color).into()
}
}
impl Widget for SelectedTab {
fn render(self, area: Rect, buf: &mut Buffer) {
match self {
SelectedTab::Fixed => self.render_fixed_example(area, buf),
SelectedTab::Length => self.render_length_example(area, buf),
SelectedTab::Percentage => self.render_percentage_example(area, buf),
SelectedTab::Ratio => self.render_ratio_example(area, buf),
SelectedTab::Proportional => self.render_proportional_example(area, buf),
SelectedTab::Min => self.render_min_example(area, buf),
SelectedTab::Max => self.render_max_example(area, buf),
}
}
}
impl SelectedTab {
fn render_fixed_example(&self, area: Rect, buf: &mut Buffer) {
let [example1, example2, example3, example4, _] =
area.split(&Layout::vertical([Fixed(EXAMPLE_HEIGHT); 5]));
Example::new(&[Fixed(40), Proportional(0)]).render(example1, buf);
Example::new(&[Fixed(20), Fixed(20), Proportional(0)]).render(example2, buf);
Example::new(&[Fixed(20), Min(20), Max(20)]).render(example3, buf);
Example::new(&[
Length(20),
Percentage(20),
Ratio(1, 5),
Proportional(1),
Fixed(15),
])
.render(example4, buf);
}
fn render_length_example(&self, area: Rect, buf: &mut Buffer) {
let [example1, example2, example3, example4, _] =
area.split(&Layout::vertical([Fixed(EXAMPLE_HEIGHT); 5]));
Example::new(&[Length(20), Fixed(20)]).render(example1, buf);
Example::new(&[Length(20), Length(20)]).render(example2, buf);
Example::new(&[Length(20), Min(20)]).render(example3, buf);
Example::new(&[Length(20), Max(20)]).render(example4, buf);
}
fn render_percentage_example(&self, area: Rect, buf: &mut Buffer) {
let [example1, example2, example3, example4, example5, _] =
area.split(&Layout::vertical([Fixed(EXAMPLE_HEIGHT); 6]));
Example::new(&[Percentage(75), Proportional(0)]).render(example1, buf);
Example::new(&[Percentage(25), Proportional(0)]).render(example2, buf);
Example::new(&[Percentage(50), Min(20)]).render(example3, buf);
Example::new(&[Percentage(0), Max(0)]).render(example4, buf);
Example::new(&[Percentage(0), Proportional(0)]).render(example5, buf);
}
fn render_ratio_example(&self, area: Rect, buf: &mut Buffer) {
let [example1, example2, example3, example4, _] =
area.split(&Layout::vertical([Fixed(EXAMPLE_HEIGHT); 5]));
Example::new(&[Ratio(1, 2); 2]).render(example1, buf);
Example::new(&[Ratio(1, 4); 4]).render(example2, buf);
Example::new(&[Ratio(1, 2), Ratio(1, 3), Ratio(1, 4)]).render(example3, buf);
Example::new(&[Ratio(1, 2), Percentage(25), Length(10)]).render(example4, buf);
}
fn render_proportional_example(&self, area: Rect, buf: &mut Buffer) {
let [example1, example2, _] = area.split(&Layout::vertical([Fixed(EXAMPLE_HEIGHT); 3]));
Example::new(&[Proportional(1), Proportional(2), Proportional(3)]).render(example1, buf);
Example::new(&[Proportional(1), Percentage(50), Proportional(1)]).render(example2, buf);
}
fn render_min_example(&self, area: Rect, buf: &mut Buffer) {
let [example1, example2, example3, example4, example5, _] =
area.split(&Layout::vertical([Fixed(EXAMPLE_HEIGHT); 6]));
Example::new(&[Percentage(100), Min(0)]).render(example1, buf);
Example::new(&[Percentage(100), Min(20)]).render(example2, buf);
Example::new(&[Percentage(100), Min(40)]).render(example3, buf);
Example::new(&[Percentage(100), Min(60)]).render(example4, buf);
Example::new(&[Percentage(100), Min(80)]).render(example5, buf);
}
fn render_max_example(&self, area: Rect, buf: &mut Buffer) {
let [example1, example2, example3, example4, example5, _] =
area.split(&Layout::vertical([Fixed(EXAMPLE_HEIGHT); 6]));
Example::new(&[Percentage(0), Max(0)]).render(example1, buf);
Example::new(&[Percentage(0), Max(20)]).render(example2, buf);
Example::new(&[Percentage(0), Max(40)]).render(example3, buf);
Example::new(&[Percentage(0), Max(60)]).render(example4, buf);
Example::new(&[Percentage(0), Max(80)]).render(example5, buf);
}
}
struct Example {
constraints: Vec<Constraint>,
}
impl Example {
fn new(constraints: &[Constraint]) -> Self {
Self {
constraints: constraints.into(),
}
}
}
impl Widget for Example {
fn render(self, area: Rect, buf: &mut Buffer) {
let [area, _] = area.split(&Layout::vertical([
Fixed(ILLUSTRATION_HEIGHT),
Fixed(SPACER_HEIGHT),
]));
let blocks = Layout::horizontal(&self.constraints).split(area);
for (block, constraint) in blocks.iter().zip(&self.constraints) {
self.illustration(*constraint, block.width)
.render(*block, buf);
}
}
}
impl Example {
fn illustration(&self, constraint: Constraint, width: u16) -> Paragraph {
let color = match constraint {
Constraint::Fixed(_) => FIXED_COLOR,
Constraint::Length(_) => LENGTH_COLOR,
Constraint::Percentage(_) => PERCENTAGE_COLOR,
Constraint::Ratio(_, _) => RATIO_COLOR,
Constraint::Proportional(_) => PROPORTIONAL_COLOR,
Constraint::Min(_) => MIN_COLOR,
Constraint::Max(_) => MAX_COLOR,
};
let fg = Color::White;
let title = format!("{constraint}");
let content = format!("{width} px");
let text = format!("{title}\n{content}");
let block = Block::bordered()
.border_set(symbols::border::QUADRANT_OUTSIDE)
.border_style(Style::reset().fg(color).reversed())
.style(Style::default().fg(fg).bg(color));
Paragraph::new(text)
.alignment(Alignment::Center)
.block(block)
}
}
fn init_error_hooks() -> Result<()> {
let (panic, error) = HookBuilder::default().into_hooks();
let panic = panic.into_panic_hook();
let error = error.into_eyre_hook();
color_eyre::eyre::set_hook(Box::new(move |e| {
let _ = restore_terminal();
error(e)
}))?;
std::panic::set_hook(Box::new(move |info| {
let _ = restore_terminal();
panic(info)
}));
Ok(())
}
fn init_terminal() -> Result<Terminal<impl Backend>> {
enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout());
let terminal = Terminal::new(backend)?;
Ok(terminal)
}
fn restore_terminal() -> Result<()> {
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
Ok(())
}

View File

@@ -1,14 +0,0 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/constraints.tape`
Output "target/constraints.gif"
Set Theme "Aardvark Blue"
Set FontSize 18
Set Width 1200
Set Height 700
Hide
Type "cargo run --example=constraints --features=crossterm"
Enter
Sleep 2s
Show
Sleep 5s
Right @5s 7

View File

@@ -171,34 +171,42 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
}
fn ui(frame: &mut Frame, states: &[State; 3]) {
let vertical = Layout::vertical([
Constraint::Length(1),
Constraint::Max(3),
Constraint::Length(1),
Constraint::Min(0), // ignore remaining space
]);
let [title, buttons, help, _] = frame.size().split(&vertical);
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)"),
title,
layout[0],
);
render_buttons(frame, layout[1], states);
frame.render_widget(
Paragraph::new("←/→: select, Space: toggle, q: quit"),
layout[2],
);
render_buttons(frame, buttons, states);
frame.render_widget(Paragraph::new("←/→: select, Space: toggle, q: quit"), help);
}
fn render_buttons(frame: &mut Frame<'_>, area: Rect, states: &[State; 3]) {
let horizontal = Layout::horizontal([
Constraint::Length(15),
Constraint::Length(15),
Constraint::Length(15),
Constraint::Min(0), // ignore remaining space
]);
let [red, green, blue, _] = area.split(&horizontal);
frame.render_widget(Button::new("Red").theme(RED).state(states[0]), red);
frame.render_widget(Button::new("Green").theme(GREEN).state(states[1]), green);
frame.render_widget(Button::new("Blue").theme(BLUE).state(states[2]), blue);
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(

View File

@@ -6,13 +6,16 @@ use ratatui::{
use crate::app::App;
pub fn draw(f: &mut Frame, app: &mut App) {
let chunks = Layout::vertical([Constraint::Length(3), Constraint::Min(0)]).split(f.size());
let tabs = app
let chunks = Layout::default()
.constraints([Constraint::Length(3), Constraint::Min(0)])
.split(f.size());
let titles = app
.tabs
.titles
.iter()
.map(|t| text::Line::from(Span::styled(*t, Style::default().fg(Color::Green))))
.collect::<Tabs>()
.collect();
let tabs = Tabs::new(titles)
.block(Block::default().borders(Borders::ALL).title(app.title))
.highlight_style(Style::default().fg(Color::Yellow))
.select(app.tabs.index);
@@ -26,25 +29,27 @@ pub fn draw(f: &mut Frame, app: &mut App) {
}
fn draw_first_tab(f: &mut Frame, app: &mut App, area: Rect) {
let chunks = Layout::vertical([
Constraint::Length(9),
Constraint::Min(8),
Constraint::Length(7),
])
.split(area);
let chunks = Layout::default()
.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(f: &mut Frame, app: &mut App, area: Rect) {
let chunks = Layout::vertical([
Constraint::Length(2),
Constraint::Length(3),
Constraint::Length(1),
])
.margin(1)
.split(area);
let chunks = Layout::default()
.constraints([
Constraint::Length(2),
Constraint::Length(3),
Constraint::Length(1),
])
.margin(1)
.split(area);
let block = Block::default().borders(Borders::ALL).title("Graphs");
f.render_widget(block, area);
@@ -91,14 +96,19 @@ fn draw_charts(f: &mut Frame, app: &mut App, area: Rect) {
} else {
vec![Constraint::Percentage(100)]
};
let chunks = Layout::horizontal(constraints).split(area);
let chunks = Layout::default()
.constraints(constraints)
.direction(Direction::Horizontal)
.split(area);
{
let chunks = Layout::vertical([Constraint::Percentage(50), Constraint::Percentage(50)])
let chunks = Layout::default()
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(chunks[0]);
{
let chunks =
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(chunks[0]);
let chunks = Layout::default()
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.direction(Direction::Horizontal)
.split(chunks[0]);
// Draw tasks
let tasks: Vec<ListItem> = app
@@ -263,8 +273,10 @@ fn draw_text(f: &mut Frame, area: Rect) {
}
fn draw_second_tab(f: &mut Frame, app: &mut App, area: Rect) {
let chunks =
Layout::horizontal([Constraint::Percentage(30), Constraint::Percentage(70)]).split(area);
let chunks = Layout::default()
.constraints([Constraint::Percentage(30), Constraint::Percentage(70)])
.direction(Direction::Horizontal)
.split(area);
let up_style = Style::default().fg(Color::Green);
let failure_style = Style::default()
.fg(Color::Red)
@@ -349,7 +361,10 @@ fn draw_second_tab(f: &mut Frame, app: &mut App, area: Rect) {
}
fn draw_third_tab(f: &mut Frame, _app: &mut App, area: Rect) {
let chunks = Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]).split(area);
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)])
.split(area);
let colors = [
Color::Reset,
Color::Black,

View File

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

View File

@@ -2,29 +2,15 @@ use std::time::Duration;
use anyhow::{Context, Result};
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
use rand::Rng;
use rand_chacha::rand_core::SeedableRng;
use ratatui::{buffer::Cell, layout::Flex, prelude::*, widgets::Widget};
use unicode_width::UnicodeWidthStr;
use ratatui::prelude::Rect;
use crate::{
big_text::{BigTextBuilder, PixelSize},
Root, Term,
};
use crate::{Root, Term};
#[derive(Debug)]
pub struct App {
term: Term,
should_quit: bool,
context: AppContext,
mode: Mode,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
enum Mode {
#[default]
Normal,
Destroy,
Quit,
}
#[derive(Debug, Default, Clone, Copy)]
@@ -37,15 +23,15 @@ impl App {
fn new() -> Result<Self> {
Ok(Self {
term: Term::start()?,
should_quit: false,
context: AppContext::default(),
mode: Mode::Normal,
})
}
pub fn run() -> Result<()> {
install_panic_hook();
let mut app = Self::new()?;
while !app.should_quit() {
while !app.should_quit {
app.draw()?;
app.handle_events()?;
}
@@ -55,20 +41,13 @@ impl App {
fn draw(&mut self) -> Result<()> {
self.term
.draw(|frame| {
frame.render_widget(Root::new(&self.context), frame.size());
if self.mode == Mode::Destroy {
destroy(frame);
}
})
.draw(|frame| frame.render_widget(Root::new(&self.context), frame.size()))
.context("terminal.draw")?;
Ok(())
}
fn handle_events(&mut self) -> Result<()> {
// https://superuser.com/questions/1449366/do-60-fps-gifs-actually-exist-or-is-the-maximum-50-fps
const GIF_FRAME_RATE: f64 = 50.0;
match Term::next_event(Duration::from_secs_f64(1.0 / GIF_FRAME_RATE))? {
match Term::next_event(Duration::from_millis(16))? {
Some(Event::Key(key)) => self.handle_key_event(key),
Some(Event::Resize(width, height)) => {
Ok(self.term.resize(Rect::new(0, 0, width, height))?)
@@ -86,7 +65,7 @@ impl App {
const TAB_COUNT: usize = 5;
match key.code {
KeyCode::Char('q') | KeyCode::Esc => {
self.mode = Mode::Quit;
self.should_quit = true;
}
KeyCode::Tab | KeyCode::BackTab if key.modifiers.contains(KeyModifiers::SHIFT) => {
let tab_index = context.tab_index + TAB_COUNT; // to wrap around properly
@@ -103,142 +82,10 @@ impl App {
KeyCode::Down | KeyCode::Char('j') => {
context.row_index = context.row_index.saturating_add(1);
}
KeyCode::Char('d') => {
self.mode = Mode::Destroy;
}
_ => {}
};
Ok(())
}
fn should_quit(&self) -> bool {
self.mode == Mode::Quit
}
}
/// delay the start of the animation so it doesn't start immediately
const DELAY: usize = 240;
/// higher means more pixels per frame are modified in the animation
const DRIP_SPEED: usize = 50;
/// delay the start of the text animation so it doesn't start immediately after the initial delay
const TEXT_DELAY: usize = 240;
/// Destroy mode activated by pressing `d`
fn destroy(frame: &mut Frame<'_>) {
let frame_count = frame.count().saturating_sub(DELAY);
if frame_count == 0 {
return;
}
let area = frame.size();
let buf = frame.buffer_mut();
drip(frame_count, area, buf);
text(frame_count, area, buf);
}
/// Move a bunch of random pixels down one row.
///
/// Each pick some random pixels and move them each down one row. This is a very inefficient way to
/// do this, but it works well enough for this demo.
fn drip(frame_count: usize, area: Rect, buf: &mut Buffer) {
// a seeded rng as we have to move the same random pixels each frame
let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(10);
let ramp_frames = 450;
let fractional_speed = frame_count as f64 / ramp_frames as f64;
let variable_speed = DRIP_SPEED as f64 * fractional_speed * fractional_speed * fractional_speed;
let pixel_count = (frame_count as f64 * variable_speed).floor() as usize;
for _ in 0..pixel_count {
let src_x = rng.gen_range(0..area.width);
let src_y = rng.gen_range(1..area.height - 2);
let src = buf.get_mut(src_x, src_y).clone();
// 1% of the time, move a blank or pixel (10:1) to the top line of the screen
if rng.gen_ratio(1, 100) {
let dest_x = rng
.gen_range(src_x.saturating_sub(5)..src_x.saturating_add(5))
.clamp(area.left(), area.right() - 1);
let dest_y = area.top() + 1;
let dest = buf.get_mut(dest_x, dest_y);
// copy the cell to the new location about 1/10 of the time blank out the cell the rest
// of the time. This has the effect of gradually removing the pixels from the screen.
if rng.gen_ratio(1, 10) {
*dest = src;
} else {
*dest = Cell::default();
}
} else {
// move the pixel down one row
let dest_x = src_x;
let dest_y = src_y.saturating_add(1).min(area.bottom() - 2);
// copy the cell to the new location
let dest = buf.get_mut(dest_x, dest_y);
*dest = src;
}
}
}
/// draw some text fading in and out from black to red and back
fn text(frame_count: usize, area: Rect, buf: &mut Buffer) {
let sub_frame = frame_count.saturating_sub(TEXT_DELAY);
if sub_frame == 0 {
return;
}
let line = "RATATUI";
let big_text = BigTextBuilder::default()
.lines([line.into()])
.pixel_size(PixelSize::Full)
.style(Style::new().fg(Color::Rgb(255, 0, 0)))
.build()
.unwrap();
// the font size is 8x8 for each character and we have 1 line
let area = centered_rect(area, line.width() as u16 * 8, 8);
let mask_buf = &mut Buffer::empty(area);
big_text.render(area, mask_buf);
let percentage = (sub_frame as f64 / 480.0).clamp(0.0, 1.0);
for row in area.rows() {
for col in row.columns() {
let cell = buf.get_mut(col.x, col.y);
let mask_cell = mask_buf.get(col.x, col.y);
cell.set_symbol(mask_cell.symbol());
// blend the mask cell color with the cell color
let cell_color = cell.style().bg.unwrap_or(Color::Rgb(0, 0, 0));
let mask_color = mask_cell.style().fg.unwrap_or(Color::Rgb(255, 0, 0));
let color = blend(mask_color, cell_color, percentage);
cell.set_style(Style::new().fg(color));
}
}
}
fn blend(mask_color: Color, cell_color: Color, percentage: f64) -> Color {
let Color::Rgb(mask_red, mask_green, mask_blue) = mask_color else {
return mask_color;
};
let Color::Rgb(cell_red, cell_green, cell_blue) = cell_color else {
return mask_color;
};
let red = mask_red as f64 * percentage + cell_red as f64 * (1.0 - percentage);
let green = mask_green as f64 * percentage + cell_green as f64 * (1.0 - percentage);
let blue = mask_blue as f64 * percentage + cell_blue as f64 * (1.0 - percentage);
Color::Rgb(red as u8, green as u8, blue as u8)
}
/// a centered rect of the given size
fn centered_rect(area: Rect, width: u16, height: u16) -> Rect {
let horizontal = Layout::horizontal([width]).flex(Flex::Center);
let vertical = Layout::vertical([height]).flex(Flex::Center);
let [area] = area.split(&vertical);
let [area] = area.split(&horizontal);
area
}
pub fn install_panic_hook() {

View File

@@ -1,815 +0,0 @@
//! [tui-big-text] is a rust crate that renders large pixel text as a [Ratatui] widget using the
//! glyphs from the [font8x8] crate.
//!
//! ![Hello World example](https://vhs.charm.sh/vhs-2UxNc2SJgiNqHoowbsXAMW.gif)
//!
//! # Installation
//!
//! ```shell
//! cargo add ratatui tui-big-text
//! ```
//!
//! # Usage
//!
//! Create a [`BigText`] widget using `BigTextBuilder` and pass it to [`Frame::render_widget`] to
//! render be rendered. The builder allows you to customize the [`Style`] of the widget and the
//! [`PixelSize`] of the glyphs. The [`PixelSize`] can be used to control how many character cells
//! are used to represent a single pixel of the 8x8 font.
//!
//! # Example
//!
//! ```rust
//! use anyhow::Result;
//! use ratatui::prelude::*;
//! use tui_big_text::{BigTextBuilder, PixelSize};
//!
//! fn render(frame: &mut Frame) -> Result<()> {
//! let big_text = BigTextBuilder::default()
//! .pixel_size(PixelSize::Full)
//! .style(Style::new().blue())
//! .lines(vec![
//! "Hello".red().into(),
//! "World".white().into(),
//! "~~~~~".into(),
//! ])
//! .build()?;
//! frame.render_widget(big_text, frame.size());
//! Ok(())
//! }
//! ```
//!
//! [tui-big-text]: https://crates.io/crates/tui-big-text
//! [Ratatui]: https://crates.io/crates/ratatui
//! [font8x8]: https://crates.io/crates/font8x8
//! [`BigText`]: crate::BigText
//! [`PixelSize`]: crate::PixelSize
//! [`Frame::render_widget`]: ratatui::Frame::render_widget
//! [`Style`]: ratatui::style::Style
use std::cmp::min;
use derive_builder::Builder;
use font8x8::UnicodeFonts;
use ratatui::{prelude::*, text::StyledGrapheme, widgets::Widget};
#[allow(unused)]
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Default)]
pub enum PixelSize {
#[default]
/// A pixel from the 8x8 font is represented by a full character cell in the terminal.
Full,
/// A pixel from the 8x8 font is represented by a half (upper/lower) character cell in the
/// terminal.
HalfHeight,
/// A pixel from the 8x8 font is represented by a half (left/right) character cell in the
/// terminal.
HalfWidth,
/// A pixel from the 8x8 font is represented by a quadrant of a character cell in the terminal.
Quadrant,
}
/// Displays one or more lines of text using 8x8 pixel characters.
///
/// The text is rendered using the [font8x8](https://crates.io/crates/font8x8) crate.
///
/// Using the `pixel_size` method, you can also chose, how 'big' a pixel should be.
/// Currently a pixel of the 8x8 font can be represented by one full or half
/// (horizontal/vertical/both) character cell of the terminal.
///
/// # Examples
///
/// ```rust
/// use ratatui::prelude::*;
/// use tui_big_text::{BigTextBuilder, PixelSize};
///
/// BigText::builder()
/// .pixel_size(PixelSize::Full)
/// .style(Style::new().white())
/// .lines(vec![
/// "Hello".red().into(),
/// "World".blue().into(),
/// "=====".into(),
/// ])
/// .build();
/// ```
///
/// Renders:
///
/// ```plain
/// ██ ██ ███ ███
/// ██ ██ ██ ██
/// ██ ██ ████ ██ ██ ████
/// ██████ ██ ██ ██ ██ ██ ██
/// ██ ██ ██████ ██ ██ ██ ██
/// ██ ██ ██ ██ ██ ██ ██
/// ██ ██ ████ ████ ████ ████
///
/// ██ ██ ███ ███
/// ██ ██ ██ ██
/// ██ ██ ████ ██ ███ ██ ██
/// ██ █ ██ ██ ██ ███ ██ ██ █████
/// ███████ ██ ██ ██ ██ ██ ██ ██
/// ███ ███ ██ ██ ██ ██ ██ ██
/// ██ ██ ████ ████ ████ ███ ██
///
/// ███ ██ ███ ██ ███ ██ ███ ██ ███ ██
/// ██ ███ ██ ███ ██ ███ ██ ███ ██ ███
/// ```
#[derive(Debug, Builder, Clone, PartialEq, Eq, Hash)]
pub struct BigText<'a> {
/// The text to display
#[builder(setter(into))]
lines: Vec<Line<'a>>,
/// The style of the widget
///
/// Defaults to `Style::default()`
#[builder(default)]
style: Style,
/// The size of single glyphs
///
/// Defaults to `BigTextSize::default()` (=> BigTextSize::Full)
#[builder(default)]
pixel_size: PixelSize,
}
impl Widget for BigText<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
let layout = layout(area, &self.pixel_size);
for (line, line_layout) in self.lines.iter().zip(layout) {
for (g, cell) in line.styled_graphemes(self.style).zip(line_layout) {
render_symbol(g, cell, buf, &self.pixel_size);
}
}
}
}
/// Returns how many cells are needed to display a full 8x8 glyphe using the given font size
fn cells_per_glyph(size: &PixelSize) -> (u16, u16) {
match size {
PixelSize::Full => (8, 8),
PixelSize::HalfHeight => (8, 4),
PixelSize::HalfWidth => (4, 8),
PixelSize::Quadrant => (4, 4),
}
}
/// Chunk the area into as many x*y cells as possible returned as a 2D iterator of `Rect`s
/// representing the rows of cells.
/// The size of each cell depends on given font size
fn layout(
area: Rect,
pixel_size: &PixelSize,
) -> impl IntoIterator<Item = impl IntoIterator<Item = Rect>> {
let (width, height) = cells_per_glyph(pixel_size);
(area.top()..area.bottom())
.step_by(height as usize)
.map(move |y| {
(area.left()..area.right())
.step_by(width as usize)
.map(move |x| {
let width = min(area.right() - x, width);
let height = min(area.bottom() - y, height);
Rect::new(x, y, width, height)
})
})
}
/// Render a single grapheme into a cell by looking up the corresponding 8x8 bitmap in the
/// `BITMAPS` array and setting the corresponding cells in the buffer.
fn render_symbol(grapheme: StyledGrapheme, area: Rect, buf: &mut Buffer, pixel_size: &PixelSize) {
buf.set_style(area, grapheme.style);
let c = grapheme.symbol.chars().next().unwrap(); // TODO: handle multi-char graphemes
if let Some(glyph) = font8x8::BASIC_FONTS.get(c) {
render_glyph(glyph, area, buf, pixel_size);
}
}
/// Get the correct unicode symbol for two vertical "pixels"
fn get_symbol_half_height(top: u8, bottom: u8) -> char {
match top {
0 => match bottom {
0 => ' ',
_ => '▄',
},
_ => match bottom {
0 => '▀',
_ => '█',
},
}
}
/// Get the correct unicode symbol for two horizontal "pixels"
fn get_symbol_half_width(left: u8, right: u8) -> char {
match left {
0 => match right {
0 => ' ',
_ => '▐',
},
_ => match right {
0 => '▌',
_ => '█',
},
}
}
/// Get the correct unicode symbol for 2x2 "pixels"
fn get_symbol_half_size(top_left: u8, top_right: u8, bottom_left: u8, bottom_right: u8) -> char {
let top_left = if top_left > 0 { 1 } else { 0 };
let top_right = if top_right > 0 { 1 } else { 0 };
let bottom_left = if bottom_left > 0 { 1 } else { 0 };
let bottom_right = if bottom_right > 0 { 1 } else { 0 };
const QUADRANT_SYMBOLS: [char; 16] = [
' ', '▘', '▝', '▀', '▖', '▌', '▞', '▛', '▗', '▚', '▐', '▜', '▄', '▙', '▟', '█',
];
QUADRANT_SYMBOLS[top_left + (top_right << 1) + (bottom_left << 2) + (bottom_right << 3)]
}
/// Render a single 8x8 glyph into a cell by setting the corresponding cells in the buffer.
fn render_glyph(glyph: [u8; 8], area: Rect, buf: &mut Buffer, pixel_size: &PixelSize) {
let (width, height) = cells_per_glyph(pixel_size);
let glyph_vertical_index = (0..glyph.len()).step_by(8 / height as usize);
let glyph_horizontal_bit_selector = (0..8).step_by(8 / width as usize);
for (row, y) in glyph_vertical_index.zip(area.top()..area.bottom()) {
for (col, x) in glyph_horizontal_bit_selector
.clone()
.zip(area.left()..area.right())
{
let cell = buf.get_mut(x, y);
let symbol_character = match pixel_size {
PixelSize::Full => match glyph[row] & (1 << col) {
0 => ' ',
_ => '█',
},
PixelSize::HalfHeight => {
let top = glyph[row] & (1 << col);
let bottom = glyph[row + 1] & (1 << col);
get_symbol_half_height(top, bottom)
}
PixelSize::HalfWidth => {
let left = glyph[row] & (1 << col);
let right = glyph[row] & (1 << (col + 1));
get_symbol_half_width(left, right)
}
PixelSize::Quadrant => {
let top_left = glyph[row] & (1 << col);
let top_right = glyph[row] & (1 << (col + 1));
let bottom_left = glyph[row + 1] & (1 << col);
let bottom_right = glyph[row + 1] & (1 << (col + 1));
get_symbol_half_size(top_left, top_right, bottom_left, bottom_right)
}
};
cell.set_char(symbol_character);
}
}
}
#[cfg(test)]
mod tests {
use ratatui::assert_buffer_eq;
use super::*;
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
#[test]
fn build() -> Result<()> {
let lines = vec![Line::from(vec!["Hello".red(), "World".blue()])];
let style = Style::new().green();
let pixel_size = PixelSize::default();
assert_eq!(
BigTextBuilder::default()
.lines(lines.clone())
.style(style)
.build()?,
BigText {
lines,
style,
pixel_size
}
);
Ok(())
}
#[test]
fn render_single_line() -> Result<()> {
let big_text = BigTextBuilder::default()
.lines(vec![Line::from("SingleLine")])
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 80, 8));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines(vec![
" ████ ██ ███ ████ ██ ",
"██ ██ ██ ██ ",
"███ ███ █████ ███ ██ ██ ████ ██ ███ █████ ████ ",
" ███ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ",
" ███ ██ ██ ██ ██ ██ ██ ██████ ██ █ ██ ██ ██ ██████ ",
"██ ██ ██ ██ ██ █████ ██ ██ ██ ██ ██ ██ ██ ██ ",
" ████ ████ ██ ██ ██ ████ ████ ███████ ████ ██ ██ ████ ",
" █████ ",
]);
assert_buffer_eq!(buf, expected);
Ok(())
}
#[test]
fn render_truncated() -> Result<()> {
let big_text = BigTextBuilder::default()
.lines(vec![Line::from("Truncated")])
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 70, 6));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines(vec![
"██████ █ ███",
"█ ██ █ ██ ██",
" ██ ██ ███ ██ ██ █████ ████ ████ █████ ████ ██",
" ██ ███ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ █████",
" ██ ██ ██ ██ ██ ██ ██ ██ █████ ██ ██████ ██ ██",
" ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ █ ██ ██ ██",
]);
assert_buffer_eq!(buf, expected);
Ok(())
}
#[test]
fn render_multiple_lines() -> Result<()> {
let big_text = BigTextBuilder::default()
.lines(vec![Line::from("Multi"), Line::from("Lines")])
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 40, 16));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines(vec![
"██ ██ ███ █ ██ ",
"███ ███ ██ ██ ",
"███████ ██ ██ ██ █████ ███ ",
"███████ ██ ██ ██ ██ ██ ",
"██ █ ██ ██ ██ ██ ██ ██ ",
"██ ██ ██ ██ ██ ██ █ ██ ",
"██ ██ ███ ██ ████ ██ ████ ",
" ",
"████ ██ ",
" ██ ",
" ██ ███ █████ ████ █████ ",
" ██ ██ ██ ██ ██ ██ ██ ",
" ██ █ ██ ██ ██ ██████ ████ ",
" ██ ██ ██ ██ ██ ██ ██ ",
"███████ ████ ██ ██ ████ █████ ",
" ",
]);
assert_buffer_eq!(buf, expected);
Ok(())
}
#[test]
fn render_widget_style() -> Result<()> {
let big_text = BigTextBuilder::default()
.lines(vec![Line::from("Styled")])
.style(Style::new().bold())
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 48, 8));
big_text.render(buf.area, &mut buf);
let mut expected = Buffer::with_lines(vec![
" ████ █ ███ ███ ",
"██ ██ ██ ██ ██ ",
"███ █████ ██ ██ ██ ████ ██ ",
" ███ ██ ██ ██ ██ ██ ██ █████ ",
" ███ ██ ██ ██ ██ ██████ ██ ██ ",
"██ ██ ██ █ █████ ██ ██ ██ ██ ",
" ████ ██ ██ ████ ████ ███ ██ ",
" █████ ",
]);
expected.set_style(Rect::new(0, 0, 48, 8), Style::new().bold());
assert_buffer_eq!(buf, expected);
Ok(())
}
#[test]
fn render_line_style() -> Result<()> {
let big_text = BigTextBuilder::default()
.lines(vec![
Line::from("Red".red()),
Line::from("Green".green()),
Line::from("Blue".blue()),
])
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 40, 24));
big_text.render(buf.area, &mut buf);
let mut expected = Buffer::with_lines(vec![
"██████ ███ ",
" ██ ██ ██ ",
" ██ ██ ████ ██ ",
" █████ ██ ██ █████ ",
" ██ ██ ██████ ██ ██ ",
" ██ ██ ██ ██ ██ ",
"███ ██ ████ ███ ██ ",
" ",
" ████ ",
" ██ ██ ",
"██ ██ ███ ████ ████ █████ ",
"██ ███ ██ ██ ██ ██ ██ ██ ██ ",
"██ ███ ██ ██ ██████ ██████ ██ ██ ",
" ██ ██ ██ ██ ██ ██ ██ ",
" █████ ████ ████ ████ ██ ██ ",
" ",
"██████ ███ ",
" ██ ██ ██ ",
" ██ ██ ██ ██ ██ ████ ",
" █████ ██ ██ ██ ██ ██ ",
" ██ ██ ██ ██ ██ ██████ ",
" ██ ██ ██ ██ ██ ██ ",
"██████ ████ ███ ██ ████ ",
" ",
]);
expected.set_style(Rect::new(0, 0, 24, 8), Style::new().red());
expected.set_style(Rect::new(0, 8, 40, 8), Style::new().green());
expected.set_style(Rect::new(0, 16, 32, 8), Style::new().blue());
assert_buffer_eq!(buf, expected);
Ok(())
}
#[test]
fn render_half_height_single_line() -> Result<()> {
let big_text = BigTextBuilder::default()
.pixel_size(PixelSize::HalfHeight)
.lines(vec![Line::from("SingleLine")])
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 80, 4));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines(vec![
"▄█▀▀█▄ ▀▀ ▀██ ▀██▀ ▀▀ ",
"▀██▄ ▀██ ██▀▀█▄ ▄█▀▀▄█▀ ██ ▄█▀▀█▄ ██ ▀██ ██▀▀█▄ ▄█▀▀█▄ ",
"▄▄ ▀██ ██ ██ ██ ▀█▄▄██ ██ ██▀▀▀▀ ██ ▄█ ██ ██ ██ ██▀▀▀▀ ",
" ▀▀▀▀ ▀▀▀▀ ▀▀ ▀▀ ▄▄▄▄█▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀▀▀▀ ▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀ ",
]);
assert_buffer_eq!(buf, expected);
Ok(())
}
#[test]
fn render_half_height_truncated() -> Result<()> {
let big_text = BigTextBuilder::default()
.pixel_size(PixelSize::HalfHeight)
.lines(vec![Line::from("Truncated")])
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 70, 3));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines(vec![
"█▀██▀█ ▄█ ▀██",
" ██ ▀█▄█▀█▄ ██ ██ ██▀▀█▄ ▄█▀▀█▄ ▀▀▀█▄ ▀██▀▀ ▄█▀▀█▄ ▄▄▄██",
" ██ ██ ▀▀ ██ ██ ██ ██ ██ ▄▄ ▄█▀▀██ ██ ▄ ██▀▀▀▀ ██ ██",
]);
assert_buffer_eq!(buf, expected);
Ok(())
}
#[test]
fn render_half_height_multiple_lines() -> Result<()> {
let big_text = BigTextBuilder::default()
.pixel_size(PixelSize::HalfHeight)
.lines(vec![Line::from("Multi"), Line::from("Lines")])
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 40, 8));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines(vec![
"██▄ ▄██ ▀██ ▄█ ▀▀ ",
"███████ ██ ██ ██ ▀██▀▀ ▀██ ",
"██ ▀ ██ ██ ██ ██ ██ ▄ ██ ",
"▀▀ ▀▀ ▀▀▀ ▀▀ ▀▀▀▀ ▀▀ ▀▀▀▀ ",
"▀██▀ ▀▀ ",
" ██ ▀██ ██▀▀█▄ ▄█▀▀█▄ ▄█▀▀▀▀ ",
" ██ ▄█ ██ ██ ██ ██▀▀▀▀ ▀▀▀█▄ ",
"▀▀▀▀▀▀▀ ▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀ ▀▀▀▀▀ ",
]);
assert_buffer_eq!(buf, expected);
Ok(())
}
#[test]
fn render_half_height_widget_style() -> Result<()> {
let big_text = BigTextBuilder::default()
.pixel_size(PixelSize::HalfHeight)
.lines(vec![Line::from("Styled")])
.style(Style::new().bold())
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 48, 4));
big_text.render(buf.area, &mut buf);
let mut expected = Buffer::with_lines(vec![
"▄█▀▀█▄ ▄█ ▀██ ▀██ ",
"▀██▄ ▀██▀▀ ██ ██ ██ ▄█▀▀█▄ ▄▄▄██ ",
"▄▄ ▀██ ██ ▄ ▀█▄▄██ ██ ██▀▀▀▀ ██ ██ ",
" ▀▀▀▀ ▀▀ ▄▄▄▄█▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀ ▀▀ ",
]);
expected.set_style(Rect::new(0, 0, 48, 4), Style::new().bold());
assert_buffer_eq!(buf, expected);
Ok(())
}
#[test]
fn render_half_height_line_style() -> Result<()> {
let big_text = BigTextBuilder::default()
.pixel_size(PixelSize::HalfHeight)
.lines(vec![
Line::from("Red".red()),
Line::from("Green".green()),
Line::from("Blue".blue()),
])
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 40, 12));
big_text.render(buf.area, &mut buf);
let mut expected = Buffer::with_lines(vec![
"▀██▀▀█▄ ▀██ ",
" ██▄▄█▀ ▄█▀▀█▄ ▄▄▄██ ",
" ██ ▀█▄ ██▀▀▀▀ ██ ██ ",
"▀▀▀ ▀▀ ▀▀▀▀ ▀▀▀ ▀▀ ",
" ▄█▀▀█▄ ",
"██ ▀█▄█▀█▄ ▄█▀▀█▄ ▄█▀▀█▄ ██▀▀█▄ ",
"▀█▄ ▀██ ██ ▀▀ ██▀▀▀▀ ██▀▀▀▀ ██ ██ ",
" ▀▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀ ▀▀ ",
"▀██▀▀█▄ ▀██ ",
" ██▄▄█▀ ██ ██ ██ ▄█▀▀█▄ ",
" ██ ██ ██ ██ ██ ██▀▀▀▀ ",
"▀▀▀▀▀▀ ▀▀▀▀ ▀▀▀ ▀▀ ▀▀▀▀ ",
]);
expected.set_style(Rect::new(0, 0, 24, 4), Style::new().red());
expected.set_style(Rect::new(0, 4, 40, 4), Style::new().green());
expected.set_style(Rect::new(0, 8, 32, 4), Style::new().blue());
assert_buffer_eq!(buf, expected);
Ok(())
}
#[test]
fn render_half_width_single_line() -> Result<()> {
let big_text = BigTextBuilder::default()
.pixel_size(PixelSize::HalfWidth)
.lines(vec![Line::from("SingleLine")])
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 40, 8));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines(vec![
"▐█▌ █ ▐█ ██ █ ",
"█ █ █ ▐▌ ",
"█▌ ▐█ ██▌ ▐█▐▌ █ ▐█▌ ▐▌ ▐█ ██▌ ▐█▌ ",
"▐█ █ █ █ █ █ █ █ █ ▐▌ █ █ █ █ █ ",
" ▐█ █ █ █ █ █ █ ███ ▐▌ ▌ █ █ █ ███ ",
"█ █ █ █ █ ▐██ █ █ ▐▌▐▌ █ █ █ █ ",
"▐█▌ ▐█▌ █ █ █ ▐█▌ ▐█▌ ███▌▐█▌ █ █ ▐█▌ ",
" ██▌ ",
]);
assert_buffer_eq!(buf, expected);
Ok(())
}
#[test]
fn render_half_width_truncated() -> Result<()> {
let big_text = BigTextBuilder::default()
.pixel_size(PixelSize::HalfWidth)
.lines(vec![Line::from("Truncated")])
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 35, 6));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines(vec![
"███ ▐ ▐█",
"▌█▐ █ █",
" █ █▐█ █ █ ██▌ ▐█▌ ▐█▌ ▐██ ▐█▌ █",
" █ ▐█▐▌█ █ █ █ █ █ █ █ █ █ ▐██",
" █ ▐▌▐▌█ █ █ █ █ ▐██ █ ███ █ █",
" █ ▐▌ █ █ █ █ █ █ █ █ █▐ █ █ █",
]);
assert_buffer_eq!(buf, expected);
Ok(())
}
#[test]
fn render_half_width_multiple_lines() -> Result<()> {
let big_text = BigTextBuilder::default()
.pixel_size(PixelSize::HalfWidth)
.lines(vec![Line::from("Multi"), Line::from("Lines")])
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 16));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines(vec![
"█ ▐▌ ▐█ ▐ █ ",
"█▌█▌ █ █ ",
"███▌█ █ █ ▐██ ▐█ ",
"███▌█ █ █ █ █ ",
"█▐▐▌█ █ █ █ █ ",
"█ ▐▌█ █ █ █▐ █ ",
"█ ▐▌▐█▐▌▐█▌ ▐▌ ▐█▌ ",
" ",
"██ █ ",
"▐▌ ",
"▐▌ ▐█ ██▌ ▐█▌ ▐██ ",
"▐▌ █ █ █ █ █ █ ",
"▐▌ ▌ █ █ █ ███ ▐█▌ ",
"▐▌▐▌ █ █ █ █ █ ",
"███▌▐█▌ █ █ ▐█▌ ██▌ ",
" ",
]);
assert_buffer_eq!(buf, expected);
Ok(())
}
#[test]
fn render_half_width_widget_style() -> Result<()> {
let big_text = BigTextBuilder::default()
.pixel_size(PixelSize::HalfWidth)
.lines(vec![Line::from("Styled")])
.style(Style::new().bold())
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 24, 8));
big_text.render(buf.area, &mut buf);
let mut expected = Buffer::with_lines(vec![
"▐█▌ ▐ ▐█ ▐█ ",
"█ █ █ █ █ ",
"█▌ ▐██ █ █ █ ▐█▌ █ ",
"▐█ █ █ █ █ █ █ ▐██ ",
" ▐█ █ █ █ █ ███ █ █ ",
"█ █ █▐ ▐██ █ █ █ █ ",
"▐█▌ ▐▌ █ ▐█▌ ▐█▌ ▐█▐▌",
" ██▌ ",
]);
expected.set_style(Rect::new(0, 0, 24, 8), Style::new().bold());
assert_buffer_eq!(buf, expected);
Ok(())
}
#[test]
fn render_half_width_line_style() -> Result<()> {
let big_text = BigTextBuilder::default()
.pixel_size(PixelSize::HalfWidth)
.lines(vec![
Line::from("Red".red()),
Line::from("Green".green()),
Line::from("Blue".blue()),
])
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 24));
big_text.render(buf.area, &mut buf);
let mut expected = Buffer::with_lines(vec![
"███ ▐█ ",
"▐▌▐▌ █ ",
"▐▌▐▌▐█▌ █ ",
"▐██ █ █ ▐██ ",
"▐▌█ ███ █ █ ",
"▐▌▐▌█ █ █ ",
"█▌▐▌▐█▌ ▐█▐▌ ",
" ",
" ██ ",
"▐▌▐▌ ",
"█ █▐█ ▐█▌ ▐█▌ ██▌ ",
"█ ▐█▐▌█ █ █ █ █ █ ",
"█ █▌▐▌▐▌███ ███ █ █ ",
"▐▌▐▌▐▌ █ █ █ █ ",
" ██▌██ ▐█▌ ▐█▌ █ █ ",
" ",
"███ ▐█ ",
"▐▌▐▌ █ ",
"▐▌▐▌ █ █ █ ▐█▌ ",
"▐██ █ █ █ █ █ ",
"▐▌▐▌ █ █ █ ███ ",
"▐▌▐▌ █ █ █ █ ",
"███ ▐█▌ ▐█▐▌▐█▌ ",
" ",
]);
expected.set_style(Rect::new(0, 0, 12, 8), Style::new().red());
expected.set_style(Rect::new(0, 8, 20, 8), Style::new().green());
expected.set_style(Rect::new(0, 16, 16, 8), Style::new().blue());
assert_buffer_eq!(buf, expected);
Ok(())
}
#[test]
fn check_half_size_symbols() -> Result<()> {
assert_eq!(get_symbol_half_size(0, 0, 0, 0), ' ');
assert_eq!(get_symbol_half_size(1, 0, 0, 0), '▘');
assert_eq!(get_symbol_half_size(0, 1, 0, 0), '▝');
assert_eq!(get_symbol_half_size(1, 1, 0, 0), '▀');
assert_eq!(get_symbol_half_size(0, 0, 1, 0), '▖');
assert_eq!(get_symbol_half_size(1, 0, 1, 0), '▌');
assert_eq!(get_symbol_half_size(0, 1, 1, 0), '▞');
assert_eq!(get_symbol_half_size(1, 1, 1, 0), '▛');
assert_eq!(get_symbol_half_size(0, 0, 0, 1), '▗');
assert_eq!(get_symbol_half_size(1, 0, 0, 1), '▚');
assert_eq!(get_symbol_half_size(0, 1, 0, 1), '▐');
assert_eq!(get_symbol_half_size(1, 1, 0, 1), '▜');
assert_eq!(get_symbol_half_size(0, 0, 1, 1), '▄');
assert_eq!(get_symbol_half_size(1, 0, 1, 1), '▙');
assert_eq!(get_symbol_half_size(0, 1, 1, 1), '▟');
assert_eq!(get_symbol_half_size(1, 1, 1, 1), '█');
Ok(())
}
#[test]
fn render_half_size_single_line() -> Result<()> {
let big_text = BigTextBuilder::default()
.pixel_size(PixelSize::Quadrant)
.lines(vec![Line::from("SingleLine")])
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 40, 4));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines(vec![
"▟▀▙ ▀ ▝█ ▜▛ ▀ ",
"▜▙ ▝█ █▀▙ ▟▀▟▘ █ ▟▀▙ ▐▌ ▝█ █▀▙ ▟▀▙ ",
"▄▝█ █ █ █ ▜▄█ █ █▀▀ ▐▌▗▌ █ █ █ █▀▀ ",
"▝▀▘ ▝▀▘ ▀ ▀ ▄▄▛ ▝▀▘ ▝▀▘ ▀▀▀▘▝▀▘ ▀ ▀ ▝▀▘ ",
]);
assert_buffer_eq!(buf, expected);
Ok(())
}
#[test]
fn render_half_size_truncated() -> Result<()> {
let big_text = BigTextBuilder::default()
.pixel_size(PixelSize::Quadrant)
.lines(vec![Line::from("Truncated")])
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 35, 3));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines(vec![
"▛█▜ ▟ ▝█",
" █ ▜▟▜▖█ █ █▀▙ ▟▀▙ ▝▀▙ ▝█▀ ▟▀▙ ▗▄█",
" █ ▐▌▝▘█ █ █ █ █ ▄ ▟▀█ █▗ █▀▀ █ █",
]);
assert_buffer_eq!(buf, expected);
Ok(())
}
#[test]
fn render_half_size_multiple_lines() -> Result<()> {
let big_text = BigTextBuilder::default()
.pixel_size(PixelSize::Quadrant)
.lines(vec![Line::from("Multi"), Line::from("Lines")])
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 8));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines(vec![
"█▖▟▌ ▝█ ▟ ▀ ",
"███▌█ █ █ ▝█▀ ▝█ ",
"█▝▐▌█ █ █ █▗ █ ",
"▀ ▝▘▝▀▝▘▝▀▘ ▝▘ ▝▀▘ ",
"▜▛ ▀ ",
"▐▌ ▝█ █▀▙ ▟▀▙ ▟▀▀ ",
"▐▌▗▌ █ █ █ █▀▀ ▝▀▙ ",
"▀▀▀▘▝▀▘ ▀ ▀ ▝▀▘ ▀▀▘ ",
]);
assert_buffer_eq!(buf, expected);
Ok(())
}
#[test]
fn render_half_size_widget_style() -> Result<()> {
let big_text = BigTextBuilder::default()
.pixel_size(PixelSize::Quadrant)
.lines(vec![Line::from("Styled")])
.style(Style::new().bold())
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 24, 4));
big_text.render(buf.area, &mut buf);
let mut expected = Buffer::with_lines(vec![
"▟▀▙ ▟ ▝█ ▝█ ",
"▜▙ ▝█▀ █ █ █ ▟▀▙ ▗▄█ ",
"▄▝█ █▗ ▜▄█ █ █▀▀ █ █ ",
"▝▀▘ ▝▘ ▄▄▛ ▝▀▘ ▝▀▘ ▝▀▝▘",
]);
expected.set_style(Rect::new(0, 0, 24, 4), Style::new().bold());
assert_buffer_eq!(buf, expected);
Ok(())
}
#[test]
fn render_half_size_line_style() -> Result<()> {
let big_text = BigTextBuilder::default()
.pixel_size(PixelSize::Quadrant)
.lines(vec![
Line::from("Red".red()),
Line::from("Green".green()),
Line::from("Blue".blue()),
])
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 12));
big_text.render(buf.area, &mut buf);
let mut expected = Buffer::with_lines(vec![
"▜▛▜▖ ▝█ ",
"▐▙▟▘▟▀▙ ▗▄█ ",
"▐▌▜▖█▀▀ █ █ ",
"▀▘▝▘▝▀▘ ▝▀▝▘ ",
"▗▛▜▖ ",
"█ ▜▟▜▖▟▀▙ ▟▀▙ █▀▙ ",
"▜▖▜▌▐▌▝▘█▀▀ █▀▀ █ █ ",
" ▀▀▘▀▀ ▝▀▘ ▝▀▘ ▀ ▀ ",
"▜▛▜▖▝█ ",
"▐▙▟▘ █ █ █ ▟▀▙ ",
"▐▌▐▌ █ █ █ █▀▀ ",
"▀▀▀ ▝▀▘ ▝▀▝▘▝▀▘ ",
]);
expected.set_style(Rect::new(0, 0, 12, 4), Style::new().red());
expected.set_style(Rect::new(0, 4, 20, 4), Style::new().green());
expected.set_style(Rect::new(0, 8, 16, 4), Style::new().blue());
assert_buffer_eq!(buf, expected);
Ok(())
}
}

View File

@@ -6,7 +6,6 @@ pub use term::*;
pub use theme::*;
mod app;
mod big_text;
mod colors;
mod root;
mod tabs;

View File

@@ -1,3 +1,5 @@
use std::rc::Rc;
use itertools::Itertools;
use ratatui::{prelude::*, widgets::*};
@@ -16,31 +18,25 @@ impl<'a> Root<'a> {
impl Widget for Root<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
Block::new().style(THEME.root).render(area, buf);
let vertical = Layout::vertical([
Constraint::Length(1),
Constraint::Min(0),
Constraint::Length(1),
]);
let [title_bar, tab, bottom_bar] = area.split(&vertical);
self.render_title_bar(title_bar, buf);
self.render_selected_tab(tab, buf);
self.render_bottom_bar(bottom_bar, buf);
let area = layout(area, Direction::Vertical, vec![1, 0, 1]);
self.render_title_bar(area[0], buf);
self.render_selected_tab(area[1], buf);
self.render_bottom_bar(area[2], buf);
}
}
impl Root<'_> {
fn render_title_bar(&self, area: Rect, buf: &mut Buffer) {
let horizontal = Layout::horizontal([Constraint::Min(0), Constraint::Length(45)]);
let [title, tabs] = area.split(&horizontal);
let area = layout(area, Direction::Horizontal, vec![0, 45]);
Paragraph::new(Span::styled("Ratatui", THEME.app_title)).render(title, buf);
Paragraph::new(Span::styled("Ratatui", THEME.app_title)).render(area[0], buf);
let titles = vec!["", " Recipe ", " Email ", " Traceroute ", " Weather "];
Tabs::new(titles)
.style(THEME.tabs)
.highlight_style(THEME.tabs_selected)
.select(self.context.tab_index)
.divider("")
.render(tabs, buf);
.render(area[1], buf);
}
fn render_selected_tab(&self, area: Rect, buf: &mut Buffer) {
@@ -77,3 +73,21 @@ impl Root<'_> {
.render(area, buf);
}
}
/// simple helper method to split an area into multiple sub-areas
pub fn layout(area: Rect, direction: Direction, heights: Vec<u16>) -> Rc<[Rect]> {
let constraints = heights
.iter()
.map(|&h| {
if h > 0 {
Constraint::Length(h)
} else {
Constraint::Min(0)
}
})
.collect_vec();
Layout::default()
.direction(direction)
.constraints(constraints)
.split(area)
}

View File

@@ -1,7 +1,7 @@
use itertools::Itertools;
use ratatui::{prelude::*, widgets::*};
use crate::{RgbSwatch, THEME};
use crate::{layout, RgbSwatch, THEME};
const RATATUI_LOGO: [&str; 32] = [
" ███ ",
@@ -51,10 +51,9 @@ impl AboutTab {
impl Widget for AboutTab {
fn render(self, area: Rect, buf: &mut Buffer) {
RgbSwatch.render(area, buf);
let horizontal = Layout::horizontal([Constraint::Length(34), Constraint::Min(0)]);
let [description, logo] = area.split(&horizontal);
render_crate_description(description, buf);
render_logo(self.selected_row, logo, buf);
let area = layout(area, Direction::Horizontal, vec![34, 0]);
render_crate_description(area[1], buf);
render_logo(self.selected_row, area[0], buf);
}
}
@@ -117,7 +116,6 @@ pub fn render_logo(selected_row: usize, area: Rect, buf: &mut Buffer) {
('█', '█') => {
cell.set_char('█');
cell.fg = rat_color;
cell.bg = rat_color;
}
('█', ' ') => {
cell.set_char('▀');

View File

@@ -2,7 +2,7 @@ use itertools::Itertools;
use ratatui::{prelude::*, widgets::*};
use unicode_width::UnicodeWidthStr;
use crate::{RgbSwatch, THEME};
use crate::{layout, RgbSwatch, THEME};
#[derive(Debug, Default)]
pub struct Email {
@@ -60,22 +60,20 @@ impl Widget for EmailTab {
horizontal: 2,
});
Clear.render(area, buf);
let vertical = Layout::vertical([Constraint::Length(5), Constraint::Min(0)]);
let [inbox, email] = area.split(&vertical);
render_inbox(self.selected_index, inbox, buf);
render_email(self.selected_index, email, buf);
let area = layout(area, Direction::Vertical, vec![5, 0]);
render_inbox(self.selected_index, area[0], buf);
render_email(self.selected_index, area[1], buf);
}
}
fn render_inbox(selected_index: usize, area: Rect, buf: &mut Buffer) {
let vertical = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
let [tabs, inbox] = area.split(&vertical);
let area = layout(area, Direction::Vertical, vec![1, 0]);
let theme = THEME.email;
Tabs::new(vec![" Inbox ", " Sent ", " Drafts "])
.style(theme.tabs)
.highlight_style(theme.tabs_selected)
.select(0)
.divider("")
.render(tabs, buf);
.render(area[0], buf);
let highlight_symbol = ">>";
let from_width = EMAILS
@@ -96,7 +94,7 @@ fn render_inbox(selected_index: usize, area: Rect, buf: &mut Buffer) {
.style(theme.inbox)
.highlight_style(theme.selected_item)
.highlight_symbol(highlight_symbol),
inbox,
area[1],
buf,
&mut state,
);
@@ -108,7 +106,7 @@ fn render_inbox(selected_index: usize, area: Rect, buf: &mut Buffer) {
.end_symbol(None)
.track_symbol(None)
.thumb_symbol("")
.render(inbox, buf, &mut scrollbar_state);
.render(area[1], buf, &mut scrollbar_state);
}
fn render_email(selected_index: usize, area: Rect, buf: &mut Buffer) {
@@ -122,8 +120,7 @@ fn render_email(selected_index: usize, area: Rect, buf: &mut Buffer) {
let inner = block.inner(area);
block.render(area, buf);
if let Some(email) = email {
let vertical = Layout::vertical([Constraint::Length(3), Constraint::Min(0)]);
let [headers_area, body_area] = inner.split(&vertical);
let area = layout(inner, Direction::Vertical, vec![3, 0]);
let headers = vec![
Line::from(vec![
"From: ".set_style(theme.header),
@@ -137,11 +134,9 @@ fn render_email(selected_index: usize, area: Rect, buf: &mut Buffer) {
];
Paragraph::new(headers)
.style(theme.body)
.render(headers_area, buf);
.render(area[0], buf);
let body = email.body.lines().map(Line::from).collect_vec();
Paragraph::new(body)
.style(theme.body)
.render(body_area, buf);
Paragraph::new(body).style(theme.body).render(area[1], buf);
} else {
Paragraph::new("No email selected").render(inner, buf);
}

View File

@@ -1,7 +1,7 @@
use itertools::Itertools;
use ratatui::{prelude::*, widgets::*};
use crate::{RgbSwatch, THEME};
use crate::{layout, RgbSwatch, THEME};
#[derive(Debug, Default, Clone, Copy)]
struct Ingredient {
@@ -123,13 +123,10 @@ impl Widget for RecipeTab {
horizontal: 2,
vertical: 1,
});
let [recipe, ingredients] = area.split(&Layout::horizontal([
Constraint::Length(44),
Constraint::Min(0),
]));
let area = layout(area, Direction::Horizontal, vec![44, 0]);
render_recipe(recipe, buf);
render_ingredients(self.selected_row, ingredients, buf);
render_recipe(area[0], buf);
render_ingredients(self.selected_row, area[1], buf);
}
}
@@ -146,7 +143,7 @@ fn render_recipe(area: Rect, buf: &mut Buffer) {
fn render_ingredients(selected_row: usize, area: Rect, buf: &mut Buffer) {
let mut state = TableState::default().with_selected(Some(selected_row));
let rows = INGREDIENTS.iter().cloned();
let rows = INGREDIENTS.iter().map(|&i| i.into()).collect_vec();
let theme = THEME.recipe;
StatefulWidget::render(
Table::new(rows, [Constraint::Length(7), Constraint::Length(30)])

View File

@@ -4,7 +4,7 @@ use ratatui::{
widgets::{canvas::*, *},
};
use crate::{RgbSwatch, THEME};
use crate::{layout, RgbSwatch, THEME};
#[derive(Debug)]
pub struct TracerouteTab {
@@ -28,14 +28,14 @@ impl Widget for TracerouteTab {
});
Clear.render(area, buf);
Block::new().style(THEME.content).render(area, buf);
let horizontal = Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]);
let vertical = Layout::vertical([Constraint::Min(0), Constraint::Length(3)]);
let [left, map] = area.split(&horizontal);
let [hops, pings] = left.split(&vertical);
render_hops(self.selected_row, hops, buf);
render_ping(self.selected_row, pings, buf);
render_map(self.selected_row, map, buf);
let area = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)])
.split(area);
let left_area = layout(area[0], Direction::Vertical, vec![0, 3]);
render_hops(self.selected_row, left_area[0], buf);
render_ping(self.selected_row, left_area[1], buf);
render_map(self.selected_row, area[1], buf);
}
}

View File

@@ -6,7 +6,7 @@ use ratatui::{
};
use time::OffsetDateTime;
use crate::{color_from_oklab, RgbSwatch, THEME};
use crate::{color_from_oklab, layout, RgbSwatch, THEME};
pub struct WeatherTab {
pub selected_row: usize,
@@ -32,24 +32,14 @@ impl Widget for WeatherTab {
horizontal: 2,
vertical: 1,
});
let [main, _, gauges] = area.split(&Layout::vertical([
Constraint::Min(0),
Constraint::Length(1),
Constraint::Length(1),
]));
let [calendar, charts] = main.split(&Layout::horizontal([
Constraint::Length(23),
Constraint::Min(0),
]));
let [simple, horizontal] = charts.split(&Layout::vertical([
Constraint::Length(29),
Constraint::Min(0),
]));
let area = layout(area, Direction::Vertical, vec![0, 1, 1]);
render_gauges(self.selected_row, area[2], buf);
render_calendar(calendar, buf);
render_simple_barchart(simple, buf);
render_horizontal_barchart(horizontal, buf);
render_gauge(self.selected_row, gauges, buf);
let area = layout(area[0], Direction::Horizontal, vec![23, 0]);
render_calendar(area[0], buf);
let area = layout(area[1], Direction::Horizontal, vec![29, 0]);
render_simple_barchart(area[0], buf);
render_horizontal_barchart(area[1], buf);
}
}
@@ -124,7 +114,7 @@ fn render_horizontal_barchart(area: Rect, buf: &mut Buffer) {
.render(area, buf);
}
pub fn render_gauge(progress: usize, area: Rect, buf: &mut Buffer) {
pub fn render_gauges(progress: usize, area: Rect, buf: &mut Buffer) {
let percent = (progress * 3).min(100) as f64;
render_line_gauge(percent, area, buf);

View File

@@ -128,9 +128,9 @@ const LIGHT_BLUE: Color = Color::Rgb(64, 96, 192);
const LIGHT_YELLOW: Color = Color::Rgb(192, 192, 96);
const LIGHT_GREEN: Color = Color::Rgb(64, 192, 96);
const LIGHT_RED: Color = Color::Rgb(192, 96, 96);
const RED: Color = Color::Rgb(215, 0, 0);
const BLACK: Color = Color::Rgb(8, 8, 8); // not really black, often #080808
const DARK_GRAY: Color = Color::Rgb(68, 68, 68);
const MID_GRAY: Color = Color::Rgb(128, 128, 128);
const LIGHT_GRAY: Color = Color::Rgb(188, 188, 188);
const WHITE: Color = Color::Rgb(238, 238, 238); // not really white, often #eeeeee
const RED: Color = Color::Indexed(160);
const BLACK: Color = Color::Indexed(232); // not really black, often #080808
const DARK_GRAY: Color = Color::Indexed(238);
const MID_GRAY: Color = Color::Indexed(244);
const LIGHT_GRAY: Color = Color::Indexed(250);
const WHITE: Color = Color::Indexed(255); // not really white, often #eeeeee

View File

@@ -53,36 +53,48 @@ fn handle_events() -> io::Result<bool> {
}
fn layout(frame: &mut Frame) {
let vertical = Layout::vertical([
Constraint::Length(1),
Constraint::Min(0),
Constraint::Length(1),
]);
let horizontal = Layout::horizontal([Constraint::Ratio(1, 2); 2]);
let [title_bar, main_area, status_bar] = frame.size().split(&vertical);
let [left, right] = main_area.split(&horizontal);
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"),
title_bar,
main_layout[0],
);
frame.render_widget(
Block::new().borders(Borders::TOP).title("Status Bar"),
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],
);
frame.render_widget(Block::default().borders(Borders::ALL).title("Left"), left);
frame.render_widget(Block::default().borders(Borders::ALL).title("Right"), right);
}
fn styling(frame: &mut Frame) {
let areas = Layout::vertical([
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
Constraint::Min(0),
])
.split(frame.size());
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(

View File

@@ -1,473 +0,0 @@
use std::io::{self, stdout};
use color_eyre::{config::HookBuilder, Result};
use crossterm::{
event::{self, Event, KeyCode, KeyEventKind},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
};
use ratatui::{
layout::{Constraint::*, Flex},
prelude::*,
style::palette::tailwind,
widgets::{block::Title, *},
};
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
const EXAMPLE_DATA: &[(&str, &[Constraint])] = &[
(
"Min(u16) takes any excess space when using `Stretch` or `StretchLast`",
&[Fixed(20), Min(20), Max(20)],
),
(
"Proportional(u16) takes any excess space in all `Flex` layouts",
&[Length(20), Percentage(20), Ratio(1, 5), Proportional(1)],
),
(
"In `StretchLast`, last constraint of lowest priority takes excess space",
&[Length(20), Fixed(20), Percentage(20)],
),
("", &[Fixed(20), Percentage(20), Length(20)]),
("", &[Percentage(20), Length(20), Fixed(20)]),
("", &[Length(20), Length(15)]),
("Spacing has no effect in `SpaceAround` and `SpaceBetween`", &[Proportional(1), Proportional(1)]),
("", &[Length(20), Fixed(20)]),
(
"When not using `Flex::Stretch` or `Flex::StretchLast`,\n`Min(u16)` and `Max(u16)` collapse to their lowest values",
&[Min(20), Max(20)],
),
(
"`SpaceBetween` stretches when there's only one constraint",
&[Max(20)],
),
("", &[Min(20), Max(20), Length(20), Fixed(20)]),
("`Proportional(u16)` always fills up space in every `Flex` layout", &[Proportional(0), Proportional(0)]),
(
"`Proportional(1)` can be to scale with respect to other `Proportional(2)`",
&[Proportional(1), Proportional(2)],
),
(
"`Proportional(0)` collapses if there are other non-zero `Proportional(_)`\nconstraints. e.g. `[Proportional(0), Proportional(0), Proportional(1)]`:",
&[
Proportional(0),
Proportional(0),
Proportional(1),
],
),
];
#[derive(Default, Clone, Copy)]
struct App {
selected_tab: SelectedTab,
scroll_offset: u16,
spacing: u16,
state: AppState,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
enum AppState {
#[default]
Running,
Quit,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct Example {
constraints: Vec<Constraint>,
description: String,
flex: Flex,
spacing: u16,
}
/// Tabs for the different layouts
///
/// Note: the order of the variants will determine the order of the tabs this uses several derive
/// macros from the `strum` crate to make it easier to iterate over the variants.
/// (`FromRepr`,`Display`,`EnumIter`).
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, FromRepr, Display, EnumIter)]
enum SelectedTab {
#[default]
StretchLast,
Stretch,
Start,
Center,
End,
SpaceAround,
SpaceBetween,
}
fn main() -> Result<()> {
// assuming the user changes spacing about a 100 times or so
Layout::init_cache(EXAMPLE_DATA.len() * SelectedTab::iter().len() * 100);
init_error_hooks()?;
let terminal = init_terminal()?;
// Each line in the example is a layout
// so 13 examples * 7 = 91 currently
// Plus additional layout for tabs ...
Layout::init_cache(120);
App::default().run(terminal)?;
restore_terminal()?;
Ok(())
}
impl App {
fn run(&mut self, mut terminal: Terminal<impl Backend>) -> Result<()> {
self.draw(&mut terminal)?;
while self.is_running() {
self.handle_events()?;
self.draw(&mut terminal)?;
}
Ok(())
}
fn is_running(&self) -> bool {
self.state == AppState::Running
}
fn draw(self, terminal: &mut Terminal<impl Backend>) -> io::Result<()> {
terminal.draw(|frame| frame.render_widget(self, frame.size()))?;
Ok(())
}
fn handle_events(&mut self) -> Result<()> {
use KeyCode::*;
match event::read()? {
Event::Key(key) if key.kind == KeyEventKind::Press => match key.code {
Char('q') | Esc => self.quit(),
Char('l') | Right => self.next(),
Char('h') | Left => self.previous(),
Char('j') | Down => self.down(),
Char('k') | Up => self.up(),
Char('g') | Home => self.top(),
Char('G') | End => self.bottom(),
Char('+') => self.increment_spacing(),
Char('-') => self.decrement_spacing(),
_ => (),
},
_ => {}
}
Ok(())
}
fn next(&mut self) {
self.selected_tab = self.selected_tab.next();
}
fn previous(&mut self) {
self.selected_tab = self.selected_tab.previous();
}
fn up(&mut self) {
self.scroll_offset = self.scroll_offset.saturating_sub(1)
}
fn down(&mut self) {
self.scroll_offset = self
.scroll_offset
.saturating_add(1)
.min(max_scroll_offset())
}
fn top(&mut self) {
self.scroll_offset = 0;
}
fn bottom(&mut self) {
self.scroll_offset = max_scroll_offset();
}
fn increment_spacing(&mut self) {
self.spacing = self.spacing.saturating_add(1);
}
fn decrement_spacing(&mut self) {
self.spacing = self.spacing.saturating_sub(1);
}
fn quit(&mut self) {
self.state = AppState::Quit;
}
}
// when scrolling, make sure we don't scroll past the last example
fn max_scroll_offset() -> u16 {
example_height()
- EXAMPLE_DATA
.last()
.map(|(desc, _)| get_description_height(desc) + 4)
.unwrap_or(0)
}
/// The height of all examples combined
///
/// Each may or may not have a title so we need to account for that.
fn example_height() -> u16 {
EXAMPLE_DATA
.iter()
.map(|(desc, _)| get_description_height(desc) + 4)
.sum()
}
impl Widget for App {
fn render(self, area: Rect, buf: &mut Buffer) {
let layout = Layout::vertical([Fixed(3), Fixed(1), Proportional(0)]);
let [tabs, axis, demo] = area.split(&layout);
self.tabs().render(tabs, buf);
let scroll_needed = self.render_demo(demo, buf);
let axis_width = if scroll_needed {
axis.width - 1
} else {
axis.width
};
self.axis(axis_width, self.spacing).render(axis, buf);
}
}
impl App {
fn tabs(&self) -> impl Widget {
let tab_titles = SelectedTab::iter().map(SelectedTab::to_tab_title);
let block = Block::new()
.title(Title::from("Flex Layouts ".bold()))
.title(" Use ◄ ► to change tab, ▲ ▼ to scroll, - + to change spacing ");
Tabs::new(tab_titles)
.block(block)
.highlight_style(Modifier::REVERSED)
.select(self.selected_tab as usize)
.divider(" ")
.padding("", "")
}
/// a bar like `<----- 80 px (gap: 2 px)? ----->`
fn axis(&self, width: u16, spacing: u16) -> impl Widget {
let width = width as usize;
// only show gap when spacing is not zero
let label = if spacing != 0 {
format!("{} px (gap: {} px)", width, spacing)
} else {
format!("{} px", width)
};
let bar_width = width - 2; // we want to `<` and `>` at the ends
let width_bar = format!("<{label:-^bar_width$}>");
Paragraph::new(width_bar.dark_gray()).alignment(Alignment::Center)
}
/// Render the demo content
///
/// This function renders the demo content into a separate buffer and then splices the buffer
/// into the main buffer. This is done to make it possible to handle scrolling easily.
///
/// Returns bool indicating whether scroll was needed
fn render_demo(self, area: Rect, buf: &mut Buffer) -> bool {
// render demo content into a separate buffer so all examples fit we add an extra
// area.height to make sure the last example is fully visible even when the scroll offset is
// at the max
let height = example_height();
let demo_area = Rect::new(0, 0, area.width, height);
let mut demo_buf = Buffer::empty(demo_area);
let scrollbar_needed = self.scroll_offset != 0 || height > area.height;
let content_area = if scrollbar_needed {
Rect {
width: demo_area.width - 1,
..demo_area
}
} else {
demo_area
};
let mut spacing = self.spacing;
self.selected_tab
.render(content_area, &mut demo_buf, &mut spacing);
let visible_content = demo_buf
.content
.into_iter()
.skip((area.width * self.scroll_offset) as usize)
.take(area.area() as usize);
for (i, cell) in visible_content.enumerate() {
let x = i as u16 % area.width;
let y = i as u16 / area.width;
*buf.get_mut(area.x + x, area.y + y) = cell;
}
if scrollbar_needed {
let area = area.intersection(buf.area);
let mut state = ScrollbarState::new(max_scroll_offset() as usize)
.position(self.scroll_offset as usize);
Scrollbar::new(ScrollbarOrientation::VerticalRight).render(area, buf, &mut state);
}
scrollbar_needed
}
}
impl SelectedTab {
/// Get the previous tab, if there is no previous tab return the current tab.
fn previous(&self) -> Self {
let current_index: usize = *self as usize;
let previous_index = current_index.saturating_sub(1);
Self::from_repr(previous_index).unwrap_or(*self)
}
/// Get the next tab, if there is no next tab return the current tab.
fn next(&self) -> Self {
let current_index = *self as usize;
let next_index = current_index.saturating_add(1);
Self::from_repr(next_index).unwrap_or(*self)
}
/// Convert a `SelectedTab` into a `Line` to display it by the `Tabs` widget.
fn to_tab_title(value: SelectedTab) -> Line<'static> {
use tailwind::*;
use SelectedTab::*;
let text = value.to_string();
let color = match value {
StretchLast => ORANGE.c400,
Stretch => ORANGE.c300,
Start => SKY.c400,
Center => SKY.c300,
End => SKY.c200,
SpaceAround => INDIGO.c400,
SpaceBetween => INDIGO.c300,
};
format!(" {text} ").fg(color).bg(Color::Black).into()
}
}
impl StatefulWidget for SelectedTab {
type State = u16;
fn render(self, area: Rect, buf: &mut Buffer, spacing: &mut Self::State) {
let spacing = *spacing;
match self {
SelectedTab::StretchLast => self.render_examples(area, buf, Flex::StretchLast, spacing),
SelectedTab::Stretch => self.render_examples(area, buf, Flex::Stretch, spacing),
SelectedTab::Start => self.render_examples(area, buf, Flex::Start, spacing),
SelectedTab::Center => self.render_examples(area, buf, Flex::Center, spacing),
SelectedTab::End => self.render_examples(area, buf, Flex::End, spacing),
SelectedTab::SpaceAround => self.render_examples(area, buf, Flex::SpaceAround, spacing),
SelectedTab::SpaceBetween => {
self.render_examples(area, buf, Flex::SpaceBetween, spacing)
}
}
}
}
impl SelectedTab {
fn render_examples(&self, area: Rect, buf: &mut Buffer, flex: Flex, spacing: u16) {
let heights = EXAMPLE_DATA
.iter()
.map(|(desc, _)| get_description_height(desc) + 4);
let areas = Layout::vertical(heights).flex(Flex::Start).split(area);
for (area, (description, constraints)) in areas.iter().zip(EXAMPLE_DATA.iter()) {
Example::new(constraints, description, flex, spacing).render(*area, buf);
}
}
}
impl Example {
fn new(constraints: &[Constraint], description: &str, flex: Flex, spacing: u16) -> Self {
Self {
constraints: constraints.into(),
description: description.into(),
flex,
spacing,
}
}
}
impl Widget for Example {
fn render(self, area: Rect, buf: &mut Buffer) {
let title_height = get_description_height(&self.description);
let layout = Layout::vertical([Fixed(title_height), Proportional(0)]);
let [title, illustrations] = area.split(&layout);
let blocks = Layout::horizontal(&self.constraints)
.flex(self.flex)
.spacing(self.spacing)
.split(illustrations);
if !self.description.is_empty() {
Paragraph::new(
self.description
.split('\n')
.map(|s| format!("// {}", s).italic().fg(tailwind::SLATE.c400))
.map(Line::from)
.collect::<Vec<Line>>(),
)
.render(title, buf);
}
for (block, constraint) in blocks.iter().zip(&self.constraints) {
self.illustration(*constraint, block.width)
.render(*block, buf);
}
}
}
impl Example {
fn illustration(&self, constraint: Constraint, width: u16) -> Paragraph {
let main_color = color_for_constraint(constraint);
let fg_color = Color::White;
let title = format!("{constraint}");
let content = format!("{width} px");
let text = format!("{title}\n{content}");
let block = Block::bordered()
.border_set(symbols::border::QUADRANT_OUTSIDE)
.border_style(Style::reset().fg(main_color).reversed())
.style(Style::default().fg(fg_color).bg(main_color));
Paragraph::new(text)
.alignment(Alignment::Center)
.block(block)
}
}
fn color_for_constraint(constraint: Constraint) -> Color {
use tailwind::*;
match constraint {
Constraint::Fixed(_) => RED.c900,
Constraint::Min(_) => BLUE.c900,
Constraint::Max(_) => BLUE.c800,
Constraint::Length(_) => SLATE.c700,
Constraint::Percentage(_) => SLATE.c800,
Constraint::Ratio(_, _) => SLATE.c900,
Constraint::Proportional(_) => SLATE.c950,
}
}
fn init_error_hooks() -> Result<()> {
let (panic, error) = HookBuilder::default().into_hooks();
let panic = panic.into_panic_hook();
let error = error.into_eyre_hook();
color_eyre::eyre::set_hook(Box::new(move |e| {
let _ = restore_terminal();
error(e)
}))?;
std::panic::set_hook(Box::new(move |info| {
let _ = restore_terminal();
panic(info)
}));
Ok(())
}
fn init_terminal() -> Result<Terminal<impl Backend>> {
enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout());
let terminal = Terminal::new(backend)?;
Ok(terminal)
}
fn restore_terminal() -> Result<()> {
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
Ok(())
}
fn get_description_height(s: &str) -> u16 {
if s.is_empty() {
0
} else {
s.split('\n').count() as u16
}
}

View File

@@ -1,17 +0,0 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/layout.tape`
Output "target/flex.gif"
Set Theme "Aardvark Blue"
Set Width 1200
Set Height 1410
Hide
Type "cargo run --example=flex --features=crossterm"
Enter
Sleep 2s
Show
Sleep 2s
Right @5s 7
Sleep 2s
Left 7
Sleep 2s
Down @200ms 50

View File

@@ -1,5 +1,6 @@
use std::{
io::{self, stdout, Stdout},
rc::Rc,
time::Duration,
};
@@ -74,7 +75,7 @@ impl App {
fn draw(&mut self) -> Result<()> {
self.term.draw(|frame| {
let state = self.state;
let layout = Layout::vertical([Constraint::Ratio(1, 4); 4]).split(frame.size());
let layout = Self::equal_layout(frame);
Self::render_gauge1(state.progress1, frame, layout[0]);
Self::render_gauge2(state.progress2, frame, layout[1]);
Self::render_gauge3(state.progress3, frame, layout[2]);
@@ -96,6 +97,18 @@ impl App {
Ok(())
}
fn equal_layout(frame: &Frame) -> Rc<[Rect]> {
Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
])
.split(frame.size())
}
fn render_gauge1(progress: u16, frame: &mut Frame, area: Rect) {
let title = Self::title_block("Gauge with percentage progress");
let gauge = Gauge::default()

View File

@@ -215,15 +215,15 @@ fn run_app<B: Backend>(
}
fn ui(f: &mut Frame, downloads: &Downloads) {
let area = f.size();
let size = f.size();
let block = Block::default().title(block::Title::from("Progress").alignment(Alignment::Center));
f.render_widget(block, area);
f.render_widget(block, size);
let vertical = Layout::vertical([Constraint::Length(2), Constraint::Length(4)]).margin(1);
let horizontal = Layout::horizontal([Constraint::Percentage(20), Constraint::Percentage(80)]);
let [progress_area, main] = area.split(&vertical);
let [list_area, gauge_area] = main.split(&horizontal);
let chunks = Layout::default()
.constraints([Constraint::Length(2), Constraint::Length(4)])
.margin(1)
.split(size);
// total progress
let done = NUM_DOWNLOADS - downloads.pending.len() - downloads.in_progress.len();
@@ -231,7 +231,12 @@ fn ui(f: &mut Frame, downloads: &Downloads) {
.gauge_style(Style::default().fg(Color::Blue))
.label(format!("{done}/{NUM_DOWNLOADS}"))
.ratio(done as f64 / NUM_DOWNLOADS as f64);
f.render_widget(progress, progress_area);
f.render_widget(progress, chunks[0]);
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(20), Constraint::Percentage(80)])
.split(chunks[1]);
// in progress downloads
let items: Vec<ListItem> = downloads
@@ -254,21 +259,21 @@ fn ui(f: &mut Frame, downloads: &Downloads) {
})
.collect();
let list = List::new(items);
f.render_widget(list, list_area);
f.render_widget(list, chunks[0]);
for (i, (_, download)) in downloads.in_progress.iter().enumerate() {
let gauge = Gauge::default()
.gauge_style(Style::default().fg(Color::Yellow))
.ratio(download.progress / 100.0);
if gauge_area.top().saturating_add(i as u16) > area.bottom() {
if chunks[1].top().saturating_add(i as u16) > size.bottom() {
continue;
}
f.render_widget(
gauge,
Rect {
x: gauge_area.left(),
y: gauge_area.top().saturating_add(i as u16),
width: gauge_area.width,
x: chunks[1].left(),
y: chunks[1].top().saturating_add(i as u16),
width: chunks[1].width,
height: 1,
},
);

View File

@@ -48,12 +48,14 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
}
fn ui(frame: &mut Frame) {
let vertical = Layout::vertical([
Length(4), // text
Length(50), // examples
Min(0), // fills remaining space
]);
let [text_area, examples_area, _] = frame.size().split(&vertical);
let main_layout = Layout::default()
.direction(Direction::Vertical)
.constraints([
Length(4), // text
Length(50), // examples
Min(0), // fills remaining space
])
.split(frame.size());
// title
frame.render_widget(
@@ -64,34 +66,38 @@ fn ui(frame: &mut Frame) {
Line::from("E.g. the second line of the Len/Min box is [Length(2), Min(2), Min(0)]"),
Line::from("Note: constraint labels that don't fit are truncated"),
]),
text_area,
main_layout[0],
);
let example_rows = Layout::vertical([
Length(9),
Length(9),
Length(9),
Length(9),
Length(9),
Min(0), // fills remaining space
])
.split(examples_area);
let example_rows = Layout::default()
.direction(Direction::Vertical)
.constraints([
Length(9),
Length(9),
Length(9),
Length(9),
Length(9),
Min(0), // fills remaining space
])
.split(main_layout[1]);
let example_areas = example_rows
.iter()
.flat_map(|area| {
Layout::horizontal([
Constraint::Length(14),
Constraint::Length(14),
Constraint::Length(14),
Constraint::Length(14),
Constraint::Length(14),
Constraint::Min(0), // fills remaining space
])
.split(*area)
.iter()
.copied()
.take(5) // ignore Min(0)
.collect_vec()
Layout::default()
.direction(Direction::Horizontal)
.constraints([
Length(14),
Length(14),
Length(14),
Length(14),
Length(14),
Min(0), // fills remaining space
])
.split(*area)
.iter()
.copied()
.take(5) // ignore Min(0)
.collect_vec()
})
.collect_vec();
@@ -176,7 +182,10 @@ fn render_example_combination(
.border_style(Style::default().fg(Color::DarkGray));
let inner = block.inner(area);
frame.render_widget(block, area);
let layout = Layout::vertical(vec![Length(1); constraints.len() + 1]).split(inner);
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![Length(1); constraints.len() + 1])
.split(inner);
for (i, (a, b)) in constraints.iter().enumerate() {
render_single_example(frame, layout[i], vec![*a, *b, Min(0)]);
}
@@ -190,11 +199,13 @@ fn render_single_example(frame: &mut Frame, area: Rect, constraints: Vec<Constra
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();
let horizontal = Layout::horizontal(constraints);
let [r, b, g] = area.split(&horizontal);
frame.render_widget(red, r);
frame.render_widget(blue, b);
frame.render_widget(green, g);
let layout = Layout::default()
.direction(Direction::Horizontal)
.constraints(constraints)
.split(area);
frame.render_widget(red, layout[0]);
frame.render_widget(blue, layout[1]);
frame.render_widget(green, layout[2]);
}
fn constraint_label(constraint: Constraint) -> String {
@@ -203,8 +214,6 @@ fn constraint_label(constraint: Constraint) -> String {
Min(n) => format!("{n}"),
Max(n) => format!("{n}"),
Percentage(n) => format!("{n}"),
Proportional(n) => format!("{n}"),
Fixed(n) => format!("{n}"),
Ratio(a, b) => format!("{a}:{b}"),
}
}

View File

@@ -198,8 +198,10 @@ fn run_app<B: Backend>(
fn ui(f: &mut Frame, app: &mut App) {
// Create two chunks with equal horizontal screen space
let horizontal = Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]);
let [item_list_area, event_list_area] = f.size().split(&horizontal);
let chunks = Layout::default()
.direction(Direction::Horizontal)
.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.
let items: Vec<ListItem> = app
@@ -207,7 +209,7 @@ fn ui(f: &mut Frame, app: &mut App) {
.items
.iter()
.map(|i| {
let mut lines = vec![Line::from(i.0.bold()).alignment(Alignment::Center)];
let mut lines = vec![Line::from(i.0)];
for _ in 0..i.1 {
lines.push(
"Lorem ipsum dolor sit amet, consectetur adipiscing elit."
@@ -230,7 +232,7 @@ fn ui(f: &mut Frame, app: &mut App) {
.highlight_symbol(">> ");
// We can now render the item list
f.render_stateful_widget(items, item_list_area, &mut app.items.state);
f.render_stateful_widget(items, chunks[0], &mut app.items.state);
// Let's do the same for the events.
// The event list doesn't have any state and only displays the current state of the list.
@@ -262,7 +264,7 @@ fn ui(f: &mut Frame, app: &mut App) {
// 3. Add a spacer line
// 4. Add the actual event
ListItem::new(vec![
Line::from("-".repeat(event_list_area.width as usize)),
Line::from("-".repeat(chunks[1].width as usize)),
header,
Line::from(""),
log,
@@ -272,5 +274,5 @@ fn ui(f: &mut Frame, app: &mut App) {
let events_list = List::new(events)
.block(Block::default().borders(Borders::ALL).title("List"))
.direction(ListDirection::BottomToTop);
f.render_widget(events_list, event_list_area);
f.render_widget(events_list, chunks[1]);
}

View File

@@ -44,18 +44,24 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
}
fn ui(frame: &mut Frame) {
let vertical = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
let [text_area, main_area] = frame.size().split(&vertical);
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(1), Constraint::Min(0)])
.split(frame.size());
frame.render_widget(
Paragraph::new("Note: not all terminals support all modifiers")
.style(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
text_area,
layout[0],
);
let layout = Layout::vertical([Constraint::Length(1); 50])
.split(main_area)
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(1); 50])
.split(layout[1])
.iter()
.flat_map(|area| {
Layout::horizontal([Constraint::Percentage(20); 5])
Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(20); 5])
.split(*area)
.to_vec()
})

View File

@@ -90,7 +90,15 @@ fn ui(f: &mut Frame, app: &App) {
let block = Block::default().black();
f.render_widget(block, size);
let layout = Layout::vertical([Constraint::Ratio(1, 4); 4]).split(size);
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
])
.split(size);
let text = vec![
Line::from("This is a line "),
@@ -121,20 +129,20 @@ fn ui(f: &mut Frame, app: &App) {
let paragraph = Paragraph::new(text.clone())
.style(Style::default().fg(Color::Gray))
.block(create_block("Default alignment (Left), no wrap"));
f.render_widget(paragraph, layout[0]);
f.render_widget(paragraph, chunks[0]);
let paragraph = Paragraph::new(text.clone())
.style(Style::default().fg(Color::Gray))
.block(create_block("Default alignment (Left), with wrap"))
.wrap(Wrap { trim: true });
f.render_widget(paragraph, layout[1]);
f.render_widget(paragraph, chunks[1]);
let paragraph = Paragraph::new(text.clone())
.style(Style::default().fg(Color::Gray))
.block(create_block("Right alignment, with wrap"))
.alignment(Alignment::Right)
.wrap(Wrap { trim: true });
f.render_widget(paragraph, layout[2]);
f.render_widget(paragraph, chunks[2]);
let paragraph = Paragraph::new(text)
.style(Style::default().fg(Color::Gray))
@@ -142,5 +150,5 @@ fn ui(f: &mut Frame, app: &App) {
.alignment(Alignment::Center)
.wrap(Wrap { trim: true })
.scroll((app.scroll, 0));
f.render_widget(paragraph, layout[3]);
f.render_widget(paragraph, chunks[3]);
}

View File

@@ -62,10 +62,11 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
}
fn ui(f: &mut Frame, app: &App) {
let area = f.size();
let size = f.size();
let vertical = Layout::vertical([Constraint::Percentage(20), Constraint::Percentage(80)]);
let [instructions, content] = area.split(&vertical);
let chunks = Layout::default()
.constraints([Constraint::Percentage(20), Constraint::Percentage(80)])
.split(size);
let text = if app.show_popup {
"Press p to close the popup"
@@ -75,17 +76,17 @@ fn ui(f: &mut Frame, app: &App) {
let paragraph = Paragraph::new(text.slow_blink())
.alignment(Alignment::Center)
.wrap(Wrap { trim: true });
f.render_widget(paragraph, instructions);
f.render_widget(paragraph, chunks[0]);
let block = Block::default()
.title("Content")
.borders(Borders::ALL)
.on_blue();
f.render_widget(block, content);
f.render_widget(block, chunks[1]);
if app.show_popup {
let block = Block::default().title("Popup").borders(Borders::ALL);
let area = centered_rect(60, 20, area);
let area = centered_rect(60, 20, size);
f.render_widget(Clear, area); //this clears out the background
f.render_widget(block, area);
}
@@ -93,17 +94,21 @@ fn ui(f: &mut Frame, app: &App) {
/// helper function to create a centered rect using up certain percentage of the available rect `r`
fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
let popup_layout = Layout::vertical([
Constraint::Percentage((100 - percent_y) / 2),
Constraint::Percentage(percent_y),
Constraint::Percentage((100 - percent_y) / 2),
])
.split(r);
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),
])
.split(r);
Layout::horizontal([
Constraint::Percentage((100 - percent_x) / 2),
Constraint::Percentage(percent_x),
Constraint::Percentage((100 - percent_x) / 2),
])
.split(popup_layout[1])[1]
Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage((100 - percent_x) / 2),
Constraint::Percentage(percent_x),
Constraint::Percentage((100 - percent_x) / 2),
])
.split(popup_layout[1])[1]
}

View File

@@ -62,22 +62,22 @@ fn run_app<B: Backend>(
if let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Char('q') => return Ok(()),
KeyCode::Char('j') | KeyCode::Down => {
KeyCode::Char('j') => {
app.vertical_scroll = app.vertical_scroll.saturating_add(1);
app.vertical_scroll_state =
app.vertical_scroll_state.position(app.vertical_scroll);
}
KeyCode::Char('k') | KeyCode::Up => {
KeyCode::Char('k') => {
app.vertical_scroll = app.vertical_scroll.saturating_sub(1);
app.vertical_scroll_state =
app.vertical_scroll_state.position(app.vertical_scroll);
}
KeyCode::Char('h') | KeyCode::Left => {
KeyCode::Char('h') => {
app.horizontal_scroll = app.horizontal_scroll.saturating_sub(1);
app.horizontal_scroll_state =
app.horizontal_scroll_state.position(app.horizontal_scroll);
}
KeyCode::Char('l') | KeyCode::Right => {
KeyCode::Char('l') => {
app.horizontal_scroll = app.horizontal_scroll.saturating_add(1);
app.horizontal_scroll_state =
app.horizontal_scroll_state.position(app.horizontal_scroll);
@@ -100,14 +100,19 @@ fn ui(f: &mut Frame, app: &mut App) {
let mut long_line = s.repeat(usize::from(size.width) / s.len() + 4);
long_line.push('\n');
let chunks = Layout::vertical([
Constraint::Min(1),
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
])
.split(size);
let block = Block::default().black();
f.render_widget(block, size);
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Min(1),
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
])
.split(size);
let text = vec![
Line::from("This is a line "),
@@ -140,10 +145,18 @@ fn ui(f: &mut Frame, app: &mut App) {
app.vertical_scroll_state = app.vertical_scroll_state.content_length(text.len());
app.horizontal_scroll_state = app.horizontal_scroll_state.content_length(long_line.len());
let create_block = |title: &'static str| Block::bordered().gray().title(title.bold());
let create_block = |title| {
Block::default()
.borders(Borders::ALL)
.gray()
.title(Span::styled(
title,
Style::default().add_modifier(Modifier::BOLD),
))
};
let title = Block::default()
.title("Use h j k l or ◄ ▲ ▼ ► to scroll ".bold())
.title("Use h j k l to scroll ◄ ▲ ▼ ►")
.title_alignment(Alignment::Center);
f.render_widget(title, chunks[0]);

View File

@@ -125,12 +125,14 @@ fn run_app<B: Backend>(
}
fn ui(f: &mut Frame, app: &App) {
let chunks = Layout::vertical([
Constraint::Length(3),
Constraint::Length(3),
Constraint::Min(0),
])
.split(f.size());
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3),
Constraint::Length(3),
Constraint::Min(0),
])
.split(f.size());
let sparkline = Sparkline::default()
.block(
Block::default()

View File

@@ -1,23 +1,42 @@
use std::{error::Error, io, str::FromStr};
use std::{error::Error, io};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use itertools::Itertools;
use ratatui::{prelude::*, widgets::*};
struct App {
struct App<'a> {
state: TableState,
items: Vec<Vec<String>>,
items: Vec<Vec<&'a str>>,
}
impl App {
fn new() -> App {
impl<'a> App<'a> {
fn new() -> App<'a> {
App {
state: TableState::default().with_selected(0),
items: generate_fake_names(),
state: TableState::default(),
items: vec![
vec!["Row11", "Row12", "Row13"],
vec!["Row21", "Row22", "Row23"],
vec!["Row31", "Row32", "Row33"],
vec!["Row41", "Row42", "Row43"],
vec!["Row51", "Row52", "Row53"],
vec!["Row61", "Row62\nTest", "Row63"],
vec!["Row71", "Row72", "Row73"],
vec!["Row81", "Row82", "Row83"],
vec!["Row91", "Row92", "Row93"],
vec!["Row101", "Row102", "Row103"],
vec!["Row111", "Row112", "Row113"],
vec!["Row121", "Row122", "Row123"],
vec!["Row131", "Row132", "Row133"],
vec!["Row141", "Row142", "Row143"],
vec!["Row151", "Row152", "Row153"],
vec!["Row161", "Row162", "Row163"],
vec!["Row171", "Row172", "Row173"],
vec!["Row181", "Row182", "Row183"],
vec!["Row191", "Row192", "Row193"],
],
}
}
pub fn next(&mut self) {
@@ -49,26 +68,6 @@ impl App {
}
}
fn generate_fake_names() -> Vec<Vec<String>> {
use fakeit::{address, contact, name};
(0..20)
.map(|_| {
let name = name::full();
let address = format!(
"{}\n{}, {} {}",
address::street(),
address::city(),
address::state(),
address::zip()
);
let email = contact::email();
vec![name, address, email]
})
.sorted_by(|a, b| a[0].cmp(&b[0]))
.collect_vec()
}
fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
enable_raw_mode()?;
@@ -115,52 +114,48 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
}
fn ui(f: &mut Frame, app: &mut App) {
let rects = Layout::vertical([Constraint::Percentage(100)]).split(f.size());
let rects = Layout::default()
.constraints([Constraint::Percentage(100)])
.split(f.size());
// colors from https://tailwindcss.com/docs/customizing-colors
let header_bg = Color::from_str("#1e3a8a").unwrap();
let header_fg = Color::from_str("#eff6ff").unwrap();
let header_style = Style::default().fg(header_fg).bg(header_bg);
let normal_row_color = Color::from_str("#1e293b").unwrap();
let alt_row_color = Color::from_str("#0f172a").unwrap();
let selected_style = Style::default().add_modifier(Modifier::REVERSED);
let header = ["Name", "Address", "Email"]
let normal_style = Style::default().bg(Color::Blue);
let header_cells = ["Header1", "Header2", "Header3"]
.iter()
.cloned()
.map(Cell::from)
.collect::<Row>()
.style(header_style)
.height(1);
let rows = app.items.iter().enumerate().map(|(i, item)| {
let color = match i % 2 {
0 => normal_row_color,
_ => alt_row_color,
};
item.iter()
.cloned()
.map(|content| Cell::from(Text::from(format!("\n{}\n", content))))
.collect::<Row>()
.style(Style::new().bg(color))
.height(4)
.map(|h| Cell::from(*h).style(Style::default().fg(Color::Red)));
let header = Row::new(header_cells)
.style(normal_style)
.height(1)
.bottom_margin(1);
let footer_cells = ["Footer1", "Footer2", "Footer3"]
.iter()
.map(|f| Cell::from(*f).style(Style::default().fg(Color::Yellow)));
let footer = Row::new(footer_cells)
.style(normal_style)
.height(1)
.top_margin(1);
let rows = app.items.iter().map(|item| {
let height = item
.iter()
.map(|content| content.chars().filter(|c| *c == '\n').count())
.max()
.unwrap_or(0)
+ 1;
let cells = item.iter().map(|c| Cell::from(*c));
Row::new(cells).height(height as u16).bottom_margin(1)
});
let bar = "";
let t = Table::new(
rows,
[
Constraint::Length(15),
Constraint::Min(30),
Constraint::Min(25),
Constraint::Percentage(50),
Constraint::Max(30),
Constraint::Min(10),
],
)
.header(header)
.footer(footer)
.block(Block::default().borders(Borders::ALL).title("Table"))
.highlight_style(selected_style)
.highlight_symbol(Text::from(vec![
"".into(),
bar.into(),
bar.into(),
"".into(),
]))
.highlight_spacing(HighlightSpacing::Always);
.highlight_symbol(">> ");
f.render_stateful_widget(t, rects[0], &mut app.state);
}

View File

@@ -3,17 +3,14 @@
Output "target/table.gif"
Set Theme "Aardvark Blue"
Set Width 1200
Set Height 800
Set Height 600
Hide
Type "cargo run --example=table --features=crossterm"
Enter
Sleep 1s
Show
Sleep 2s
Set TypingSpeed 0.5s
Down 2
Up 2
Down 8
Up 12
Down 4
Sleep 2s
Down@1s 4
Up@1s 2
Down@1s 8
Up@1s 12
Sleep 5s

View File

@@ -79,25 +79,28 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
}
fn ui(f: &mut Frame, app: &App) {
let area = f.size();
let vertical = Layout::vertical([Constraint::Length(3), Constraint::Min(0)]);
let [tabs_area, inner_area] = area.split(&vertical);
let size = f.size();
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(3), Constraint::Min(0)])
.split(size);
let block = Block::default().on_white().black();
f.render_widget(block, area);
let tabs = app
f.render_widget(block, size);
let titles = app
.titles
.iter()
.map(|t| {
let (first, rest) = t.split_at(1);
Line::from(vec![first.yellow(), rest.green()])
})
.collect::<Tabs>()
.collect();
let tabs = Tabs::new(titles)
.block(Block::default().borders(Borders::ALL).title("Tabs"))
.select(app.index)
.style(Style::default().cyan().on_gray())
.highlight_style(Style::default().bold().on_black());
f.render_widget(tabs, tabs_area);
f.render_widget(tabs, chunks[0]);
let inner = match app.index {
0 => Block::default().title("Inner 0").borders(Borders::ALL),
1 => Block::default().title("Inner 1").borders(Borders::ALL),
@@ -105,5 +108,5 @@ fn ui(f: &mut Frame, app: &App) {
3 => Block::default().title("Inner 3").borders(Borders::ALL),
_ => unreachable!(),
};
f.render_widget(inner, inner_area);
f.render_widget(inner, chunks[1]);
}

View File

@@ -172,12 +172,14 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
}
fn ui(f: &mut Frame, app: &App) {
let vertical = Layout::vertical([
Constraint::Length(1),
Constraint::Length(3),
Constraint::Min(1),
]);
let [help_area, input_area, messages_area] = f.size().split(&vertical);
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1),
Constraint::Length(3),
Constraint::Min(1),
])
.split(f.size());
let (msg, style) = match app.input_mode {
InputMode::Normal => (
@@ -201,9 +203,10 @@ fn ui(f: &mut Frame, app: &App) {
Style::default(),
),
};
let text = Text::from(Line::from(msg)).patch_style(style);
let mut text = Text::from(Line::from(msg));
text.patch_style(style);
let help_message = Paragraph::new(text);
f.render_widget(help_message, help_area);
f.render_widget(help_message, chunks[0]);
let input = Paragraph::new(app.input.as_str())
.style(match app.input_mode {
@@ -211,7 +214,7 @@ fn ui(f: &mut Frame, app: &App) {
InputMode::Editing => Style::default().fg(Color::Yellow),
})
.block(Block::default().borders(Borders::ALL).title("Input"));
f.render_widget(input, input_area);
f.render_widget(input, chunks[1]);
match app.input_mode {
InputMode::Normal =>
// Hide the cursor. `Frame` does this by default, so we don't need to do anything here
@@ -223,9 +226,9 @@ fn ui(f: &mut Frame, app: &App) {
f.set_cursor(
// Draw the cursor at the current position in the input field.
// This position is can be controlled via the left and right arrow key
input_area.x + app.cursor_position as u16 + 1,
chunks[1].x + app.cursor_position as u16 + 1,
// Move one line down, from the border to the input line
input_area.y + 1,
chunks[1].y + 1,
)
}
}
@@ -241,5 +244,5 @@ fn ui(f: &mut Frame, app: &App) {
.collect();
let messages =
List::new(messages).block(Block::default().borders(Borders::ALL).title("Messages"));
f.render_widget(messages, messages_area);
f.render_widget(messages, chunks[2]);
}

View File

@@ -38,7 +38,7 @@
//! # std::io::Result::Ok(())
//! ```
//!
//! See the the [Examples] directory for more examples.
//! See the the [examples] directory for more examples.
//!
//! # Raw Mode
//!
@@ -96,7 +96,7 @@
//! [Crossterm]: https://crates.io/crates/crossterm
//! [Termion]: https://crates.io/crates/termion
//! [Termwiz]: https://crates.io/crates/termwiz
//! [Examples]: https://github.com/ratatui-org/ratatui/tree/main/examples/README.md
//! [examples]: https://github.com/ratatui-org/ratatui/tree/main/examples#readme
//! [Backend Comparison]:
//! https://ratatui.rs/concepts/backends/comparison/
//! [Ratatui Website]: https://ratatui-org.github.io/ratatui-book

View File

@@ -70,14 +70,14 @@ use crate::{
/// # std::io::Result::Ok(())
/// ```
///
/// See the the [Examples] directory for more examples. See the [`backend`] module documentation
/// See the the [examples] directory for more examples. See the [`backend`] module documentation
/// for more details on raw mode and alternate screen.
///
/// [`Write`]: std::io::Write
/// [`Terminal`]: crate::terminal::Terminal
/// [`backend`]: crate::backend
/// [Crossterm]: https://crates.io/crates/crossterm
/// [Examples]: https://github.com/ratatui-org/ratatui/tree/main/examples/README.md
/// [examples]: https://github.com/ratatui-org/ratatui/tree/main/examples#examples
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct CrosstermBackend<W: Write> {
/// The writer used to send commands to the terminal.

View File

@@ -52,14 +52,14 @@ use crate::{
/// # std::result::Result::Ok::<(), Box<dyn std::error::Error>>(())
/// ```
///
/// See the the [Examples] directory for more examples. See the [`backend`] module documentation
/// See the the [examples] directory for more examples. See the [`backend`] module documentation
/// for more details on raw mode and alternate screen.
///
/// [`backend`]: crate::backend
/// [`Terminal`]: crate::terminal::Terminal
/// [`BufferedTerminal`]: termwiz::terminal::buffered::BufferedTerminal
/// [Termwiz]: https://crates.io/crates/termwiz
/// [Examples]: https://github.com/ratatui-org/ratatui/tree/main/examples/README.md
/// [examples]: https://github.com/ratatui-org/ratatui/tree/main/examples#readme
pub struct TermwizBackend {
buffered_terminal: BufferedTerminal<SystemTerminal>,
}

View File

@@ -4,9 +4,6 @@
#[macro_export]
macro_rules! assert_buffer_eq {
($actual_expr:expr, $expected_expr:expr) => {
assert_buffer_eq!($actual_expr, $expected_expr, "buffers not equal")
};
($actual_expr:expr, $expected_expr:expr, $message:expr) => {
match (&$actual_expr, &$expected_expr) {
(actual, expected) => {
if actual.area != expected.area {
@@ -49,7 +46,7 @@ macro_rules! assert_buffer_eq {
}
// shouldn't get here, but this guards against future behavior
// that changes equality but not area or content
assert_eq!(actual, expected, $message);
assert_eq!(actual, expected, "buffers not equal");
}
}
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,31 +0,0 @@
use strum::{Display, EnumString};
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
pub enum Alignment {
#[default]
Left,
Center,
Right,
}
#[cfg(test)]
mod tests {
use strum::ParseError;
use super::*;
#[test]
fn alignment_to_string() {
assert_eq!(Alignment::Left.to_string(), "Left");
assert_eq!(Alignment::Center.to_string(), "Center");
assert_eq!(Alignment::Right.to_string(), "Right");
}
#[test]
fn alignment_from_str() {
assert_eq!("Left".parse::<Alignment>(), Ok(Alignment::Left));
assert_eq!("Center".parse::<Alignment>(), Ok(Alignment::Center));
assert_eq!("Right".parse::<Alignment>(), Ok(Alignment::Right));
assert_eq!("".parse::<Alignment>(), Err(ParseError::VariantNotFound));
}
}

View File

@@ -1,551 +0,0 @@
use std::fmt::{self, Display};
use itertools::Itertools;
/// A constraint that defines the size of a layout element.
///
/// Constraints can be used to specify a fixed size, a percentage of the available space, a ratio of
/// the available space, a minimum or maximum size or a proportional value for a layout element.
///
/// Relative constraints (percentage, ratio) are calculated relative to the entire space being
/// divided, rather than the space available after applying more fixed constraints (min, max,
/// length).
///
/// Constraints are prioritized in the following order:
///
/// 1. [`Constraint::Fixed`]
/// 2. [`Constraint::Min`] / [`Constraint::Max`]
/// 3. [`Constraint::Length`] / [`Constraint::Percentage`] / [`Constraint::Ratio`]
/// 4. [`Constraint::Proportional`]
///
/// # Examples
///
/// `Constraint` provides helper methods to create lists of constraints from various input formats.
///
/// ```rust
/// # use ratatui::prelude::*;
/// // Create a layout with specified lengths for each element
/// let constraints = Constraint::from_lengths([10, 20, 10]);
///
/// // Create a layout with specified fixed lengths for each element
/// let constraints = Constraint::from_fixed_lengths([10, 20, 10]);
///
/// // Create a centered layout using ratio or percentage constraints
/// let constraints = Constraint::from_ratios([(1, 4), (1, 2), (1, 4)]);
/// let constraints = Constraint::from_percentages([25, 50, 25]);
///
/// // Create a centered layout with a minimum size constraint for specific elements
/// let constraints = Constraint::from_mins([0, 100, 0]);
///
/// // Create a sidebar layout specifying maximum sizes for the columns
/// let constraints = Constraint::from_maxes([30, 170]);
///
/// // Create a layout with proportional sizes for each element
/// let constraints = Constraint::from_proportional_lengths([1, 2, 1]);
/// ```
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub enum Constraint {
/// Applies a percentage of the available space to the element
///
/// Converts the given percentage to a floating-point value and multiplies that with area.
/// This value is rounded back to a integer as part of the layout split calculation.
///
/// # Examples
///
/// `[Percentage(75), Proportional(1)]`
///
/// ```plain
/// ┌────────────────────────────────────┐┌──────────┐
/// │ 38 px ││ 12 px │
/// └────────────────────────────────────┘└──────────┘
/// ```
///
/// `[Percentage(50), Proportional(1)]`
///
/// ```plain
/// ┌───────────────────────┐┌───────────────────────┐
/// │ 25 px ││ 25 px │
/// └───────────────────────┘└───────────────────────┘
/// ```
Percentage(u16),
/// Applies a ratio of the available space to the element
///
/// Converts the given ratio to a floating-point value and multiplies that with area.
/// This value is rounded back to a integer as part of the layout split calculation.
///
/// # Examples
///
/// `[Ratio(1, 2) ; 2]`
///
/// ```plain
/// ┌───────────────────────┐┌───────────────────────┐
/// │ 25 px ││ 25 px │
/// └───────────────────────┘└───────────────────────┘
/// ```
///
/// `[Ratio(1, 4) ; 4]`
///
/// ```plain
/// ┌───────────┐┌──────────┐┌───────────┐┌──────────┐
/// │ 13 px ││ 12 px ││ 13 px ││ 12 px │
/// └───────────┘└──────────┘└───────────┘└──────────┘
/// ```
Ratio(u32, u32),
/// Applies a fixed size to the element
///
/// The element size is set to the specified amount.
/// [`Constraint::Fixed`] will take precedence over all other constraints.
///
/// # Examples
///
/// `[Fixed(40), Proportional(1)]`
///
/// ```plain
/// ┌──────────────────────────────────────┐┌────────┐
/// │ 40 px ││ 10 px │
/// └──────────────────────────────────────┘└────────┘
/// ```
///
/// `[Fixed(20), Fixed(20), Proportional(1)]`
///
/// ```plain
/// ┌──────────────────┐┌──────────────────┐┌────────┐
/// │ 20 px ││ 20 px ││ 10 px │
/// └──────────────────┘└──────────────────┘└────────┘
/// ```
Fixed(u16),
/// Applies a length constraint to the element
///
/// The element size is set to the specified amount.
///
/// # Examples
///
/// `[Length(20), Fixed(20)]`
///
/// ```plain
/// ┌────────────────────────────┐┌──────────────────┐
/// │ 30 px ││ 20 px │
/// └────────────────────────────┘└──────────────────┘
/// ```
///
/// `[Length(20), Length(20)]`
///
/// ```plain
/// ┌──────────────────┐┌────────────────────────────┐
/// │ 20 px ││ 30 px │
/// └──────────────────┘└────────────────────────────┘
/// ```
Length(u16),
/// Applies the scaling factor proportional to all other [`Constraint::Proportional`] elements
/// to fill excess space
///
/// The element will only expand into excess available space, proportionally matching other
/// [`Constraint::Proportional`] elements while satisfying all other constraints.
///
/// # Examples
///
///
/// `[Proportional(1), Proportional(2), Proportional(3)]`
///
/// ```plain
/// ┌──────┐┌───────────────┐┌───────────────────────┐
/// │ 8 px ││ 17 px ││ 25 px │
/// └──────┘└───────────────┘└───────────────────────┘
/// ```
///
/// `[Proportional(1), Percentage(50), Proportional(1)]`
///
/// ```plain
/// ┌───────────┐┌───────────────────────┐┌──────────┐
/// │ 13 px ││ 25 px ││ 12 px │
/// └───────────┘└───────────────────────┘└──────────┘
/// ```
Proportional(u16),
/// Applies a maximum size constraint to the element
///
/// The element size is set to at most the specified amount.
///
/// # Examples
///
/// `[Percentage(100), Min(20)]`
///
/// ```plain
/// ┌────────────────────────────┐┌──────────────────┐
/// │ 30 px ││ 20 px │
/// └────────────────────────────┘└──────────────────┘
/// ```
///
/// `[Percentage(100), Min(10)]`
///
/// ```plain
/// ┌──────────────────────────────────────┐┌────────┐
/// │ 40 px ││ 10 px │
/// └──────────────────────────────────────┘└────────┘
/// ```
Max(u16),
/// Applies a minimum size constraint to the element
///
/// The element size is set to at least the specified amount.
///
/// # Examples
///
/// `[Percentage(100), Min(20)]`
///
/// ```plain
/// ┌────────────────────────────┐┌──────────────────┐
/// │ 30 px ││ 20 px │
/// └────────────────────────────┘└──────────────────┘
/// ```
///
/// `[Percentage(100), Min(10)]`
///
/// ```plain
/// ┌──────────────────────────────────────┐┌────────┐
/// │ 40 px ││ 10 px │
/// └──────────────────────────────────────┘└────────┘
/// ```
Min(u16),
}
impl Constraint {
#[deprecated(
since = "0.26.0",
note = "This field will be hidden in the next minor version."
)]
pub fn apply(&self, length: u16) -> u16 {
match *self {
Constraint::Percentage(p) => {
let p = p as f32 / 100.0;
let length = length as f32;
(p * length).min(length) as u16
}
Constraint::Ratio(numerator, denominator) => {
// avoid division by zero by using 1 when denominator is 0
// this results in 0/0 -> 0 and x/0 -> x for x != 0
let percentage = numerator as f32 / denominator.max(1) as f32;
let length = length as f32;
(percentage * length).min(length) as u16
}
Constraint::Length(l) => length.min(l),
Constraint::Fixed(l) => length.min(l),
Constraint::Proportional(l) => length.min(l),
Constraint::Max(m) => length.min(m),
Constraint::Min(m) => length.max(m),
}
}
/// Convert an iterator of lengths into a vector of constraints
///
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// # let area = Rect::default();
/// let constraints = Constraint::from_lengths([1, 2, 3]);
/// let layout = Layout::default().constraints(constraints).split(area);
/// ```
pub fn from_lengths<T>(lengths: T) -> Vec<Constraint>
where
T: IntoIterator<Item = u16>,
{
lengths.into_iter().map(Constraint::Length).collect_vec()
}
/// Convert an iterator of fixed lengths into a vector of constraints
///
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// # let area = Rect::default();
/// let constraints = Constraint::from_fixed_lengths([1, 2, 3]);
/// let layout = Layout::default().constraints(constraints).split(area);
/// ```
pub fn from_fixed_lengths<T>(fixed_lengths: T) -> Vec<Constraint>
where
T: IntoIterator<Item = u16>,
{
fixed_lengths
.into_iter()
.map(Constraint::Fixed)
.collect_vec()
}
/// Convert an iterator of ratios into a vector of constraints
///
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// # let area = Rect::default();
/// let constraints = Constraint::from_ratios([(1, 4), (1, 2), (1, 4)]);
/// let layout = Layout::default().constraints(constraints).split(area);
/// ```
pub fn from_ratios<T>(ratios: T) -> Vec<Constraint>
where
T: IntoIterator<Item = (u32, u32)>,
{
ratios
.into_iter()
.map(|(n, d)| Constraint::Ratio(n, d))
.collect_vec()
}
/// Convert an iterator of percentages into a vector of constraints
///
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// # let area = Rect::default();
/// let constraints = Constraint::from_percentages([25, 50, 25]);
/// let layout = Layout::default().constraints(constraints).split(area);
/// ```
pub fn from_percentages<T>(percentages: T) -> Vec<Constraint>
where
T: IntoIterator<Item = u16>,
{
percentages
.into_iter()
.map(Constraint::Percentage)
.collect_vec()
}
/// Convert an iterator of maxes into a vector of constraints
///
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// # let area = Rect::default();
/// let constraints = Constraint::from_maxes([1, 2, 3]);
/// let layout = Layout::default().constraints(constraints).split(area);
/// ```
pub fn from_maxes<T>(maxes: T) -> Vec<Constraint>
where
T: IntoIterator<Item = u16>,
{
maxes.into_iter().map(Constraint::Max).collect_vec()
}
/// Convert an iterator of mins into a vector of constraints
///
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// # let area = Rect::default();
/// let constraints = Constraint::from_mins([1, 2, 3]);
/// let layout = Layout::default().constraints(constraints).split(area);
/// ```
pub fn from_mins<T>(mins: T) -> Vec<Constraint>
where
T: IntoIterator<Item = u16>,
{
mins.into_iter().map(Constraint::Min).collect_vec()
}
/// Convert an iterator of proportional factors into a vector of constraints
///
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// # let area = Rect::default();
/// let constraints = Constraint::from_mins([1, 2, 3]);
/// let layout = Layout::default().constraints(constraints).split(area);
/// ```
pub fn from_proportional_lengths<T>(proportional_lengths: T) -> Vec<Constraint>
where
T: IntoIterator<Item = u16>,
{
proportional_lengths
.into_iter()
.map(Constraint::Proportional)
.collect_vec()
}
}
impl From<u16> for Constraint {
/// Convert a u16 into a [Constraint::Length]
///
/// This is useful when you want to specify a fixed size for a layout, but don't want to
/// explicitly create a [Constraint::Length] yourself.
///
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// # let area = Rect::default();
/// let layout = Layout::new(Direction::Vertical, [1, 2, 3]).split(area);
/// let layout = Layout::horizontal([1, 2, 3]).split(area);
/// let layout = Layout::vertical([1, 2, 3]).split(area);
/// ````
fn from(length: u16) -> Constraint {
Constraint::Length(length)
}
}
impl From<&Constraint> for Constraint {
fn from(constraint: &Constraint) -> Self {
*constraint
}
}
impl AsRef<Constraint> for Constraint {
fn as_ref(&self) -> &Constraint {
self
}
}
impl Default for Constraint {
fn default() -> Self {
Constraint::Percentage(100)
}
}
impl Display for Constraint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Constraint::Percentage(p) => write!(f, "Percentage({})", p),
Constraint::Ratio(n, d) => write!(f, "Ratio({}, {})", n, d),
Constraint::Length(l) => write!(f, "Length({})", l),
Constraint::Fixed(l) => write!(f, "Fixed({})", l),
Constraint::Proportional(l) => write!(f, "Proportional({})", l),
Constraint::Max(m) => write!(f, "Max({})", m),
Constraint::Min(m) => write!(f, "Min({})", m),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default() {
assert_eq!(Constraint::default(), Constraint::Percentage(100));
}
#[test]
fn to_string() {
assert_eq!(Constraint::Percentage(50).to_string(), "Percentage(50)");
assert_eq!(Constraint::Ratio(1, 2).to_string(), "Ratio(1, 2)");
assert_eq!(Constraint::Length(10).to_string(), "Length(10)");
assert_eq!(Constraint::Max(10).to_string(), "Max(10)");
assert_eq!(Constraint::Min(10).to_string(), "Min(10)");
}
#[test]
fn from_lengths() {
let expected = [
Constraint::Length(1),
Constraint::Length(2),
Constraint::Length(3),
];
assert_eq!(Constraint::from_lengths([1, 2, 3]), expected);
assert_eq!(Constraint::from_lengths(vec![1, 2, 3]), expected);
}
#[test]
fn from_fixed_lengths() {
let expected = [
Constraint::Fixed(1),
Constraint::Fixed(2),
Constraint::Fixed(3),
];
assert_eq!(Constraint::from_fixed_lengths([1, 2, 3]), expected);
assert_eq!(Constraint::from_fixed_lengths(vec![1, 2, 3]), expected);
}
#[test]
fn from_ratios() {
let expected = [
Constraint::Ratio(1, 4),
Constraint::Ratio(1, 2),
Constraint::Ratio(1, 4),
];
assert_eq!(Constraint::from_ratios([(1, 4), (1, 2), (1, 4)]), expected);
assert_eq!(
Constraint::from_ratios(vec![(1, 4), (1, 2), (1, 4)]),
expected
);
}
#[test]
fn from_percentages() {
let expected = [
Constraint::Percentage(25),
Constraint::Percentage(50),
Constraint::Percentage(25),
];
assert_eq!(Constraint::from_percentages([25, 50, 25]), expected);
assert_eq!(Constraint::from_percentages(vec![25, 50, 25]), expected);
}
#[test]
fn from_maxes() {
let expected = [Constraint::Max(1), Constraint::Max(2), Constraint::Max(3)];
assert_eq!(Constraint::from_maxes([1, 2, 3]), expected);
assert_eq!(Constraint::from_maxes(vec![1, 2, 3]), expected);
}
#[test]
fn from_mins() {
let expected = [Constraint::Min(1), Constraint::Min(2), Constraint::Min(3)];
assert_eq!(Constraint::from_mins([1, 2, 3]), expected);
assert_eq!(Constraint::from_mins(vec![1, 2, 3]), expected);
}
#[test]
fn from_proportional_lengths() {
let expected = [
Constraint::Proportional(1),
Constraint::Proportional(2),
Constraint::Proportional(3),
];
assert_eq!(Constraint::from_proportional_lengths([1, 2, 3]), expected);
assert_eq!(
Constraint::from_proportional_lengths(vec![1, 2, 3]),
expected
);
}
#[test]
#[allow(deprecated)]
fn apply() {
assert_eq!(Constraint::Percentage(0).apply(100), 0);
assert_eq!(Constraint::Percentage(50).apply(100), 50);
assert_eq!(Constraint::Percentage(100).apply(100), 100);
assert_eq!(Constraint::Percentage(200).apply(100), 100);
assert_eq!(Constraint::Percentage(u16::MAX).apply(100), 100);
// 0/0 intentionally avoids a panic by returning 0.
assert_eq!(Constraint::Ratio(0, 0).apply(100), 0);
// 1/0 intentionally avoids a panic by returning 100% of the length.
assert_eq!(Constraint::Ratio(1, 0).apply(100), 100);
assert_eq!(Constraint::Ratio(0, 1).apply(100), 0);
assert_eq!(Constraint::Ratio(1, 2).apply(100), 50);
assert_eq!(Constraint::Ratio(2, 2).apply(100), 100);
assert_eq!(Constraint::Ratio(3, 2).apply(100), 100);
assert_eq!(Constraint::Ratio(u32::MAX, 2).apply(100), 100);
assert_eq!(Constraint::Length(0).apply(100), 0);
assert_eq!(Constraint::Length(50).apply(100), 50);
assert_eq!(Constraint::Length(100).apply(100), 100);
assert_eq!(Constraint::Length(200).apply(100), 100);
assert_eq!(Constraint::Length(u16::MAX).apply(100), 100);
assert_eq!(Constraint::Max(0).apply(100), 0);
assert_eq!(Constraint::Max(50).apply(100), 50);
assert_eq!(Constraint::Max(100).apply(100), 100);
assert_eq!(Constraint::Max(200).apply(100), 100);
assert_eq!(Constraint::Max(u16::MAX).apply(100), 100);
assert_eq!(Constraint::Min(0).apply(100), 100);
assert_eq!(Constraint::Min(50).apply(100), 100);
assert_eq!(Constraint::Min(100).apply(100), 100);
assert_eq!(Constraint::Min(200).apply(100), 200);
assert_eq!(Constraint::Min(u16::MAX).apply(100), u16::MAX);
}
}

View File

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

View File

@@ -1,28 +0,0 @@
use strum::{Display, EnumString};
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
pub enum Direction {
Horizontal,
#[default]
Vertical,
}
#[cfg(test)]
mod tests {
use strum::ParseError;
use super::*;
#[test]
fn direction_to_string() {
assert_eq!(Direction::Horizontal.to_string(), "Horizontal");
assert_eq!(Direction::Vertical.to_string(), "Vertical");
}
#[test]
fn direction_from_str() {
assert_eq!("Horizontal".parse::<Direction>(), Ok(Direction::Horizontal));
assert_eq!("Vertical".parse::<Direction>(), Ok(Direction::Vertical));
assert_eq!("".parse::<Direction>(), Err(ParseError::VariantNotFound));
}
}

View File

@@ -1,250 +0,0 @@
use strum::{Display, EnumString};
#[allow(unused_imports)]
use super::constraint::Constraint;
/// Defines the options for layout flex justify content in a container.
///
/// This enumeration controls the distribution of space when layout constraints are met.
///
/// - `StretchLast`: Fills the available space within the container, putting excess space into the
/// last element.
/// - `Stretch`: Always fills the available space within the container.
/// - `Start`: Aligns items to the start of the container.
/// - `End`: Aligns items to the end of the container.
/// - `Center`: Centers items within the container.
/// - `SpaceBetween`: Adds excess space between each element.
/// - `SpaceAround`: Adds excess space around each element.
#[derive(Copy, Debug, Default, Display, EnumString, Clone, Eq, PartialEq, Hash)]
pub enum Flex {
/// Fills the available space within the container, putting excess space into the last
/// constraint of the lowest priority. This matches the default behavior of ratatui and tui
/// applications without [`Flex`]
///
/// The following examples illustrate the allocation of excess in various combinations of
/// constraints. As a refresher, the priorities of constraints are as follows:
///
/// 1. [`Constraint::Fixed`]
/// 2. [`Constraint::Min`] / [`Constraint::Max`]
/// 3. [`Constraint::Length`] / [`Constraint::Percentage`] / [`Constraint::Ratio`]
/// 4. [`Constraint::Proportional`]
///
/// When every constraint is `Length`, the last element gets the excess.
///
/// ```plain
/// <----------------------------------- 80 px ------------------------------------>
/// ┌──────20 px───────┐┌──────20 px───────┐┌────────────────40 px─────────────────┐
/// │ Length(20) ││ Length(20) ││ Length(20) │
/// └──────────────────┘└──────────────────┘└──────────────────────────────────────┘
/// ^^^^^^^^^^^^^^^^ EXCESS ^^^^^^^^^^^^^^^^
/// ```
///
/// If we replace the constraint at the end with a `Fixed`, because it has a
/// higher priority, the last constraint with the lowest priority, i.e. the last
/// `Length` gets the excess.
///
/// ```plain
/// <----------------------------------- 80 px ------------------------------------>
/// ┌──────20 px───────┐┌────────────────40 px─────────────────┐┌──────20 px───────┐
/// │ Length(20) ││ Length(20) ││ Fixed(20) │
/// └──────────────────┘└──────────────────────────────────────┘└──────────────────┘
/// ^^^^^^^^^^^^^^^^ EXCESS ^^^^^^^^^^^^^^^^
/// ```
///
/// Violating a `Max` is lower priority than `Fixed` but higher
/// than `Length`.
///
/// ```plain
/// <----------------------------------- 80 px ------------------------------------>
/// ┌────────────────40 px─────────────────┐┌──────20 px───────┐┌──────20 px───────┐
/// │ Length(20) ││ Max(20) ││ Fixed(20) │
/// └──────────────────────────────────────┘└──────────────────┘└──────────────────┘
/// ^^^^^^^^^^^^^^^^ EXCESS ^^^^^^^^^^^^^^^^
/// ```
///
/// It's important to note that while not violating a `Min` or `Max` constraint is
/// prioritized higher than a `Length`, `Min` and `Max` constraints allow for a range
/// of values and excess can (and will) be dumped into these ranges first, if possible,
/// even if it not the last constraint.
///
/// ```plain
/// <----------------------------------- 80 px ------------------------------------>
/// ┌──────20 px───────┐┌────────────────40 px─────────────────┐┌──────20 px───────┐
/// │ Length(20) ││ Min(20) ││ Fixed(20) │
/// └──────────────────┘└──────────────────────────────────────┘└──────────────────┘
/// ^^^^^^^^^^^^^^^^ EXCESS ^^^^^^^^^^^^^^^^
///
/// <----------------------------------- 80 px ------------------------------------>
/// ┌────────────────40 px─────────────────┐┌──────20 px───────┐┌──────20 px───────┐
/// │ Min(20) ││ Length(20) ││ Fixed(20) │
/// └──────────────────────────────────────┘└──────────────────┘└──────────────────┘
/// ^^^^^^^^^^^^^^^^ EXCESS ^^^^^^^^^^^^^^^^
/// ```
///
/// Proportional constraints have the lowest priority amongst all the constraints and hence
/// will always take up any excess space available.
///
/// ```plain
/// <----------------------------------- 80 px ------------------------------------>
/// ┌──────20 px───────┐┌──────20 px───────┐┌──────20 px───────┐┌──────20 px───────┐
/// │ Proportional(0) ││ Min(20) ││ Length(20) ││ Fixed(20) │
/// └──────────────────┘└──────────────────┘└──────────────────┘└──────────────────┘
/// ^^^^^^ EXCESS ^^^^^^
/// ```
///
/// # Examples
///
/// ```plain
/// <------------------------------------80 px------------------------------------->
/// ┌───────────30 px────────────┐┌───────────30 px────────────┐┌──────20 px───────┐
/// │ Percentage(20) ││ Length(20) ││ Fixed(20) │
/// └────────────────────────────┘└────────────────────────────┘└──────────────────┘
///
/// <------------------------------------80 px------------------------------------->
/// ┌──────────────────────────60 px───────────────────────────┐┌──────20 px───────┐
/// │ Min(20) ││ Max(20) │
/// └──────────────────────────────────────────────────────────┘└──────────────────┘
///
/// <------------------------------------80 px------------------------------------->
/// ┌────────────────────────────────────80 px─────────────────────────────────────┐
/// │ Max(20) │
/// └──────────────────────────────────────────────────────────────────────────────┘
/// ```
#[default]
StretchLast,
/// Always fills the available space within the container.
///
/// # Examples
///
/// ```plain
/// <------------------------------------80 px------------------------------------->
/// ┌────16 px─────┐┌──────────────────44 px───────────────────┐┌──────20 px───────┐
/// │Percentage(20)││ Length(20) ││ Fixed(20) │
/// └──────────────┘└──────────────────────────────────────────┘└──────────────────┘
///
/// <------------------------------------80 px------------------------------------->
/// ┌──────────────────────────60 px───────────────────────────┐┌──────20 px───────┐
/// │ Min(20) ││ Max(20) │
/// └──────────────────────────────────────────────────────────┘└──────────────────┘
///
/// <------------------------------------80 px------------------------------------->
/// ┌────────────────────────────────────80 px─────────────────────────────────────┐
/// │ Max(20) │
/// └──────────────────────────────────────────────────────────────────────────────┘
/// ```
Stretch,
/// Aligns items to the start of the container.
///
/// # Examples
///
/// ```plain
/// <------------------------------------80 px------------------------------------->
/// ┌────16 px─────┐┌──────20 px───────┐┌──────20 px───────┐
/// │Percentage(20)││ Length(20) ││ Fixed(20) │
/// └──────────────┘└──────────────────┘└──────────────────┘
///
/// <------------------------------------80 px------------------------------------->
/// ┌──────20 px───────┐┌──────20 px───────┐
/// │ Min(20) ││ Max(20) │
/// └──────────────────┘└──────────────────┘
///
/// <------------------------------------80 px------------------------------------->
/// ┌──────20 px───────┐
/// │ Max(20) │
/// └──────────────────┘
/// ```
Start,
/// Aligns items to the end of the container.
///
/// # Examples
///
/// ```plain
/// <------------------------------------80 px------------------------------------->
/// ┌────16 px─────┐┌──────20 px───────┐┌──────20 px───────┐
/// │Percentage(20)││ Length(20) ││ Fixed(20) │
/// └──────────────┘└──────────────────┘└──────────────────┘
///
/// <------------------------------------80 px------------------------------------->
/// ┌──────20 px───────┐┌──────20 px───────┐
/// │ Min(20) ││ Max(20) │
/// └──────────────────┘└──────────────────┘
///
/// <------------------------------------80 px------------------------------------->
/// ┌──────20 px───────┐
/// │ Max(20) │
/// └──────────────────┘
/// ```
End,
/// Centers items within the container.
///
/// # Examples
///
/// ```plain
/// <------------------------------------80 px------------------------------------->
/// ┌────16 px─────┐┌──────20 px───────┐┌──────20 px───────┐
/// │Percentage(20)││ Length(20) ││ Fixed(20) │
/// └──────────────┘└──────────────────┘└──────────────────┘
///
/// <------------------------------------80 px------------------------------------->
/// ┌──────20 px───────┐┌──────20 px───────┐
/// │ Min(20) ││ Max(20) │
/// └──────────────────┘└──────────────────┘
///
/// <------------------------------------80 px------------------------------------->
/// ┌──────20 px───────┐
/// │ Max(20) │
/// └──────────────────┘
/// ```
Center,
/// Adds excess space between each element.
///
/// # Examples
///
/// ```plain
///
/// <------------------------------------80 px------------------------------------->
/// ┌────16 px─────┐ ┌──────20 px───────┐ ┌──────20 px───────┐
/// │Percentage(20)│ │ Length(20) │ │ Fixed(20) │
/// └──────────────┘ └──────────────────┘ └──────────────────┘
///
/// <------------------------------------80 px------------------------------------->
/// ┌──────20 px───────┐ ┌──────20 px───────┐
/// │ Min(20) │ │ Max(20) │
/// └──────────────────┘ └──────────────────┘
///
/// <------------------------------------80 px------------------------------------->
/// ┌────────────────────────────────────80 px─────────────────────────────────────┐
/// │ Max(20) │
/// └──────────────────────────────────────────────────────────────────────────────┘
/// ```
SpaceBetween,
/// Adds excess space around each element.
///
/// # Examples
///
/// ```plain
/// <------------------------------------80 px------------------------------------->
/// ┌────16 px─────┐ ┌──────20 px───────┐ ┌──────20 px───────┐
/// │Percentage(20)│ │ Length(20) │ │ Fixed(20) │
/// └──────────────┘ └──────────────────┘ └──────────────────┘
///
/// <------------------------------------80 px------------------------------------->
/// ┌──────20 px───────┐ ┌──────20 px───────┐
/// │ Min(20) │ │ Max(20) │
/// └──────────────────┘ └──────────────────┘
///
/// <------------------------------------80 px------------------------------------->
/// ┌──────20 px───────┐
/// │ Max(20) │
/// └──────────────────┘
/// ```
SpaceAround,
}
#[cfg(test)]
mod tests {}

File diff suppressed because it is too large Load Diff

View File

@@ -1,43 +0,0 @@
use std::fmt::{self, Display};
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
pub struct Margin {
pub horizontal: u16,
pub vertical: u16,
}
impl Margin {
pub const fn new(horizontal: u16, vertical: u16) -> Margin {
Margin {
horizontal,
vertical,
}
}
}
impl Display for Margin {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}x{}", self.horizontal, self.vertical)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn margin_to_string() {
assert_eq!(Margin::new(1, 2).to_string(), "1x2");
}
#[test]
fn margin_new() {
assert_eq!(
Margin::new(1, 2),
Margin {
horizontal: 1,
vertical: 2
}
);
}
}

View File

@@ -1,97 +0,0 @@
#![warn(missing_docs)]
use crate::layout::Rect;
/// Position in the terminal
///
/// The position is relative to the top left corner of the terminal window, with the top left corner
/// being (0, 0). The x axis is horizontal increasing to the right, and the y axis is vertical
/// increasing downwards.
///
/// # Examples
///
/// ```
/// use ratatui::layout::{Position, Rect};
///
/// // the following are all equivalent
/// let position = Position { x: 1, y: 2 };
/// let position = Position::new(1, 2);
/// let position = Position::from((1, 2));
/// let position = Position::from(Rect::new(1, 2, 3, 4));
///
/// // position can be converted back into the components when needed
/// let (x, y) = position.into();
/// ```
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub struct Position {
/// The x coordinate of the position
///
/// The x coordinate is relative to the left edge of the terminal window, with the left edge
/// being 0.
pub x: u16,
/// The y coordinate of the position
///
/// The y coordinate is relative to the top edge of the terminal window, with the top edge
/// being 0.
pub y: u16,
}
impl Position {
/// Create a new position
pub fn new(x: u16, y: u16) -> Self {
Position { x, y }
}
}
impl From<(u16, u16)> for Position {
fn from((x, y): (u16, u16)) -> Self {
Position { x, y }
}
}
impl From<Position> for (u16, u16) {
fn from(position: Position) -> Self {
(position.x, position.y)
}
}
impl From<Rect> for Position {
fn from(rect: Rect) -> Self {
rect.as_position()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new() {
let position = Position::new(1, 2);
assert_eq!(position.x, 1);
assert_eq!(position.y, 2);
}
#[test]
fn from_tuple() {
let position = Position::from((1, 2));
assert_eq!(position.x, 1);
assert_eq!(position.y, 2);
}
#[test]
fn into_tuple() {
let position = Position::new(1, 2);
let (x, y) = position.into();
assert_eq!(x, 1);
assert_eq!(y, 2);
}
#[test]
fn from_rect() {
let rect = Rect::new(1, 2, 3, 4);
let position = Position::from(rect);
assert_eq!(position.x, 1);
assert_eq!(position.y, 2);
}
}

View File

@@ -4,11 +4,10 @@ use std::{
fmt,
};
use layout::{Position, Size};
use crate::prelude::*;
mod offset;
pub use offset::*;
/// A simple rectangle used in the computation of the layout and to give widgets a hint about the
@@ -26,58 +25,6 @@ pub struct Rect {
pub height: u16,
}
/// Manages row divisions within a `Rect`.
///
/// The `Rows` struct is an iterator that allows iterating through rows of a given `Rect`.
pub struct Rows {
/// The `Rect` associated with the rows.
pub rect: Rect,
/// The y coordinate of the row within the `Rect`.
pub current_row: u16,
}
impl Iterator for Rows {
type Item = Rect;
/// Retrieves the next row within the `Rect`.
///
/// Returns `None` when there are no more rows to iterate through.
fn next(&mut self) -> Option<Self::Item> {
if self.current_row >= self.rect.bottom() {
return None;
}
let row = Rect::new(self.rect.x, self.current_row, self.rect.width, 1);
self.current_row += 1;
Some(row)
}
}
/// Manages column divisions within a `Rect`.
///
/// The `Columns` struct is an iterator that allows iterating through columns of a given `Rect`.
pub struct Columns {
/// The `Rect` associated with the columns.
pub rect: Rect,
/// The x coordinate of the column within the `Rect`.
pub current_column: u16,
}
impl Iterator for Columns {
type Item = Rect;
/// Retrieves the next column within the `Rect`.
///
/// Returns `None` when there are no more columns to iterate through.
fn next(&mut self) -> Option<Self::Item> {
if self.current_column >= self.rect.right() {
return None;
}
let column = Rect::new(self.current_column, self.rect.y, 1, self.rect.height);
self.current_column += 1;
Some(column)
}
}
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)
@@ -220,153 +167,10 @@ impl Rect {
&& self.y < other.bottom()
&& self.bottom() > other.y
}
/// Split the rect into a number of sub-rects according to the given [`Layout`]`.
///
/// An ergonomic wrapper around [`Layout::split`] that returns an array of `Rect`s instead of
/// `Rc<[Rect]>`.
///
/// This method requires the number of constraints to be known at compile time. If you don't
/// know the number of constraints at compile time, use [`Layout::split`] instead.
///
/// # Panics
///
/// Panics if the number of constraints is not equal to the length of the returned array.
///
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// # fn render(frame: &mut Frame) {
/// let area = frame.size();
/// let layout = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
/// let [top, main] = area.split(&layout);
/// // or explicitly specify the number of constraints:
/// let rects = area.split::<2>(&layout);
/// # }
pub fn split<const N: usize>(self, layout: &Layout) -> [Rect; N] {
layout
.split(self)
.to_vec()
.try_into()
.expect("invalid number of rects")
}
/// Clamp this rect to fit inside the other rect.
///
/// If the width or height of this rect is larger than the other rect, it will be clamped to the
/// other rect's width or height.
///
/// If the left or top coordinate of this rect is smaller than the other rect, it will be
/// clamped to the other rect's left or top coordinate.
///
/// If the right or bottom coordinate of this rect is larger than the other rect, it will be
/// clamped to the other rect's right or bottom coordinate.
///
/// This is different from [`Rect::intersection`] because it will move this rect to fit inside
/// the other rect, while [`Rect::intersection`] instead would keep this rect's position and
/// truncate its size to only that which is inside the other rect.
///
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// # fn render(frame: &mut Frame) {
/// let area = frame.size();
/// let rect = Rect::new(0, 0, 100, 100).clamp(area);
/// # }
/// ```
pub fn clamp(self, other: Rect) -> Rect {
let width = self.width.min(other.width);
let height = self.height.min(other.height);
let x = self.x.clamp(other.x, other.right().saturating_sub(width));
let y = self.y.clamp(other.y, other.bottom().saturating_sub(height));
Rect::new(x, y, width, height)
}
/// Creates an iterator over rows within the `Rect`.
///
/// This method returns a `Rows` iterator that allows iterating through rows of the `Rect`.
///
/// # Examples
///
/// ```
/// use ratatui::prelude::*;
/// let area = Rect::new(0, 0, 10, 5);
/// for row in area.rows() {
/// // Perform operations on each row of the area
/// println!("Row: {:?}", row);
/// }
/// ```
pub fn rows(&self) -> Rows {
Rows {
rect: *self,
current_row: self.y,
}
}
/// Creates an iterator over columns within the `Rect`.
///
/// This method returns a `Columns` iterator that allows iterating through columns of the
/// `Rect`.
///
/// # Examples
///
/// ```
/// use ratatui::prelude::*;
/// let area = Rect::new(0, 0, 10, 5);
/// for column in area.columns() {
/// // Perform operations on each column of the area
/// println!("Column: {:?}", column);
/// }
/// ```
pub fn columns(&self) -> Columns {
Columns {
rect: *self,
current_column: self.x,
}
}
/// Returns a [`Position`] with the same coordinates as this rect.
///
/// # Examples
///
/// ```
/// # use ratatui::prelude::*;
/// let rect = Rect::new(1, 2, 3, 4);
/// let position = rect.as_position();
/// ````
pub fn as_position(self) -> Position {
Position {
x: self.x,
y: self.y,
}
}
/// Converts the rect into a size struct.
pub fn as_size(self) -> Size {
Size {
width: self.width,
height: self.height,
}
}
}
impl From<(Position, Size)> for Rect {
fn from((position, size): (Position, Size)) -> Self {
Rect {
x: position.x,
y: position.y,
width: size.width,
height: size.height,
}
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[test]
@@ -549,98 +353,4 @@ mod tests {
const _BOTTOM: u16 = RECT.bottom();
assert!(RECT.intersects(RECT));
}
#[test]
fn split() {
let layout = Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]);
let [a, b] = Rect::new(0, 0, 2, 1).split(&layout);
assert_eq!(a, Rect::new(0, 0, 1, 1));
assert_eq!(b, Rect::new(1, 0, 1, 1));
}
#[test]
#[should_panic(expected = "invalid number of rects")]
fn split_invalid_number_of_recs() {
let layout = Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]);
let [_a, _b, _c] = Rect::new(0, 0, 2, 1).split(&layout);
}
#[rstest]
#[case::inside(Rect::new(20, 20, 10, 10), Rect::new(20, 20, 10, 10))]
#[case::up_left(Rect::new(5, 5, 10, 10), Rect::new(10, 10, 10, 10))]
#[case::up(Rect::new(20, 5, 10, 10), Rect::new(20, 10, 10, 10))]
#[case::up_right(Rect::new(105, 5, 10, 10), Rect::new(100, 10, 10, 10))]
#[case::left(Rect::new(5, 20, 10, 10), Rect::new(10, 20, 10, 10))]
#[case::right(Rect::new(105, 20, 10, 10), Rect::new(100, 20, 10, 10))]
#[case::down_left(Rect::new(5, 105, 10, 10), Rect::new(10, 100, 10, 10))]
#[case::down(Rect::new(20, 105, 10, 10), Rect::new(20, 100, 10, 10))]
#[case::down_right(Rect::new(105, 105, 10, 10), Rect::new(100, 100, 10, 10))]
#[case::too_wide(Rect::new(5, 20, 200, 10), Rect::new(10, 20, 100, 10))]
#[case::too_tall(Rect::new(20, 5, 10, 200), Rect::new(20, 10, 10, 100))]
#[case::too_large(Rect::new(0, 0, 200, 200), Rect::new(10, 10, 100, 100))]
fn clamp(#[case] rect: Rect, #[case] expected: Rect) {
let other = Rect::new(10, 10, 100, 100);
assert_eq!(rect.clamp(other), expected);
}
#[test]
fn rows() {
let area = Rect::new(0, 0, 3, 2);
let rows: Vec<Rect> = area.rows().collect();
let expected_rows: Vec<Rect> = vec![Rect::new(0, 0, 3, 1), Rect::new(0, 1, 3, 1)];
assert_eq!(rows, expected_rows);
}
#[test]
fn columns() {
let area = Rect::new(0, 0, 3, 2);
let columns: Vec<Rect> = area.columns().collect();
let expected_columns: Vec<Rect> = vec![
Rect::new(0, 0, 1, 2),
Rect::new(1, 0, 1, 2),
Rect::new(2, 0, 1, 2),
];
assert_eq!(columns, expected_columns);
}
#[test]
fn as_position() {
let rect = Rect::new(1, 2, 3, 4);
let position = rect.as_position();
assert_eq!(position.x, 1);
assert_eq!(position.y, 2);
}
#[test]
fn as_size() {
assert_eq!(
Rect::new(1, 2, 3, 4).as_size(),
Size {
width: 3,
height: 4
}
);
}
#[test]
fn from_position_and_size() {
let position = Position { x: 1, y: 2 };
let size = Size {
width: 3,
height: 4,
};
assert_eq!(
Rect::from((position, size)),
Rect {
x: 1,
y: 2,
width: 3,
height: 4
}
);
}
}

View File

@@ -1,147 +0,0 @@
use strum::{Display, EnumString};
/// Option for segment size preferences
///
/// This controls how the space is distributed when the constraints are satisfied. By default, the
/// last chunk is expanded to fill the remaining space, but this can be changed to prefer equal
/// chunks or to not distribute extra space at all (which is the default used for laying out the
/// columns for [`Table`] widgets).
///
/// Note: If you're using this feature please help us come up with a good name. See [Issue
/// #536](https://github.com/ratatui-org/ratatui/issues/536) for more information.
///
/// [`Table`]: crate::widgets::Table
#[stability::unstable(
feature = "segment-size",
reason = "The name for this feature is not final and may change in the future",
issue = "https://github.com/ratatui-org/ratatui/issues/536"
)]
#[derive(Copy, Debug, Default, Display, EnumString, Clone, Eq, PartialEq, Hash)]
pub enum SegmentSize {
/// prefer equal chunks if other constraints are all satisfied
EvenDistribution,
/// the last chunk is expanded to fill the remaining space
#[default]
LastTakesRemainder,
/// extra space is not distributed
None,
}
#[cfg(test)]
mod tests {
use strum::ParseError;
use super::{SegmentSize::*, *};
use crate::prelude::{Constraint::*, *};
#[test]
fn segment_size_to_string() {
assert_eq!(EvenDistribution.to_string(), "EvenDistribution");
assert_eq!(LastTakesRemainder.to_string(), "LastTakesRemainder");
assert_eq!(None.to_string(), "None");
}
#[test]
fn segment_size_from_string() {
assert_eq!(
"EvenDistribution".parse::<SegmentSize>(),
Ok(EvenDistribution)
);
assert_eq!(
"LastTakesRemainder".parse::<SegmentSize>(),
Ok(LastTakesRemainder)
);
assert_eq!("None".parse::<SegmentSize>(), Ok(None));
assert_eq!("".parse::<SegmentSize>(), Err(ParseError::VariantNotFound));
}
fn get_x_width_with_segment_size(
segment_size: SegmentSize,
constraints: Vec<Constraint>,
target: Rect,
) -> Vec<(u16, u16)> {
#[allow(deprecated)]
let layout = Layout::default()
.direction(Direction::Horizontal)
.constraints(constraints)
.segment_size(segment_size);
let chunks = layout.split(target);
chunks.iter().map(|r| (r.x, r.width)).collect()
}
#[test]
fn test_split_equally_in_underspecified_case() {
let target = Rect::new(100, 200, 10, 10);
assert_eq!(
get_x_width_with_segment_size(LastTakesRemainder, vec![Min(2), Min(2), Min(0)], target),
[(100, 2), (102, 2), (104, 6)]
);
assert_eq!(
get_x_width_with_segment_size(EvenDistribution, vec![Min(2), Min(2), Min(0)], target),
[(100, 3), (103, 4), (107, 3)]
);
}
#[test]
fn test_split_equally_in_overconstrained_case_for_min() {
let target = Rect::new(100, 200, 100, 10);
assert_eq!(
get_x_width_with_segment_size(
LastTakesRemainder,
vec![Percentage(50), Min(10), Percentage(50)],
target
),
[(100, 50), (150, 10), (160, 40)]
);
assert_eq!(
get_x_width_with_segment_size(
EvenDistribution,
vec![Percentage(50), Min(10), Percentage(50)],
target
),
[(100, 45), (145, 10), (155, 45)]
);
}
#[test]
fn test_split_equally_in_overconstrained_case_for_max() {
let target = Rect::new(100, 200, 100, 10);
assert_eq!(
get_x_width_with_segment_size(
LastTakesRemainder,
vec![Percentage(30), Max(10), Percentage(30)],
target
),
[(100, 30), (130, 10), (140, 60)]
);
assert_eq!(
get_x_width_with_segment_size(
EvenDistribution,
vec![Percentage(30), Max(10), Percentage(30)],
target
),
[(100, 45), (145, 10), (155, 45)]
);
}
#[test]
fn test_split_equally_in_overconstrained_case_for_length() {
let target = Rect::new(100, 200, 100, 10);
assert_eq!(
get_x_width_with_segment_size(
LastTakesRemainder,
vec![Percentage(50), Length(10), Percentage(50)],
target
),
[(100, 50), (150, 10), (160, 40)]
);
assert_eq!(
get_x_width_with_segment_size(
EvenDistribution,
vec![Percentage(50), Length(10), Percentage(50)],
target
),
[(100, 45), (145, 10), (155, 45)]
);
}
}

View File

@@ -1,59 +0,0 @@
#![warn(missing_docs)]
use crate::prelude::*;
/// A simple size struct
///
/// The width and height are stored as `u16` values and represent the number of columns and rows
/// respectively.
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
pub struct Size {
/// The width in columns
pub width: u16,
/// The height in rows
pub height: u16,
}
impl Size {
/// Create a new `Size` struct
pub fn new(width: u16, height: u16) -> Self {
Size { width, height }
}
}
impl From<(u16, u16)> for Size {
fn from((width, height): (u16, u16)) -> Self {
Size { width, height }
}
}
impl From<Rect> for Size {
fn from(rect: Rect) -> Self {
rect.as_size()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new() {
let size = Size::new(10, 20);
assert_eq!(size.width, 10);
assert_eq!(size.height, 20);
}
#[test]
fn from_tuple() {
let size = Size::from((10, 20));
assert_eq!(size.width, 10);
assert_eq!(size.height, 20);
}
#[test]
fn from_rect() {
let size = Size::from(Rect::new(0, 0, 10, 20));
assert_eq!(size.width, 10);
assert_eq!(size.height, 20);
}
}

View File

@@ -1,24 +1,32 @@
#![forbid(unsafe_code)]
//! ![Demo](https://github.com/ratatui-org/ratatui/blob/1d39444e3dea6f309cf9035be2417ac711c1abc9/examples/demo2-destroy.gif?raw=true)
//! ![Demo](https://raw.githubusercontent.com/ratatui-org/ratatui/b33c878808c4c40591d7a2d9f9d94d6fee95a96f/examples/demo2.gif)
//!
//! <div align="center">
//!
//! [![Crate Badge]][Crate] [![Docs Badge]][API Docs] [![CI Badge]][CI Workflow] [![License
//! Badge]](./LICENSE)<br>
//! [![Codecov Badge]][Codecov] [![Deps.rs Badge]][Deps.rs] [![Discord Badge]][Discord Server]
//! [![Matrix Badge]][Matrix]<br>
//! [![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>
//!
//! [Ratatui Website] · [API Docs] · [Examples] · [Changelog] · [Breaking Changes]<br>
//! [Contributing] · [Report a bug] · [Request a Feature] · [Create a Pull Request]
//! [Documentation](https://docs.rs/ratatui)
//! · [Ratatui Website](https://ratatui.rs)
//! · [Examples](https://github.com/ratatui-org/ratatui/tree/main/examples)
//! · [Report a bug](https://github.com/ratatui-org/ratatui/issues/new?labels=bug&projects=&template=bug_report.md)
//! · [Request a Feature](https://github.com/ratatui-org/ratatui/issues/new?labels=enhancement&projects=&template=feature_request.md)
//! · [Send a Pull Request](https://github.com/ratatui-org/ratatui/compare)
//!
//! </div>
//!
//! # Ratatui
//!
//! [Ratatui][Ratatui Website] 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.
//! [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
//!
@@ -43,20 +51,22 @@
//! ## Other documentation
//!
//! - [Ratatui Website] - explains the library's concepts and provides step-by-step tutorials
//! - [API Docs] - the full API documentation for the library on docs.rs.
//! - [Examples] - a collection of examples that demonstrate how to use the library.
//! - [Contributing] - Please read this if you are interested in contributing to the project.
//! - [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
//! the [Examples] directory. For more guidance on different ways to structure your application see
//! the [Application Patterns] and [Hello World tutorial] sections in the [Ratatui Website] and the
//! various [Examples]. There are also several starter templates available in the [templates]
//! repository.
//! [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 Website] and the
//! various [Examples]. There are also several starter templates available:
//!
//! - [template]
//! - [async-template] (book and template)
//!
//! Every application built with `ratatui` needs to implement the following steps:
//!
@@ -287,14 +297,12 @@
//! [Handling Events]: https://ratatui.rs/concepts/event-handling/
//! [Layout]: https://ratatui.rs/how-to/layout/
//! [Styling Text]: https://ratatui.rs/how-to/render/style-text/
//! [templates]: https://github.com/ratatui-org/templates/
//! [Examples]: https://github.com/ratatui-org/ratatui/tree/main/examples/README.md
//! [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
//! [Create a Pull Request]: https://github.com/ratatui-org/ratatui/compare
//! [template]: https://github.com/ratatui-org/template
//! [async-template]: https://ratatui-org.github.io/async-template
//! [Examples]: https://github.com/ratatui-org/ratatui/tree/main/examples
//! [git-cliff]: https://git-cliff.org
//! [Conventional Commits]: https://www.conventionalcommits.org
//! [API Docs]: https://docs.rs/ratatui
//! [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
@@ -314,28 +322,24 @@
//! [`Backend`]: backend::Backend
//! [`backend` module]: backend
//! [`crossterm::event`]: https://docs.rs/crossterm/latest/crossterm/event/index.html
//! [Crate]: https://crates.io/crates/ratatui
//! [Ratatui]: https://ratatui.rs
//! [Crossterm]: https://crates.io/crates/crossterm
//! [Termion]: https://crates.io/crates/termion
//! [Termwiz]: https://crates.io/crates/termwiz
//! [tui-rs]: 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
//! [License Badge]: https://img.shields.io/crates/l/ratatui?style=flat-square
//! [CI Badge]:
//! https://img.shields.io/github/actions/workflow/status/ratatui-org/ratatui/ci.yml?style=flat-square&logo=github
//! [CI Workflow]: https://github.com/ratatui-org/ratatui/actions/workflows/ci.yml
//! [Codecov Badge]:
//! https://img.shields.io/codecov/c/github/ratatui-org/ratatui?logo=codecov&style=flat-square&token=BAQ8SOKEST
//! [Codecov]: https://app.codecov.io/gh/ratatui-org/ratatui
//! [Deps.rs Badge]: https://deps.rs/repo/github/ratatui-org/ratatui/status.svg?style=flat-square
//! [Deps.rs]: https://deps.rs/repo/github/ratatui-org/ratatui
//! [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
//! [Discord Server]: https://discord.gg/pMCEU9hNEj
//! [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
//! [Matrix]: https://matrix.to/#/#ratatui:matrix.org
// show the feature flags in the generated documentation
#![cfg_attr(docsrs, feature(doc_auto_cfg))]

View File

@@ -72,12 +72,10 @@ use std::fmt::{self, Debug};
use bitflags::bitflags;
mod color;
mod stylize;
pub use color::Color;
pub use stylize::{Styled, Stylize};
pub mod palette;
mod color;
pub use color::Color;
bitflags! {
/// Modifier changes the way a piece of text is displayed.

View File

@@ -127,18 +127,6 @@ pub enum Color {
Indexed(u8),
}
impl Color {
/// Convert a u32 to a Color
///
/// The u32 should be in the format 0x00RRGGBB.
pub const fn from_u32(u: u32) -> Color {
let r = (u >> 16) as u8;
let g = (u >> 8) as u8;
let b = u as u8;
Color::Rgb(r, g, b)
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for Color {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
@@ -282,15 +270,6 @@ mod tests {
use super::*;
#[test]
fn from_u32() {
assert_eq!(Color::from_u32(0x000000), Color::Rgb(0, 0, 0));
assert_eq!(Color::from_u32(0xFF0000), Color::Rgb(255, 0, 0));
assert_eq!(Color::from_u32(0x00FF00), Color::Rgb(0, 255, 0));
assert_eq!(Color::from_u32(0x0000FF), Color::Rgb(0, 0, 255));
assert_eq!(Color::from_u32(0xFFFFFF), Color::Rgb(255, 255, 255));
}
#[test]
fn from_rgb_color() {
let color: Color = Color::from_str("#FF0000").unwrap();

View File

@@ -1,4 +0,0 @@
//! A module for defining color palettes.
pub mod material;
pub mod tailwind;

View File

@@ -1,606 +0,0 @@
//! Material design color palettes.
//!
//! Represents the colors from the 2014 [Material design color palettes][palettes] by Google.
//!
//! [palettes]: https://m2.material.io/design/color/the-color-system.html#tools-for-picking-colors
//!
//! There are 16 palettes with accent colors, and 3 palettes without accent colors. Each palette
//! has 10 colors, with variants from 50 to 900. The accent palettes also have 4 accent colors
//! with variants from 100 to 700. Black and White are also included for completeness and to avoid
//! being affected by any terminal theme that might be in use.
//!
//! This module exists to provide a convenient way to use the colors from the
//! [`matdesign-color` crate] in your application.
//!
//! <style>
//! .color { display: flex; align-items: center; }
//! .color > div { width: 2rem; height: 2rem; }
//! .color > div.name { width: 150px; !important; }
//! </style>
//! <div style="overflow-x: auto">
//! <div style="display: flex; flex-direction:column; text-align: left">
//! <div class="color" style="font-size:0.8em">
//! <div class="name"></div>
//! <div>C50</div>
//! <div>C100</div>
//! <div>C200</div>
//! <div>C300</div>
//! <div>C400</div>
//! <div>C500</div>
//! <div>C600</div>
//! <div>C700</div>
//! <div>C800</div>
//! <div>C900</div>
//! <div>A100</div>
//! <div>A200</div>
//! <div>A400</div>
//! <div>A700</div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`RED`]</div>
//! <div style="background-color: #FFEBEE"></div>
//! <div style="background-color: #FFCDD2"></div>
//! <div style="background-color: #EF9A9A"></div>
//! <div style="background-color: #E57373"></div>
//! <div style="background-color: #EF5350"></div>
//! <div style="background-color: #F44336"></div>
//! <div style="background-color: #E53935"></div>
//! <div style="background-color: #D32F2F"></div>
//! <div style="background-color: #C62828"></div>
//! <div style="background-color: #B71C1C"></div>
//! <div style="background-color: #FF8A80"></div>
//! <div style="background-color: #FF5252"></div>
//! <div style="background-color: #FF1744"></div>
//! <div style="background-color: #D50000"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`PINK`]</div>
//! <div style="background-color: #FCE4EC"></div>
//! <div style="background-color: #F8BBD0"></div>
//! <div style="background-color: #F48FB1"></div>
//! <div style="background-color: #F06292"></div>
//! <div style="background-color: #EC407A"></div>
//! <div style="background-color: #E91E63"></div>
//! <div style="background-color: #D81B60"></div>
//! <div style="background-color: #C2185B"></div>
//! <div style="background-color: #AD1457"></div>
//! <div style="background-color: #880E4F"></div>
//! <div style="background-color: #FF80AB"></div>
//! <div style="background-color: #FF4081"></div>
//! <div style="background-color: #F50057"></div>
//! <div style="background-color: #C51162"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`PURPLE`]</div>
//! <div style="background-color: #F3E5F5"></div>
//! <div style="background-color: #E1BEE7"></div>
//! <div style="background-color: #CE93D8"></div>
//! <div style="background-color: #BA68C8"></div>
//! <div style="background-color: #AB47BC"></div>
//! <div style="background-color: #9C27B0"></div>
//! <div style="background-color: #8E24AA"></div>
//! <div style="background-color: #7B1FA2"></div>
//! <div style="background-color: #6A1B9A"></div>
//! <div style="background-color: #4A148C"></div>
//! <div style="background-color: #EA80FC"></div>
//! <div style="background-color: #E040FB"></div>
//! <div style="background-color: #D500F9"></div>
//! <div style="background-color: #AA00FF"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`DEEP_PURPLE`]</div>
//! <div style="background-color: #EDE7F6"></div>
//! <div style="background-color: #D1C4E9"></div>
//! <div style="background-color: #B39DDB"></div>
//! <div style="background-color: #9575CD"></div>
//! <div style="background-color: #7E57C2"></div>
//! <div style="background-color: #673AB7"></div>
//! <div style="background-color: #5E35B1"></div>
//! <div style="background-color: #512DA8"></div>
//! <div style="background-color: #4527A0"></div>
//! <div style="background-color: #311B92"></div>
//! <div style="background-color: #B388FF"></div>
//! <div style="background-color: #7C4DFF"></div>
//! <div style="background-color: #651FFF"></div>
//! <div style="background-color: #6200EA"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`INDIGO`]</div>
//! <div style="background-color: #E8EAF6"></div>
//! <div style="background-color: #C5CAE9"></div>
//! <div style="background-color: #9FA8DA"></div>
//! <div style="background-color: #7986CB"></div>
//! <div style="background-color: #5C6BC0"></div>
//! <div style="background-color: #3F51B5"></div>
//! <div style="background-color: #3949AB"></div>
//! <div style="background-color: #303F9F"></div>
//! <div style="background-color: #283593"></div>
//! <div style="background-color: #1A237E"></div>
//! <div style="background-color: #8C9EFF"></div>
//! <div style="background-color: #536DFE"></div>
//! <div style="background-color: #3D5AFE"></div>
//! <div style="background-color: #304FFE"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`BLUE`]</div>
//! <div style="background-color: #E3F2FD"></div>
//! <div style="background-color: #BBDEFB"></div>
//! <div style="background-color: #90CAF9"></div>
//! <div style="background-color: #64B5F6"></div>
//! <div style="background-color: #42A5F5"></div>
//! <div style="background-color: #2196F3"></div>
//! <div style="background-color: #1E88E5"></div>
//! <div style="background-color: #1976D2"></div>
//! <div style="background-color: #1565C0"></div>
//! <div style="background-color: #0D47A1"></div>
//! <div style="background-color: #82B1FF"></div>
//! <div style="background-color: #448AFF"></div>
//! <div style="background-color: #2979FF"></div>
//! <div style="background-color: #2962FF"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`LIGHT_BLUE`]</div>
//! <div style="background-color: #E1F5FE"></div>
//! <div style="background-color: #B3E5FC"></div>
//! <div style="background-color: #81D4FA"></div>
//! <div style="background-color: #4FC3F7"></div>
//! <div style="background-color: #29B6F6"></div>
//! <div style="background-color: #03A9F4"></div>
//! <div style="background-color: #039BE5"></div>
//! <div style="background-color: #0288D1"></div>
//! <div style="background-color: #0277BD"></div>
//! <div style="background-color: #01579B"></div>
//! <div style="background-color: #80D8FF"></div>
//! <div style="background-color: #40C4FF"></div>
//! <div style="background-color: #00B0FF"></div>
//! <div style="background-color: #0091EA"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`CYAN`]</div>
//! <div style="background-color: #E0F7FA"></div>
//! <div style="background-color: #B2EBF2"></div>
//! <div style="background-color: #80DEEA"></div>
//! <div style="background-color: #4DD0E1"></div>
//! <div style="background-color: #26C6DA"></div>
//! <div style="background-color: #00BCD4"></div>
//! <div style="background-color: #00ACC1"></div>
//! <div style="background-color: #0097A7"></div>
//! <div style="background-color: #00838F"></div>
//! <div style="background-color: #006064"></div>
//! <div style="background-color: #84FFFF"></div>
//! <div style="background-color: #18FFFF"></div>
//! <div style="background-color: #00E5FF"></div>
//! <div style="background-color: #00B8D4"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`TEAL`]</div>
//! <div style="background-color: #E0F2F1"></div>
//! <div style="background-color: #B2DFDB"></div>
//! <div style="background-color: #80CBC4"></div>
//! <div style="background-color: #4DB6AC"></div>
//! <div style="background-color: #26A69A"></div>
//! <div style="background-color: #009688"></div>
//! <div style="background-color: #00897B"></div>
//! <div style="background-color: #00796B"></div>
//! <div style="background-color: #00695C"></div>
//! <div style="background-color: #004D40"></div>
//! <div style="background-color: #A7FFEB"></div>
//! <div style="background-color: #64FFDA"></div>
//! <div style="background-color: #1DE9B6"></div>
//! <div style="background-color: #00BFA5"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`GREEN`]</div>
//! <div style="background-color: #E8F5E9"></div>
//! <div style="background-color: #C8E6C9"></div>
//! <div style="background-color: #A5D6A7"></div>
//! <div style="background-color: #81C784"></div>
//! <div style="background-color: #66BB6A"></div>
//! <div style="background-color: #4CAF50"></div>
//! <div style="background-color: #43A047"></div>
//! <div style="background-color: #388E3C"></div>
//! <div style="background-color: #2E7D32"></div>
//! <div style="background-color: #1B5E20"></div>
//! <div style="background-color: #B9F6CA"></div>
//! <div style="background-color: #69F0AE"></div>
//! <div style="background-color: #00E676"></div>
//! <div style="background-color: #00C853"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`LIGHT_GREEN`]</div>
//! <div style="background-color: #F1F8E9"></div>
//! <div style="background-color: #DCEDC8"></div>
//! <div style="background-color: #C5E1A5"></div>
//! <div style="background-color: #AED581"></div>
//! <div style="background-color: #9CCC65"></div>
//! <div style="background-color: #8BC34A"></div>
//! <div style="background-color: #7CB342"></div>
//! <div style="background-color: #689F38"></div>
//! <div style="background-color: #558B2F"></div>
//! <div style="background-color: #33691E"></div>
//! <div style="background-color: #CCFF90"></div>
//! <div style="background-color: #B2FF59"></div>
//! <div style="background-color: #76FF03"></div>
//! <div style="background-color: #64DD17"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`LIME`]</div>
//! <div style="background-color: #F9FBE7"></div>
//! <div style="background-color: #F0F4C3"></div>
//! <div style="background-color: #E6EE9C"></div>
//! <div style="background-color: #DCE775"></div>
//! <div style="background-color: #D4E157"></div>
//! <div style="background-color: #CDDC39"></div>
//! <div style="background-color: #C0CA33"></div>
//! <div style="background-color: #AFB42B"></div>
//! <div style="background-color: #9E9D24"></div>
//! <div style="background-color: #827717"></div>
//! <div style="background-color: #F4FF81"></div>
//! <div style="background-color: #EEFF41"></div>
//! <div style="background-color: #C6FF00"></div>
//! <div style="background-color: #AEEA00"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`YELLOW`]</div>
//! <div style="background-color: #FFFDE7"></div>
//! <div style="background-color: #FFF9C4"></div>
//! <div style="background-color: #FFF59D"></div>
//! <div style="background-color: #FFF176"></div>
//! <div style="background-color: #FFEE58"></div>
//! <div style="background-color: #FFEB3B"></div>
//! <div style="background-color: #FDD835"></div>
//! <div style="background-color: #FBC02D"></div>
//! <div style="background-color: #F9A825"></div>
//! <div style="background-color: #F57F17"></div>
//! <div style="background-color: #FFFF8D"></div>
//! <div style="background-color: #FFFF00"></div>
//! <div style="background-color: #FFEA00"></div>
//! <div style="background-color: #FFD600"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`AMBER`]</div>
//! <div style="background-color: #FFF8E1"></div>
//! <div style="background-color: #FFECB3"></div>
//! <div style="background-color: #FFE082"></div>
//! <div style="background-color: #FFD54F"></div>
//! <div style="background-color: #FFCA28"></div>
//! <div style="background-color: #FFC107"></div>
//! <div style="background-color: #FFB300"></div>
//! <div style="background-color: #FFA000"></div>
//! <div style="background-color: #FF8F00"></div>
//! <div style="background-color: #FF6F00"></div>
//! <div style="background-color: #FFE57F"></div>
//! <div style="background-color: #FFD740"></div>
//! <div style="background-color: #FFC400"></div>
//! <div style="background-color: #FFAB00"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`ORANGE`]</div>
//! <div style="background-color: #FFF3E0"></div>
//! <div style="background-color: #FFE0B2"></div>
//! <div style="background-color: #FFCC80"></div>
//! <div style="background-color: #FFB74D"></div>
//! <div style="background-color: #FFA726"></div>
//! <div style="background-color: #FF9800"></div>
//! <div style="background-color: #FB8C00"></div>
//! <div style="background-color: #F57C00"></div>
//! <div style="background-color: #EF6C00"></div>
//! <div style="background-color: #E65100"></div>
//! <div style="background-color: #FFD180"></div>
//! <div style="background-color: #FFAB40"></div>
//! <div style="background-color: #FF9100"></div>
//! <div style="background-color: #FF6D00"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`DEEP_ORANGE`]</div>
//! <div style="background-color: #FBE9E7"></div>
//! <div style="background-color: #FFCCBC"></div>
//! <div style="background-color: #FFAB91"></div>
//! <div style="background-color: #FF8A65"></div>
//! <div style="background-color: #FF7043"></div>
//! <div style="background-color: #FF5722"></div>
//! <div style="background-color: #F4511E"></div>
//! <div style="background-color: #E64A19"></div>
//! <div style="background-color: #D84315"></div>
//! <div style="background-color: #BF360C"></div>
//! <div style="background-color: #FF9E80"></div>
//! <div style="background-color: #FF6E40"></div>
//! <div style="background-color: #FF3D00"></div>
//! <div style="background-color: #DD2C00"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`BROWN`]</div>
//! <div style="background-color: #EFEBE9"></div>
//! <div style="background-color: #D7CCC8"></div>
//! <div style="background-color: #BCAAA4"></div>
//! <div style="background-color: #A1887F"></div>
//! <div style="background-color: #8D6E63"></div>
//! <div style="background-color: #795548"></div>
//! <div style="background-color: #6D4C41"></div>
//! <div style="background-color: #5D4037"></div>
//! <div style="background-color: #4E342E"></div>
//! <div style="background-color: #3E2723"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`GRAY`]</div>
//! <div style="background-color: #FAFAFA"></div>
//! <div style="background-color: #F5F5F5"></div>
//! <div style="background-color: #EEEEEE"></div>
//! <div style="background-color: #E0E0E0"></div>
//! <div style="background-color: #BDBDBD"></div>
//! <div style="background-color: #9E9E9E"></div>
//! <div style="background-color: #757575"></div>
//! <div style="background-color: #616161"></div>
//! <div style="background-color: #424242"></div>
//! <div style="background-color: #212121"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`BLUE_GRAY`]</div>
//! <div style="background-color: #ECEFF1"></div>
//! <div style="background-color: #CFD8DC"></div>
//! <div style="background-color: #B0BEC5"></div>
//! <div style="background-color: #90A4AE"></div>
//! <div style="background-color: #78909C"></div>
//! <div style="background-color: #607D8B"></div>
//! <div style="background-color: #546E7A"></div>
//! <div style="background-color: #455A64"></div>
//! <div style="background-color: #37474F"></div>
//! <div style="background-color: #263238"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`BLACK`]</div>
//! <div class="bw" style="width: 350px; background-color: #000000"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`WHITE`]</div>
//! <div style="width: 350px; background-color: #FFFFFF"></div>
//! </div>
//! </div>
//! </div>
//!
//! # Example
//!
//! ```rust
//! # use ratatui::prelude::*;
//! use ratatui::style::palette::material::{BLUE, RED};
//!
//! assert_eq!(RED.c500, Color::Rgb(244, 67, 54));
//! assert_eq!(BLUE.c500, Color::Rgb(33, 150, 243));
//! ```
//!
//! [`matdesign-color` crate]: https://crates.io/crates/matdesign-color
use crate::prelude::*;
/// A palette of colors for use in Material design with accent colors
///
/// This is a collection of colors that are used in Material design. They consist of a set of
/// colors from 50 to 900, and a set of accent colors from 100 to 700.
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub struct AccentedPalette {
pub c50: Color,
pub c100: Color,
pub c200: Color,
pub c300: Color,
pub c400: Color,
pub c500: Color,
pub c600: Color,
pub c700: Color,
pub c800: Color,
pub c900: Color,
pub a100: Color,
pub a200: Color,
pub a400: Color,
pub a700: Color,
}
/// A palette of colors for use in Material design without accent colors
///
/// This is a collection of colors that are used in Material design. They consist of a set of
/// colors from 50 to 900.
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub struct NonAccentedPalette {
pub c50: Color,
pub c100: Color,
pub c200: Color,
pub c300: Color,
pub c400: Color,
pub c500: Color,
pub c600: Color,
pub c700: Color,
pub c800: Color,
pub c900: Color,
}
impl AccentedPalette {
/// Create a new AccentedPalette from the given variants
///
/// The variants should be in the format [0x00RRGGBB, ...]
pub const fn from_variants(variants: [u32; 14]) -> AccentedPalette {
AccentedPalette {
c50: Color::from_u32(variants[0]),
c100: Color::from_u32(variants[1]),
c200: Color::from_u32(variants[2]),
c300: Color::from_u32(variants[3]),
c400: Color::from_u32(variants[4]),
c500: Color::from_u32(variants[5]),
c600: Color::from_u32(variants[6]),
c700: Color::from_u32(variants[7]),
c800: Color::from_u32(variants[8]),
c900: Color::from_u32(variants[9]),
a100: Color::from_u32(variants[10]),
a200: Color::from_u32(variants[11]),
a400: Color::from_u32(variants[12]),
a700: Color::from_u32(variants[13]),
}
}
}
impl NonAccentedPalette {
/// Create a new NonAccented from the given variants
///
/// The variants should be in the format [0x00RRGGBB, ...]
pub const fn from_variants(variants: [u32; 10]) -> NonAccentedPalette {
NonAccentedPalette {
c50: Color::from_u32(variants[0]),
c100: Color::from_u32(variants[1]),
c200: Color::from_u32(variants[2]),
c300: Color::from_u32(variants[3]),
c400: Color::from_u32(variants[4]),
c500: Color::from_u32(variants[5]),
c600: Color::from_u32(variants[6]),
c700: Color::from_u32(variants[7]),
c800: Color::from_u32(variants[8]),
c900: Color::from_u32(variants[9]),
}
}
}
// Accented palettes
pub const RED: AccentedPalette = AccentedPalette::from_variants(variants::RED);
pub const PINK: AccentedPalette = AccentedPalette::from_variants(variants::PINK);
pub const PURPLE: AccentedPalette = AccentedPalette::from_variants(variants::PURPLE);
pub const DEEP_PURPLE: AccentedPalette = AccentedPalette::from_variants(variants::DEEP_PURPLE);
pub const INDIGO: AccentedPalette = AccentedPalette::from_variants(variants::INDIGO);
pub const BLUE: AccentedPalette = AccentedPalette::from_variants(variants::BLUE);
pub const LIGHT_BLUE: AccentedPalette = AccentedPalette::from_variants(variants::LIGHT_BLUE);
pub const CYAN: AccentedPalette = AccentedPalette::from_variants(variants::CYAN);
pub const TEAL: AccentedPalette = AccentedPalette::from_variants(variants::TEAL);
pub const GREEN: AccentedPalette = AccentedPalette::from_variants(variants::GREEN);
pub const LIGHT_GREEN: AccentedPalette = AccentedPalette::from_variants(variants::LIGHT_GREEN);
pub const LIME: AccentedPalette = AccentedPalette::from_variants(variants::LIME);
pub const YELLOW: AccentedPalette = AccentedPalette::from_variants(variants::YELLOW);
pub const AMBER: AccentedPalette = AccentedPalette::from_variants(variants::AMBER);
pub const ORANGE: AccentedPalette = AccentedPalette::from_variants(variants::ORANGE);
pub const DEEP_ORANGE: AccentedPalette = AccentedPalette::from_variants(variants::DEEP_ORANGE);
// Unaccented palettes
pub const BROWN: NonAccentedPalette = NonAccentedPalette::from_variants(variants::BROWN);
pub const GRAY: NonAccentedPalette = NonAccentedPalette::from_variants(variants::GRAY);
pub const BLUE_GRAY: NonAccentedPalette = NonAccentedPalette::from_variants(variants::BLUE_GRAY);
// Black and white included for completeness
pub const BLACK: Color = Color::from_u32(0x000000);
pub const WHITE: Color = Color::from_u32(0xFFFFFF);
mod variants {
pub const RED: [u32; 14] = [
0xFFEBEE, 0xFFCDD2, 0xEF9A9A, 0xE57373, 0xEF5350, 0xF44336, 0xE53935, 0xD32F2F, 0xC62828,
0xB71C1C, 0xFF8A80, 0xFF5252, 0xFF1744, 0xD50000,
];
pub const PINK: [u32; 14] = [
0xFCE4EC, 0xF8BBD0, 0xF48FB1, 0xF06292, 0xEC407A, 0xE91E63, 0xD81B60, 0xC2185B, 0xAD1457,
0x880E4F, 0xFF80AB, 0xFF4081, 0xF50057, 0xC51162,
];
pub const PURPLE: [u32; 14] = [
0xF3E5F5, 0xE1BEE7, 0xCE93D8, 0xBA68C8, 0xAB47BC, 0x9C27B0, 0x8E24AA, 0x7B1FA2, 0x6A1B9A,
0x4A148C, 0xEA80FC, 0xE040FB, 0xD500F9, 0xAA00FF,
];
pub const DEEP_PURPLE: [u32; 14] = [
0xEDE7F6, 0xD1C4E9, 0xB39DDB, 0x9575CD, 0x7E57C2, 0x673AB7, 0x5E35B1, 0x512DA8, 0x4527A0,
0x311B92, 0xB388FF, 0x7C4DFF, 0x651FFF, 0x6200EA,
];
pub const INDIGO: [u32; 14] = [
0xE8EAF6, 0xC5CAE9, 0x9FA8DA, 0x7986CB, 0x5C6BC0, 0x3F51B5, 0x3949AB, 0x303F9F, 0x283593,
0x1A237E, 0x8C9EFF, 0x536DFE, 0x3D5AFE, 0x304FFE,
];
pub const BLUE: [u32; 14] = [
0xE3F2FD, 0xBBDEFB, 0x90CAF9, 0x64B5F6, 0x42A5F5, 0x2196F3, 0x1E88E5, 0x1976D2, 0x1565C0,
0x0D47A1, 0x82B1FF, 0x448AFF, 0x2979FF, 0x2962FF,
];
pub const LIGHT_BLUE: [u32; 14] = [
0xE1F5FE, 0xB3E5FC, 0x81D4FA, 0x4FC3F7, 0x29B6F6, 0x03A9F4, 0x039BE5, 0x0288D1, 0x0277BD,
0x01579B, 0x80D8FF, 0x40C4FF, 0x00B0FF, 0x0091EA,
];
pub const CYAN: [u32; 14] = [
0xE0F7FA, 0xB2EBF2, 0x80DEEA, 0x4DD0E1, 0x26C6DA, 0x00BCD4, 0x00ACC1, 0x0097A7, 0x00838F,
0x006064, 0x84FFFF, 0x18FFFF, 0x00E5FF, 0x00B8D4,
];
pub const TEAL: [u32; 14] = [
0xE0F2F1, 0xB2DFDB, 0x80CBC4, 0x4DB6AC, 0x26A69A, 0x009688, 0x00897B, 0x00796B, 0x00695C,
0x004D40, 0xA7FFEB, 0x64FFDA, 0x1DE9B6, 0x00BFA5,
];
pub const GREEN: [u32; 14] = [
0xE8F5E9, 0xC8E6C9, 0xA5D6A7, 0x81C784, 0x66BB6A, 0x4CAF50, 0x43A047, 0x388E3C, 0x2E7D32,
0x1B5E20, 0xB9F6CA, 0x69F0AE, 0x00E676, 0x00C853,
];
pub const LIGHT_GREEN: [u32; 14] = [
0xF1F8E9, 0xDCEDC8, 0xC5E1A5, 0xAED581, 0x9CCC65, 0x8BC34A, 0x7CB342, 0x689F38, 0x558B2F,
0x33691E, 0xCCFF90, 0xB2FF59, 0x76FF03, 0x64DD17,
];
pub const LIME: [u32; 14] = [
0xF9FBE7, 0xF0F4C3, 0xE6EE9C, 0xDCE775, 0xD4E157, 0xCDDC39, 0xC0CA33, 0xAFB42B, 0x9E9D24,
0x827717, 0xF4FF81, 0xEEFF41, 0xC6FF00, 0xAEEA00,
];
pub const YELLOW: [u32; 14] = [
0xFFFDE7, 0xFFF9C4, 0xFFF59D, 0xFFF176, 0xFFEE58, 0xFFEB3B, 0xFDD835, 0xFBC02D, 0xF9A825,
0xF57F17, 0xFFFF8D, 0xFFFF00, 0xFFEA00, 0xFFD600,
];
pub const AMBER: [u32; 14] = [
0xFFF8E1, 0xFFECB3, 0xFFE082, 0xFFD54F, 0xFFCA28, 0xFFC107, 0xFFB300, 0xFFA000, 0xFF8F00,
0xFF6F00, 0xFFE57F, 0xFFD740, 0xFFC400, 0xFFAB00,
];
pub const ORANGE: [u32; 14] = [
0xFFF3E0, 0xFFE0B2, 0xFFCC80, 0xFFB74D, 0xFFA726, 0xFF9800, 0xFB8C00, 0xF57C00, 0xEF6C00,
0xE65100, 0xFFD180, 0xFFAB40, 0xFF9100, 0xFF6D00,
];
pub const DEEP_ORANGE: [u32; 14] = [
0xFBE9E7, 0xFFCCBC, 0xFFAB91, 0xFF8A65, 0xFF7043, 0xFF5722, 0xF4511E, 0xE64A19, 0xD84315,
0xBF360C, 0xFF9E80, 0xFF6E40, 0xFF3D00, 0xDD2C00,
];
pub const BROWN: [u32; 10] = [
0xEFEBE9, 0xD7CCC8, 0xBCAAA4, 0xA1887F, 0x8D6E63, 0x795548, 0x6D4C41, 0x5D4037, 0x4E342E,
0x3E2723,
];
pub const GRAY: [u32; 10] = [
0xFAFAFA, 0xF5F5F5, 0xEEEEEE, 0xE0E0E0, 0xBDBDBD, 0x9E9E9E, 0x757575, 0x616161, 0x424242,
0x212121,
];
pub const BLUE_GRAY: [u32; 10] = [
0xECEFF1, 0xCFD8DC, 0xB0BEC5, 0x90A4AE, 0x78909C, 0x607D8B, 0x546E7A, 0x455A64, 0x37474F,
0x263238,
];
}

View File

@@ -1,652 +0,0 @@
//! Represents the Tailwind CSS [default color palette][palette].
//!
//! [palette]: https://tailwindcss.com/docs/customizing-colors#default-color-palette
//!
//! There are 22 palettes. Each palette has 11 colors, with variants from 50 to 950. Black and White
//! are also included for completeness and to avoid being affected by any terminal theme that might
//! be in use.
//!
//! <style>
//! .color { display: flex; align-items: center; }
//! .color > div { width: 2rem; height: 2rem; }
//! .color > div.name { width: 150px; !important; }
//! </style>
//! <div style="overflow-x: auto">
//! <div style="display: flex; flex-direction:column; text-align: left">
//! <div class="color" style="font-size:0.8em">
//! <div class="name"></div>
//! <div>C50</div> <div>C100</div> <div>C200</div> <div>C300</div> <div>C400</div>
//! <div>C500</div> <div>C600</div> <div>C700</div> <div>C800</div> <div>C900</div>
//! <div>C950</div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`SLATE`]</div>
//! <div style="background-color: #f8fafc"></div> <div style="background-color: #f1f5f9"></div>
//! <div style="background-color: #e2e8f0"></div> <div style="background-color: #cbd5e1"></div>
//! <div style="background-color: #94a3b8"></div> <div style="background-color: #64748b"></div>
//! <div style="background-color: #475569"></div> <div style="background-color: #334155"></div>
//! <div style="background-color: #1e293b"></div> <div style="background-color: #0f172a"></div>
//! <div style="background-color: #020617"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`GRAY`]</div>
//! <div style="background-color: #f9fafb"></div> <div style="background-color: #f3f4f6"></div>
//! <div style="background-color: #e5e7eb"></div> <div style="background-color: #d1d5db"></div>
//! <div style="background-color: #9ca3af"></div> <div style="background-color: #6b7280"></div>
//! <div style="background-color: #4b5563"></div> <div style="background-color: #374151"></div>
//! <div style="background-color: #1f2937"></div> <div style="background-color: #111827"></div>
//! <div style="background-color: #0a0a0a"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`ZINC`]</div>
//! <div style="background-color: #fafafa"></div> <div style="background-color: #f5f5f5"></div>
//! <div style="background-color: #e5e5e5"></div> <div style="background-color: #d4d4d4"></div>
//! <div style="background-color: #a1a1aa"></div> <div style="background-color: #71717a"></div>
//! <div style="background-color: #52525b"></div> <div style="background-color: #404040"></div>
//! <div style="background-color: #262626"></div> <div style="background-color: #171717"></div>
//! <div style="background-color: #0a0a0a"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`NEUTRAL`]</div>
//! <div style="background-color: #fafafa"></div> <div style="background-color: #f5f5f5"></div>
//! <div style="background-color: #e5e5e5"></div> <div style="background-color: #d4d4d4"></div>
//! <div style="background-color: #a3a3a3"></div> <div style="background-color: #737373"></div>
//! <div style="background-color: #525252"></div> <div style="background-color: #404040"></div>
//! <div style="background-color: #262626"></div> <div style="background-color: #171717"></div>
//! <div style="background-color: #0a0a0a"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`STONE`]</div>
//! <div style="background-color: #fafaf9"></div> <div style="background-color: #f5f5f4"></div>
//! <div style="background-color: #e7e5e4"></div> <div style="background-color: #d6d3d1"></div>
//! <div style="background-color: #a8a29e"></div> <div style="background-color: #78716c"></div>
//! <div style="background-color: #57534e"></div> <div style="background-color: #44403c"></div>
//! <div style="background-color: #292524"></div> <div style="background-color: #1c1917"></div>
//! <div style="background-color: #0c0a09"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`RED`]</div>
//! <div style="background-color: #fef2f2"></div> <div style="background-color: #fee2e2"></div>
//! <div style="background-color: #fecaca"></div> <div style="background-color: #fca5a5"></div>
//! <div style="background-color: #f87171"></div> <div style="background-color: #ef4444"></div>
//! <div style="background-color: #dc2626"></div> <div style="background-color: #b91c1c"></div>
//! <div style="background-color: #991b1b"></div> <div style="background-color: #7f1d1d"></div>
//! <div style="background-color: #450a0a"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`ORANGE`]</div>
//! <div style="background-color: #fff7ed"></div> <div style="background-color: #ffedd5"></div>
//! <div style="background-color: #fed7aa"></div> <div style="background-color: #fdba74"></div>
//! <div style="background-color: #fb923c"></div> <div style="background-color: #f97316"></div>
//! <div style="background-color: #ea580c"></div> <div style="background-color: #c2410c"></div>
//! <div style="background-color: #9a3412"></div> <div style="background-color: #7c2d12"></div>
//! <div style="background-color: #431407"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`AMBER`]</div>
//! <div style="background-color: #fffbeb"></div> <div style="background-color: #fef3c7"></div>
//! <div style="background-color: #fde68a"></div> <div style="background-color: #fcd34d"></div>
//! <div style="background-color: #fbbf24"></div> <div style="background-color: #f59e0b"></div>
//! <div style="background-color: #d97706"></div> <div style="background-color: #b45309"></div>
//! <div style="background-color: #92400e"></div> <div style="background-color: #78350f"></div>
//! <div style="background-color: #451a03"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`YELLOW`]</div>
//! <div style="background-color: #fefce8"></div> <div style="background-color: #fef9c3"></div>
//! <div style="background-color: #fef08a"></div> <div style="background-color: #fde047"></div>
//! <div style="background-color: #facc15"></div> <div style="background-color: #eab308"></div>
//! <div style="background-color: #ca8a04"></div> <div style="background-color: #a16207"></div>
//! <div style="background-color: #854d0e"></div> <div style="background-color: #713f12"></div>
//! <div style="background-color: #422006"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`LIME`]</div>
//! <div style="background-color: #f7fee7"></div> <div style="background-color: #ecfccb"></div>
//! <div style="background-color: #d9f99d"></div> <div style="background-color: #bef264"></div>
//! <div style="background-color: #a3e635"></div> <div style="background-color: #84cc16"></div>
//! <div style="background-color: #65a30d"></div> <div style="background-color: #4d7c0f"></div>
//! <div style="background-color: #3f6212"></div> <div style="background-color: #365314"></div>
//! <div style="background-color: #1a2e05"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`GREEN`]</div>
//! <div style="background-color: #f0fdf4"></div> <div style="background-color: #dcfce7"></div>
//! <div style="background-color: #bbf7d0"></div> <div style="background-color: #86efac"></div>
//! <div style="background-color: #4ade80"></div> <div style="background-color: #22c55e"></div>
//! <div style="background-color: #16a34a"></div> <div style="background-color: #15803d"></div>
//! <div style="background-color: #166534"></div> <div style="background-color: #14532d"></div>
//! <div style="background-color: #052e16"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`EMERALD`]</div>
//! <div style="background-color: #ecfdf5"></div> <div style="background-color: #d1fae5"></div>
//! <div style="background-color: #a7f3d0"></div> <div style="background-color: #6ee7b7"></div>
//! <div style="background-color: #34d399"></div> <div style="background-color: #10b981"></div>
//! <div style="background-color: #059669"></div> <div style="background-color: #047857"></div>
//! <div style="background-color: #065f46"></div> <div style="background-color: #064e3b"></div>
//! <div style="background-color: #022c22"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`TEAL`]</div>
//! <div style="background-color: #f0fdfa"></div> <div style="background-color: #ccfbf1"></div>
//! <div style="background-color: #99f6e4"></div> <div style="background-color: #5eead4"></div>
//! <div style="background-color: #2dd4bf"></div> <div style="background-color: #14b8a6"></div>
//! <div style="background-color: #0d9488"></div> <div style="background-color: #0f766e"></div>
//! <div style="background-color: #115e59"></div> <div style="background-color: #134e4a"></div>
//! <div style="background-color: #042f2e"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`CYAN`]</div>
//! <div style="background-color: #ecfeff"></div> <div style="background-color: #cffafe"></div>
//! <div style="background-color: #a5f3fc"></div> <div style="background-color: #67e8f9"></div>
//! <div style="background-color: #22d3ee"></div> <div style="background-color: #06b6d4"></div>
//! <div style="background-color: #0891b2"></div> <div style="background-color: #0e7490"></div>
//! <div style="background-color: #155e75"></div> <div style="background-color: #164e63"></div>
//! <div style="background-color: #083344"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`SKY`]</div>
//! <div style="background-color: #f0f9ff"></div> <div style="background-color: #e0f2fe"></div>
//! <div style="background-color: #bae6fd"></div> <div style="background-color: #7dd3fc"></div>
//! <div style="background-color: #38bdf8"></div> <div style="background-color: #0ea5e9"></div>
//! <div style="background-color: #0284c7"></div> <div style="background-color: #0369a1"></div>
//! <div style="background-color: #075985"></div> <div style="background-color: #0c4a6e"></div>
//! <div style="background-color: #082f49"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`BLUE`]</div>
//! <div style="background-color: #eff6ff"></div> <div style="background-color: #dbeafe"></div>
//! <div style="background-color: #bfdbfe"></div> <div style="background-color: #93c5fd"></div>
//! <div style="background-color: #60a5fa"></div> <div style="background-color: #3b82f6"></div>
//! <div style="background-color: #2563eb"></div> <div style="background-color: #1d4ed8"></div>
//! <div style="background-color: #1e40af"></div> <div style="background-color: #1e3a8a"></div>
//! <div style="background-color: #172554"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`INDIGO`]</div>
//! <div style="background-color: #eef2ff"></div> <div style="background-color: #e0e7ff"></div>
//! <div style="background-color: #c7d2fe"></div> <div style="background-color: #a5b4fc"></div>
//! <div style="background-color: #818cf8"></div> <div style="background-color: #6366f1"></div>
//! <div style="background-color: #4f46e5"></div> <div style="background-color: #4338ca"></div>
//! <div style="background-color: #3730a3"></div> <div style="background-color: #312e81"></div>
//! <div style="background-color: #1e1b4b"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`VIOLET`]</div>
//! <div style="background-color: #f5f3ff"></div> <div style="background-color: #ede9fe"></div>
//! <div style="background-color: #ddd6fe"></div> <div style="background-color: #c4b5fd"></div>
//! <div style="background-color: #a78bfa"></div> <div style="background-color: #8b5cf6"></div>
//! <div style="background-color: #7c3aed"></div> <div style="background-color: #6d28d9"></div>
//! <div style="background-color: #5b21b6"></div> <div style="background-color: #4c1d95"></div>
//! <div style="background-color: #2e1065"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`PURPLE`]</div>
//! <div style="background-color: #faf5ff"></div> <div style="background-color: #f3e8ff"></div>
//! <div style="background-color: #e9d5ff"></div> <div style="background-color: #d8b4fe"></div>
//! <div style="background-color: #c084fc"></div> <div style="background-color: #a855f7"></div>
//! <div style="background-color: #9333ea"></div> <div style="background-color: #7e22ce"></div>
//! <div style="background-color: #6b21a8"></div> <div style="background-color: #581c87"></div>
//! <div style="background-color: #4c136e"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`FUCHSIA`]</div>
//! <div style="background-color: #fdf4ff"></div> <div style="background-color: #fae8ff"></div>
//! <div style="background-color: #f5d0fe"></div> <div style="background-color: #f0abfc"></div>
//! <div style="background-color: #e879f9"></div> <div style="background-color: #d946ef"></div>
//! <div style="background-color: #c026d3"></div> <div style="background-color: #a21caf"></div>
//! <div style="background-color: #86198f"></div> <div style="background-color: #701a75"></div>
//! <div style="background-color: #4e145b"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`PINK`]</div>
//! <div style="background-color: #fdf2f8"></div> <div style="background-color: #fce7f3"></div>
//! <div style="background-color: #fbcfe8"></div> <div style="background-color: #f9a8d4"></div>
//! <div style="background-color: #f472b6"></div> <div style="background-color: #ec4899"></div>
//! <div style="background-color: #db2777"></div> <div style="background-color: #be185d"></div>
//! <div style="background-color: #9d174d"></div> <div style="background-color: #831843"></div>
//! <div style="background-color: #5f0b37"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`BLACK`]</div>
//! <div style="background-color: #000000; width:22rem"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`WHITE`]</div>
//! <div style="background-color: #ffffff; width:22rem"></div>
//! </div>
//! </div>
//! </div>
//!
//! # Example
//!
//! ```rust
//! # use ratatui::prelude::*;
//! use ratatui::style::palette::tailwind::{BLUE, RED};
//!
//! assert_eq!(RED.c500, Color::Rgb(239, 68, 68));
//! assert_eq!(BLUE.c500, Color::Rgb(59, 130, 246));
//! ```
use crate::prelude::*;
pub struct Palette {
pub c50: Color,
pub c100: Color,
pub c200: Color,
pub c300: Color,
pub c400: Color,
pub c500: Color,
pub c600: Color,
pub c700: Color,
pub c800: Color,
pub c900: Color,
pub c950: Color,
}
#[rustfmt::skip]
/// <style>.palette div{width:22rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #000000"></div></div>
pub const BLACK: Color = Color::from_u32(0x000000);
#[rustfmt::skip]
/// <style>.palette div{width:22rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #ffffff"></div></div>
pub const WHITE: Color = Color::from_u32(0xffffff);
#[rustfmt::skip]
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #f8fafc"></div><div style="background-color: #f1f5f9"></div><div style="background-color: #e2e8f0"></div><div style="background-color: #cbd5e1"></div><div style="background-color: #94a3b8"></div><div style="background-color: #64748b"></div><div style="background-color: #475569"></div><div style="background-color: #334155"></div><div style="background-color: #1e293b"></div><div style="background-color: #0f172a"></div><div style="background-color: #020617"></div></div>
pub const SLATE: Palette = Palette {
c50: Color::from_u32(0xf8fafc),
c100: Color::from_u32(0xf1f5f9),
c200: Color::from_u32(0xe2e8f0),
c300: Color::from_u32(0xcbd5e1),
c400: Color::from_u32(0x94a3b8),
c500: Color::from_u32(0x64748b),
c600: Color::from_u32(0x475569),
c700: Color::from_u32(0x334155),
c800: Color::from_u32(0x1e293b),
c900: Color::from_u32(0x0f172a),
c950: Color::from_u32(0x020617),
};
#[rustfmt::skip]
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #f9fafb"></div><div style="background-color: #f3f4f6"></div><div style="background-color: #e5e7eb"></div><div style="background-color: #d1d5db"></div><div style="background-color: #9ca3af"></div><div style="background-color: #6b7280"></div><div style="background-color: #4b5563"></div><div style="background-color: #374151"></div><div style="background-color: #1f2937"></div><div style="background-color: #111827"></div><div style="background-color: #030712"></div></div>
pub const GRAY: Palette = Palette {
c50: Color::from_u32(0xf9fafb),
c100: Color::from_u32(0xf3f4f6),
c200: Color::from_u32(0xe5e7eb),
c300: Color::from_u32(0xd1d5db),
c400: Color::from_u32(0x9ca3af),
c500: Color::from_u32(0x6b7280),
c600: Color::from_u32(0x4b5563),
c700: Color::from_u32(0x374151),
c800: Color::from_u32(0x1f2937),
c900: Color::from_u32(0x111827),
c950: Color::from_u32(0x030712),
};
#[rustfmt::skip]
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #fafafa"></div><div style="background-color: #f5f5f5"></div><div style="background-color: #e5e5e5"></div><div style="background-color: #d4d4d4"></div><div style="background-color: #a1a1aa"></div><div style="background-color: #71717a"></div><div style="background-color: #52525b"></div><div style="background-color: #404040"></div><div style="background-color: #262626"></div><div style="background-color: #171717"></div><div style="background-color: #09090b"></div></div>
pub const ZINC: Palette = Palette {
c50: Color::from_u32(0xfafafa),
c100: Color::from_u32(0xf4f4f5),
c200: Color::from_u32(0xe4e4e7),
c300: Color::from_u32(0xd4d4d8),
c400: Color::from_u32(0xa1a1aa),
c500: Color::from_u32(0x71717a),
c600: Color::from_u32(0x52525b),
c700: Color::from_u32(0x3f3f46),
c800: Color::from_u32(0x27272a),
c900: Color::from_u32(0x18181b),
c950: Color::from_u32(0x09090b),
};
#[rustfmt::skip]
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #fafafa"></div><div style="background-color: #f5f5f5"></div><div style="background-color: #e5e5e5"></div><div style="background-color: #d4d4d4"></div><div style="background-color: #a3a3a3"></div><div style="background-color: #737373"></div><div style="background-color: #525252"></div><div style="background-color: #404040"></div><div style="background-color: #262626"></div><div style="background-color: #171717"></div><div style="background-color: #0a0a0a"></div></div>
pub const NEUTRAL: Palette = Palette {
c50: Color::from_u32(0xfafafa),
c100: Color::from_u32(0xf5f5f5),
c200: Color::from_u32(0xe5e5e5),
c300: Color::from_u32(0xd4d4d4),
c400: Color::from_u32(0xa3a3a3),
c500: Color::from_u32(0x737373),
c600: Color::from_u32(0x525252),
c700: Color::from_u32(0x404040),
c800: Color::from_u32(0x262626),
c900: Color::from_u32(0x171717),
c950: Color::from_u32(0x0a0a0a),
};
#[rustfmt::skip]
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #fafaf9"></div><div style="background-color: #f5f5f4"></div><div style="background-color: #e7e5e4"></div><div style="background-color: #d6d3d1"></div><div style="background-color: #a8a29e"></div><div style="background-color: #78716c"></div><div style="background-color: #57534e"></div><div style="background-color: #44403c"></div><div style="background-color: #292524"></div><div style="background-color: #1c1917"></div><div style="background-color: #0c0a09"></div></div>
pub const STONE: Palette = Palette {
c50: Color::from_u32(0xfafaf9),
c100: Color::from_u32(0xf5f5f4),
c200: Color::from_u32(0xe7e5e4),
c300: Color::from_u32(0xd6d3d1),
c400: Color::from_u32(0xa8a29e),
c500: Color::from_u32(0x78716c),
c600: Color::from_u32(0x57534e),
c700: Color::from_u32(0x44403c),
c800: Color::from_u32(0x292524),
c900: Color::from_u32(0x1c1917),
c950: Color::from_u32(0x0c0a09),
};
#[rustfmt::skip]
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #fef2f2"></div><div style="background-color: #fee2e2"></div><div style="background-color: #fecaca"></div><div style="background-color: #fca5a5"></div><div style="background-color: #f87171"></div><div style="background-color: #ef4444"></div><div style="background-color: #dc2626"></div><div style="background-color: #b91c1c"></div><div style="background-color: #991b1b"></div><div style="background-color: #7f1d1d"></div><div style="background-color: #450a0a"></div></div>
pub const RED: Palette = Palette {
c50: Color::from_u32(0xfef2f2),
c100: Color::from_u32(0xfee2e2),
c200: Color::from_u32(0xfecaca),
c300: Color::from_u32(0xfca5a5),
c400: Color::from_u32(0xf87171),
c500: Color::from_u32(0xef4444),
c600: Color::from_u32(0xdc2626),
c700: Color::from_u32(0xb91c1c),
c800: Color::from_u32(0x991b1b),
c900: Color::from_u32(0x7f1d1d),
c950: Color::from_u32(0x450a0a),
};
#[rustfmt::skip]
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #fff7ed"></div><div style="background-color: #ffedd5"></div><div style="background-color: #fed7aa"></div><div style="background-color: #fdba74"></div><div style="background-color: #fb923c"></div><div style="background-color: #f97316"></div><div style="background-color: #ea580c"></div><div style="background-color: #c2410c"></div><div style="background-color: #9a3412"></div><div style="background-color: #7c2d12"></div><div style="background-color: #431407"></div></div>
pub const ORANGE: Palette = Palette {
c50: Color::from_u32(0xfff7ed),
c100: Color::from_u32(0xffedd5),
c200: Color::from_u32(0xfed7aa),
c300: Color::from_u32(0xfdba74),
c400: Color::from_u32(0xfb923c),
c500: Color::from_u32(0xf97316),
c600: Color::from_u32(0xea580c),
c700: Color::from_u32(0xc2410c),
c800: Color::from_u32(0x9a3412),
c900: Color::from_u32(0x7c2d12),
c950: Color::from_u32(0x431407),
};
#[rustfmt::skip]
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #fffbeb"></div><div style="background-color: #fef3c7"></div><div style="background-color: #fde68a"></div><div style="background-color: #fcd34d"></div><div style="background-color: #fbbf24"></div><div style="background-color: #f59e0b"></div><div style="background-color: #d97706"></div><div style="background-color: #b45309"></div><div style="background-color: #92400e"></div><div style="background-color: #78350f"></div><div style="background-color: #451a03"></div></div>
pub const AMBER: Palette = Palette {
c50: Color::from_u32(0xfffbeb),
c100: Color::from_u32(0xfef3c7),
c200: Color::from_u32(0xfde68a),
c300: Color::from_u32(0xfcd34d),
c400: Color::from_u32(0xfbbf24),
c500: Color::from_u32(0xf59e0b),
c600: Color::from_u32(0xd97706),
c700: Color::from_u32(0xb45309),
c800: Color::from_u32(0x92400e),
c900: Color::from_u32(0x78350f),
c950: Color::from_u32(0x451a03),
};
#[rustfmt::skip]
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #fefce8"></div><div style="background-color: #fef9c3"></div><div style="background-color: #fef08a"></div><div style="background-color: #fde047"></div><div style="background-color: #facc15"></div><div style="background-color: #eab308"></div><div style="background-color: #ca8a04"></div><div style="background-color: #a16207"></div><div style="background-color: #854d0e"></div><div style="background-color: #713f12"></div><div style="background-color: #422006"></div></div>
pub const YELLOW: Palette = Palette {
c50: Color::from_u32(0xfefce8),
c100: Color::from_u32(0xfef9c3),
c200: Color::from_u32(0xfef08a),
c300: Color::from_u32(0xfde047),
c400: Color::from_u32(0xfacc15),
c500: Color::from_u32(0xeab308),
c600: Color::from_u32(0xca8a04),
c700: Color::from_u32(0xa16207),
c800: Color::from_u32(0x854d0e),
c900: Color::from_u32(0x713f12),
c950: Color::from_u32(0x422006),
};
#[rustfmt::skip]
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #f7fee7"></div><div style="background-color: #ecfccb"></div><div style="background-color: #d9f99d"></div><div style="background-color: #bef264"></div><div style="background-color: #a3e635"></div><div style="background-color: #84cc16"></div><div style="background-color: #65a30d"></div><div style="background-color: #4d7c0f"></div><div style="background-color: #3f6212"></div><div style="background-color: #365314"></div><div style="background-color: #1a2e05"></div></div>
pub const LIME: Palette = Palette {
c50: Color::from_u32(0xf7fee7),
c100: Color::from_u32(0xecfccb),
c200: Color::from_u32(0xd9f99d),
c300: Color::from_u32(0xbef264),
c400: Color::from_u32(0xa3e635),
c500: Color::from_u32(0x84cc16),
c600: Color::from_u32(0x65a30d),
c700: Color::from_u32(0x4d7c0f),
c800: Color::from_u32(0x3f6212),
c900: Color::from_u32(0x365314),
c950: Color::from_u32(0x1a2e05),
};
#[rustfmt::skip]
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #f0fdf4"></div><div style="background-color: #dcfce7"></div><div style="background-color: #bbf7d0"></div><div style="background-color: #86efac"></div><div style="background-color: #4ade80"></div><div style="background-color: #22c55e"></div><div style="background-color: #16a34a"></div><div style="background-color: #15803d"></div><div style="background-color: #166534"></div><div style="background-color: #14532d"></div><div style="background-color: #052e16"></div></div>
pub const GREEN: Palette = Palette {
c50: Color::from_u32(0xf0fdf4),
c100: Color::from_u32(0xdcfce7),
c200: Color::from_u32(0xbbf7d0),
c300: Color::from_u32(0x86efac),
c400: Color::from_u32(0x4ade80),
c500: Color::from_u32(0x22c55e),
c600: Color::from_u32(0x16a34a),
c700: Color::from_u32(0x15803d),
c800: Color::from_u32(0x166534),
c900: Color::from_u32(0x14532d),
c950: Color::from_u32(0x052e16),
};
#[rustfmt::skip]
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #f0fdfa"></div><div style="background-color: #ccfbf1"></div><div style="background-color: #99f6e4"></div><div style="background-color: #5eead4"></div><div style="background-color: #2dd4bf"></div><div style="background-color: #14b8a6"></div><div style="background-color: #0d9488"></div><div style="background-color: #0f766e"></div><div style="background-color: #115e59"></div><div style="background-color: #134e4a"></div><div style="background-color: #042f2e"></div></div>
pub const EMERALD: Palette = Palette {
c50: Color::from_u32(0xecfdf5),
c100: Color::from_u32(0xd1fae5),
c200: Color::from_u32(0xa7f3d0),
c300: Color::from_u32(0x6ee7b7),
c400: Color::from_u32(0x34d399),
c500: Color::from_u32(0x10b981),
c600: Color::from_u32(0x059669),
c700: Color::from_u32(0x047857),
c800: Color::from_u32(0x065f46),
c900: Color::from_u32(0x064e3b),
c950: Color::from_u32(0x022c22),
};
#[rustfmt::skip]
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #f5fdf4"></div><div style="background-color: #e7f9e7"></div><div style="background-color: #c6f6d5"></div><div style="background-color: #9ae6b4"></div><div style="background-color: #68d391"></div><div style="background-color: #48bb78"></div><div style="background-color: #38a169"></div><div style="background-color: #2f855a"></div><div style="background-color: #276749"></div><div style="background-color: #22543d"></div><div style="background-color: #0d3321"></div></div>
pub const TEAL: Palette = Palette {
c50: Color::from_u32(0xf0fdfa),
c100: Color::from_u32(0xccfbf1),
c200: Color::from_u32(0x99f6e4),
c300: Color::from_u32(0x5eead4),
c400: Color::from_u32(0x2dd4bf),
c500: Color::from_u32(0x14b8a6),
c600: Color::from_u32(0x0d9488),
c700: Color::from_u32(0x0f766e),
c800: Color::from_u32(0x115e59),
c900: Color::from_u32(0x134e4a),
c950: Color::from_u32(0x042f2e),
};
#[rustfmt::skip]
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #ecfeff"></div><div style="background-color: #cffafe"></div><div style="background-color: #a5f3fc"></div><div style="background-color: #67e8f9"></div><div style="background-color: #22d3ee"></div><div style="background-color: #06b6d4"></div><div style="background-color: #0891b2"></div><div style="background-color: #0e7490"></div><div style="background-color: #155e75"></div><div style="background-color: #164e63"></div><div style="background-color: #083344"></div></div>
pub const CYAN: Palette = Palette {
c50: Color::from_u32(0xecfeff),
c100: Color::from_u32(0xcffafe),
c200: Color::from_u32(0xa5f3fc),
c300: Color::from_u32(0x67e8f9),
c400: Color::from_u32(0x22d3ee),
c500: Color::from_u32(0x06b6d4),
c600: Color::from_u32(0x0891b2),
c700: Color::from_u32(0x0e7490),
c800: Color::from_u32(0x155e75),
c900: Color::from_u32(0x164e63),
c950: Color::from_u32(0x083344),
};
#[rustfmt::skip]
/// <style>.palette div{width:22rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #f0f9ff"></div><div style="background-color: #e0f2fe"></div><div style="background-color: #bae6fd"></div><div style="background-color: #7dd3fc"></div><div style="background-color: #38bdf8"></div><div style="background-color: #0ea5e9"></div><div style="background-color: #0284c7"></div><div style="background-color: #0369a1"></div><div style="background-color: #075985"></div><div style="background-color: #0c4a6e"></div><div style="background-color: #082f49"></div></div>
pub const SKY: Palette = Palette {
c50: Color::from_u32(0xf0f9ff),
c100: Color::from_u32(0xe0f2fe),
c200: Color::from_u32(0xbae6fd),
c300: Color::from_u32(0x7dd3fc),
c400: Color::from_u32(0x38bdf8),
c500: Color::from_u32(0x0ea5e9),
c600: Color::from_u32(0x0284c7),
c700: Color::from_u32(0x0369a1),
c800: Color::from_u32(0x075985),
c900: Color::from_u32(0x0c4a6e),
c950: Color::from_u32(0x082f49),
};
#[rustfmt::skip]
/// <style>.palette div{width:22rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #eff6ff"></div><div style="background-color: #dbeafe"></div><div style="background-color: #bfdbfe"></div><div style="background-color: #93c5fd"></div><div style="background-color: #60a5fa"></div><div style="background-color: #3b82f6"></div><div style="background-color: #2563eb"></div><div style="background-color: #1d4ed8"></div><div style="background-color: #1e40af"></div><div style="background-color: #1e3a8a"></div><div style="background-color: #172554"></div></div>
pub const BLUE: Palette = Palette {
c50: Color::from_u32(0xeff6ff),
c100: Color::from_u32(0xdbeafe),
c200: Color::from_u32(0xbfdbfe),
c300: Color::from_u32(0x93c5fd),
c400: Color::from_u32(0x60a5fa),
c500: Color::from_u32(0x3b82f6),
c600: Color::from_u32(0x2563eb),
c700: Color::from_u32(0x1d4ed8),
c800: Color::from_u32(0x1e40af),
c900: Color::from_u32(0x1e3a8a),
c950: Color::from_u32(0x172554),
};
#[rustfmt::skip]
/// <style>.palette div{width:22rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #eef2ff"></div><div style="background-color: #e0e7ff"></div><div style="background-color: #c7d2fe"></div><div style="background-color: #a5b4fc"></div><div style="background-color: #818cf8"></div><div style="background-color: #6366f1"></div><div style="background-color: #4f46e5"></div><div style="background-color: #4338ca"></div><div style="background-color: #3730a3"></div><div style="background-color: #312e81"></div><div style="background-color: #1e1b4b"></div></div>
pub const INDIGO: Palette = Palette {
c50: Color::from_u32(0xeef2ff),
c100: Color::from_u32(0xe0e7ff),
c200: Color::from_u32(0xc7d2fe),
c300: Color::from_u32(0xa5b4fc),
c400: Color::from_u32(0x818cf8),
c500: Color::from_u32(0x6366f1),
c600: Color::from_u32(0x4f46e5),
c700: Color::from_u32(0x4338ca),
c800: Color::from_u32(0x3730a3),
c900: Color::from_u32(0x312e81),
c950: Color::from_u32(0x1e1b4b),
};
#[rustfmt::skip]
/// <style>.palette div{width:22rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #f5f3ff"></div><div style="background-color: #ede9fe"></div><div style="background-color: #ddd6fe"></div><div style="background-color: #c4b5fd"></div><div style="background-color: #a78bfa"></div><div style="background-color: #8b5cf6"></div><div style="background-color: #7c3aed"></div><div style="background-color: #6d28d9"></div><div style="background-color: #5b21b6"></div><div style="background-color: #4c1d95"></div><div style="background-color: #2e1065"></div></div>
pub const VIOLET: Palette = Palette {
c50: Color::from_u32(0xf5f3ff),
c100: Color::from_u32(0xede9fe),
c200: Color::from_u32(0xddd6fe),
c300: Color::from_u32(0xc4b5fd),
c400: Color::from_u32(0xa78bfa),
c500: Color::from_u32(0x8b5cf6),
c600: Color::from_u32(0x7c3aed),
c700: Color::from_u32(0x6d28d9),
c800: Color::from_u32(0x5b21b6),
c900: Color::from_u32(0x4c1d95),
c950: Color::from_u32(0x2e1065),
};
#[rustfmt::skip]
/// <style>.palette div{width:22rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #faf5ff"></div><div style="background-color: #f3e8ff"></div><div style="background-color: #e9d5ff"></div><div style="background-color: #d8b4fe"></div><div style="background-color: #c084fc"></div><div style="background-color: #a855f7"></div><div style="background-color: #9333ea"></div><div style="background-color: #7e22ce"></div><div style="background-color: #6b21a8"></div><div style="background-color: #581c87"></div><div style="background-color: #3b0764"></div></div>
pub const PURPLE: Palette = Palette {
c50: Color::from_u32(0xfaf5ff),
c100: Color::from_u32(0xf3e8ff),
c200: Color::from_u32(0xe9d5ff),
c300: Color::from_u32(0xd8b4fe),
c400: Color::from_u32(0xc084fc),
c500: Color::from_u32(0xa855f7),
c600: Color::from_u32(0x9333ea),
c700: Color::from_u32(0x7e22ce),
c800: Color::from_u32(0x6b21a8),
c900: Color::from_u32(0x581c87),
c950: Color::from_u32(0x3b0764),
};
#[rustfmt::skip]
/// <style>.palette div{width:22rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #fdf4ff"></div><div style="background-color: #fae8ff"></div><div style="background-color: #f5d0fe"></div><div style="background-color: #f0abfc"></div><div style="background-color: #e879f9"></div><div style="background-color: #d946ef"></div><div style="background-color: #c026d3"></div><div style="background-color: #a21caf"></div><div style="background-color: #86198f"></div><div style="background-color: #701a75"></div><div style="background-color: #4a044e"></div></div>
pub const FUCHSIA: Palette = Palette {
c50: Color::from_u32(0xfdf4ff),
c100: Color::from_u32(0xfae8ff),
c200: Color::from_u32(0xf5d0fe),
c300: Color::from_u32(0xf0abfc),
c400: Color::from_u32(0xe879f9),
c500: Color::from_u32(0xd946ef),
c600: Color::from_u32(0xc026d3),
c700: Color::from_u32(0xa21caf),
c800: Color::from_u32(0x86198f),
c900: Color::from_u32(0x701a75),
c950: Color::from_u32(0x4a044e),
};
#[rustfmt::skip]
/// <style>.palette div{width:22rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #fdf2f8"></div><div style="background-color: #fce7f3"></div><div style="background-color: #fbcfe8"></div><div style="background-color: #f9a8d4"></div><div style="background-color: #f472b6"></div><div style="background-color: #ec4899"></div><div style="background-color: #db2777"></div><div style="background-color: #be185d"></div><div style="background-color: #9d174d"></div><div style="background-color: #831843"></div><div style="background-color: #500724"></div></div>
pub const PINK: Palette = Palette {
c50: Color::from_u32(0xfdf2f8),
c100: Color::from_u32(0xfce7f3),
c200: Color::from_u32(0xfbcfe8),
c300: Color::from_u32(0xf9a8d4),
c400: Color::from_u32(0xf472b6),
c500: Color::from_u32(0xec4899),
c600: Color::from_u32(0xdb2777),
c700: Color::from_u32(0xbe185d),
c800: Color::from_u32(0x9d174d),
c900: Color::from_u32(0x831843),
c950: Color::from_u32(0x500724),
};
#[rustfmt::skip]
/// <style>.palette div{width:22rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #fff1f2"></div><div style="background-color: #ffe4e6"></div><div style="background-color: #fecdd3"></div><div style="background-color: #fda4af"></div><div style="background-color: #fb7185"></div><div style="background-color: #f43f5e"></div><div style="background-color: #e11d48"></div><div style="background-color: #be123c"></div><div style="background-color: #9f1239"></div><div style="background-color: #881337"></div><div style="background-color: #4c0519"></div></div>
pub const ROSE: Palette = Palette {
c50: Color::from_u32(0xfff1f2),
c100: Color::from_u32(0xffe4e6),
c200: Color::from_u32(0xfecdd3),
c300: Color::from_u32(0xfda4af),
c400: Color::from_u32(0xfb7185),
c500: Color::from_u32(0xf43f5e),
c600: Color::from_u32(0xe11d48),
c700: Color::from_u32(0xbe123c),
c800: Color::from_u32(0x9f1239),
c900: Color::from_u32(0x881337),
c950: Color::from_u32(0x4c0519),
};

View File

@@ -381,49 +381,6 @@ pub mod border {
horizontal_top: QUADRANT_BOTTOM_HALF,
horizontal_bottom: QUADRANT_TOP_HALF,
};
pub const ONE_EIGHTH_TOP_EIGHT: &str = "";
pub const ONE_EIGHTH_BOTTOM_EIGHT: &str = "";
pub const ONE_EIGHTH_LEFT_EIGHT: &str = "";
pub const ONE_EIGHTH_RIGHT_EIGHT: &str = "";
/// Wide border set based on McGugan box technique
///
/// ```text
/// ▁▁▁▁▁▁▁
/// ▏xxxxx▕
/// ▏xxxxx▕
/// ▔▔▔▔▔▔▔
/// ```
pub const ONE_EIGHTH_WIDE: Set = Set {
top_right: ONE_EIGHTH_BOTTOM_EIGHT,
top_left: ONE_EIGHTH_BOTTOM_EIGHT,
bottom_right: ONE_EIGHTH_TOP_EIGHT,
bottom_left: ONE_EIGHTH_TOP_EIGHT,
vertical_left: ONE_EIGHTH_LEFT_EIGHT,
vertical_right: ONE_EIGHTH_RIGHT_EIGHT,
horizontal_top: ONE_EIGHTH_BOTTOM_EIGHT,
horizontal_bottom: ONE_EIGHTH_TOP_EIGHT,
};
/// Tall border set based on McGugan box technique
///
/// ```text
/// ▕▔▔▏
/// ▕xx▏
/// ▕xx▏
/// ▕▁▁▏
/// ```
pub const ONE_EIGHTH_TALL: Set = Set {
top_right: ONE_EIGHTH_LEFT_EIGHT,
top_left: ONE_EIGHTH_RIGHT_EIGHT,
bottom_right: ONE_EIGHTH_LEFT_EIGHT,
bottom_left: ONE_EIGHTH_RIGHT_EIGHT,
vertical_left: ONE_EIGHTH_RIGHT_EIGHT,
vertical_right: ONE_EIGHTH_LEFT_EIGHT,
horizontal_top: ONE_EIGHTH_TOP_EIGHT,
horizontal_bottom: ONE_EIGHTH_BOTTOM_EIGHT,
};
}
pub const DOT: &str = "";

View File

@@ -30,12 +30,659 @@
//! [`backend`]: crate::backend
//! [`Backend`]: crate::backend::Backend
//! [`Buffer`]: crate::buffer::Buffer
use std::{fmt, io};
mod frame;
#[allow(clippy::module_inception)]
mod terminal;
mod viewport;
use crate::{
backend::{Backend, ClearType},
buffer::Buffer,
layout::Rect,
widgets::{StatefulWidget, Widget},
};
pub use frame::{CompletedFrame, Frame};
pub use terminal::{Options as TerminalOptions, Terminal};
pub use viewport::Viewport;
/// Represents the viewport of the terminal. The viewport is the area of the terminal that is
/// currently visible to the user. It can be either fullscreen, inline or fixed.
///
/// When the viewport is fullscreen, the whole terminal is used to draw the application.
///
/// When the viewport is inline, it is drawn inline with the rest of the terminal. The height of
/// the viewport is fixed, but the width is the same as the terminal width.
///
/// When the viewport is fixed, it is drawn in a fixed area of the terminal. The area is specified
/// by a [`Rect`].
///
/// See [`Terminal::with_options`] for more information.
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub enum Viewport {
/// The viewport is fullscreen
#[default]
Fullscreen,
/// The viewport is inline with the rest of the terminal.
///
/// The viewport's height is fixed and specified in number of lines. The width is the same as
/// the terminal's width. The viewport is drawn below the cursor position.
Inline(u16),
/// The viewport is drawn in a fixed area of the terminal. The area is specified by a [`Rect`].
Fixed(Rect),
}
impl fmt::Display for Viewport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Viewport::Fullscreen => write!(f, "Fullscreen"),
Viewport::Inline(height) => write!(f, "Inline({})", height),
Viewport::Fixed(area) => write!(f, "Fixed({})", area),
}
}
}
/// Options to pass to [`Terminal::with_options`]
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct TerminalOptions {
/// Viewport used to draw to the terminal
pub viewport: Viewport,
}
/// 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.
///
/// The [`Terminal`] 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.
///
/// 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.
///
/// Applications should detect terminal resizes and call [`Terminal::draw`] to redraw the
/// application with the new size. This will automatically resize the internal buffers to match the
/// new size for inline and fullscreen viewports. Fixed viewports are not resized automatically.
///
/// # Examples
///
/// ```rust,no_run
/// use std::io::stdout;
///
/// use ratatui::{prelude::*, widgets::Paragraph};
///
/// let backend = CrosstermBackend::new(stdout());
/// let mut terminal = Terminal::new(backend)?;
/// terminal.draw(|frame| {
/// let area = frame.size();
/// frame.render_widget(Paragraph::new("Hello World!"), area);
/// frame.set_cursor(0, 0);
/// })?;
/// # std::io::Result::Ok(())
/// ```
///
/// [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, Default, Clone, Eq, PartialEq, Hash)]
pub struct Terminal<B>
where
B: Backend,
{
/// The backend used to interface with the terminal
backend: B,
/// Holds the results of the current and previous draw calls. The two are compared at the end
/// of each draw pass to output the necessary updates to the terminal
buffers: [Buffer; 2],
/// Index of the current buffer in the previous array
current: usize,
/// Whether the cursor is currently hidden
hidden_cursor: bool,
/// Viewport
viewport: Viewport,
/// Area of the viewport
viewport_area: Rect,
/// Last known size of the terminal. Used to detect if the internal buffers have to be resized.
last_known_size: Rect,
/// Last known position of the cursor. Used to find the new area when the viewport is inlined
/// and the terminal resized.
last_known_cursor_pos: (u16, u16),
}
impl<B> Drop for Terminal<B>
where
B: Backend,
{
fn drop(&mut self) {
// Attempt to restore the cursor state
if self.hidden_cursor {
if let Err(err) = self.show_cursor() {
eprintln!("Failed to show the cursor: {err}");
}
}
}
}
impl<B> Terminal<B>
where
B: Backend,
{
/// Creates a new [`Terminal`] with the given [`Backend`] with a full screen viewport.
///
/// # Example
///
/// ```rust,no_run
/// # use std::io::stdout;
/// # use ratatui::prelude::*;
/// let backend = CrosstermBackend::new(stdout());
/// let terminal = Terminal::new(backend)?;
/// # std::io::Result::Ok(())
/// ```
pub fn new(backend: B) -> io::Result<Terminal<B>> {
Terminal::with_options(
backend,
TerminalOptions {
viewport: Viewport::Fullscreen,
},
)
}
/// Creates a new [`Terminal`] with the given [`Backend`] and [`TerminalOptions`].
///
/// # Example
///
/// ```rust
/// # use std::io::stdout;
/// # use ratatui::{prelude::*, backend::TestBackend};
/// let backend = CrosstermBackend::new(stdout());
/// let viewport = Viewport::Fixed(Rect::new(0, 0, 10, 10));
/// let terminal = Terminal::with_options(backend, TerminalOptions { viewport })?;
/// # std::io::Result::Ok(())
/// ```
pub fn with_options(mut backend: B, options: TerminalOptions) -> io::Result<Terminal<B>> {
let size = match options.viewport {
Viewport::Fullscreen | Viewport::Inline(_) => backend.size()?,
Viewport::Fixed(area) => area,
};
let (viewport_area, cursor_pos) = match options.viewport {
Viewport::Fullscreen => (size, (0, 0)),
Viewport::Inline(height) => compute_inline_size(&mut backend, height, size, 0)?,
Viewport::Fixed(area) => (area, (area.left(), area.top())),
};
Ok(Terminal {
backend,
buffers: [Buffer::empty(viewport_area), Buffer::empty(viewport_area)],
current: 0,
hidden_cursor: false,
viewport: options.viewport,
viewport_area,
last_known_size: size,
last_known_cursor_pos: cursor_pos,
})
}
/// Get a Frame object which provides a consistent view into the terminal state for rendering.
pub fn get_frame(&mut self) -> Frame {
Frame {
cursor_position: None,
viewport_area: self.viewport_area,
buffer: self.current_buffer_mut(),
}
}
/// Gets the current buffer as a mutable reference.
pub fn current_buffer_mut(&mut self) -> &mut Buffer {
&mut self.buffers[self.current]
}
/// Gets the backend
pub fn backend(&self) -> &B {
&self.backend
}
/// Gets the backend as a mutable reference
pub fn backend_mut(&mut self) -> &mut B {
&mut self.backend
}
/// Obtains a difference between the previous and the current buffer and passes it to the
/// current backend for drawing.
pub fn flush(&mut self) -> io::Result<()> {
let previous_buffer = &self.buffers[1 - self.current];
let current_buffer = &self.buffers[self.current];
let updates = previous_buffer.diff(current_buffer);
if let Some((col, row, _)) = updates.last() {
self.last_known_cursor_pos = (*col, *row);
}
self.backend.draw(updates.into_iter())
}
/// Updates the Terminal so that internal buffers match the requested size.
///
/// Requested size will be saved so the size can remain consistent when rendering. This leads
/// to a full clear of the screen.
pub fn resize(&mut self, size: Rect) -> io::Result<()> {
let next_area = match self.viewport {
Viewport::Fullscreen => size,
Viewport::Inline(height) => {
let offset_in_previous_viewport = self
.last_known_cursor_pos
.1
.saturating_sub(self.viewport_area.top());
compute_inline_size(&mut self.backend, height, size, offset_in_previous_viewport)?.0
}
Viewport::Fixed(area) => area,
};
self.set_viewport_area(next_area);
self.clear()?;
self.last_known_size = size;
Ok(())
}
fn set_viewport_area(&mut self, area: Rect) {
self.buffers[self.current].resize(area);
self.buffers[1 - self.current].resize(area);
self.viewport_area = area;
}
/// Queries the backend for size and resizes if it doesn't match the previous size.
pub fn autoresize(&mut self) -> io::Result<()> {
// fixed viewports do not get autoresized
if matches!(self.viewport, Viewport::Fullscreen | Viewport::Inline(_)) {
let size = self.size()?;
if size != self.last_known_size {
self.resize(size)?;
}
};
Ok(())
}
/// Synchronizes terminal size, calls the rendering closure, flushes the current internal state
/// and prepares for the next draw call.
///
/// This is the main entry point for drawing to the terminal.
///
/// # Examples
///
/// ```rust,no_run
/// # use std::io::stdout;
/// # use ratatui::{prelude::*, widgets::Paragraph};
/// let backend = CrosstermBackend::new(stdout());
/// let mut terminal = Terminal::new(backend)?;
/// terminal.draw(|frame| {
/// let area = frame.size();
/// frame.render_widget(Paragraph::new("Hello World!"), area);
/// frame.set_cursor(0, 0);
/// })?;
/// # std::io::Result::Ok(())
/// ```
pub fn draw<F>(&mut self, f: F) -> io::Result<CompletedFrame>
where
F: FnOnce(&mut Frame),
{
// Autoresize - otherwise we get glitches if shrinking or potential desync between widgets
// and the terminal (if growing), which may OOB.
self.autoresize()?;
let mut frame = self.get_frame();
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
// Buffer. Thus, we're taking the important data out of the Frame and dropping it.
let cursor_position = frame.cursor_position;
// Draw to stdout
self.flush()?;
match cursor_position {
None => self.hide_cursor()?,
Some((x, y)) => {
self.show_cursor()?;
self.set_cursor(x, y)?;
}
}
self.swap_buffers();
// Flush
self.backend.flush()?;
Ok(CompletedFrame {
buffer: &self.buffers[1 - self.current],
area: self.last_known_size,
})
}
/// Hides the cursor.
pub fn hide_cursor(&mut self) -> io::Result<()> {
self.backend.hide_cursor()?;
self.hidden_cursor = true;
Ok(())
}
/// Shows the cursor.
pub fn show_cursor(&mut self) -> io::Result<()> {
self.backend.show_cursor()?;
self.hidden_cursor = false;
Ok(())
}
/// Gets the current cursor position.
///
/// This is the position of the cursor after the last draw call and is returned as a tuple of
/// `(x, y)` coordinates.
pub fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
self.backend.get_cursor()
}
/// Sets the cursor position.
pub fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
self.backend.set_cursor(x, y)?;
self.last_known_cursor_pos = (x, y);
Ok(())
}
/// Clear the terminal and force a full redraw on the next draw call.
pub fn clear(&mut self) -> io::Result<()> {
match self.viewport {
Viewport::Fullscreen => self.backend.clear_region(ClearType::All)?,
Viewport::Inline(_) => {
self.backend
.set_cursor(self.viewport_area.left(), self.viewport_area.top())?;
self.backend.clear_region(ClearType::AfterCursor)?;
}
Viewport::Fixed(area) => {
for row in area.top()..area.bottom() {
self.backend.set_cursor(0, row)?;
self.backend.clear_region(ClearType::AfterCursor)?;
}
}
}
// Reset the back buffer to make sure the next update will redraw everything.
self.buffers[1 - self.current].reset();
Ok(())
}
/// Clears the inactive buffer and swaps it with the current buffer
pub fn swap_buffers(&mut self) {
self.buffers[1 - self.current].reset();
self.current = 1 - self.current;
}
/// Queries the real size of the backend.
pub fn size(&self) -> io::Result<Rect> {
self.backend.size()
}
/// Insert some content before the current inline viewport. This has no effect when the
/// viewport is fullscreen.
///
/// This function scrolls down the current viewport by the given height. The newly freed space
/// is then made available to the `draw_fn` closure through a writable `Buffer`.
///
/// Before:
/// ```ignore
/// +-------------------+
/// | |
/// | viewport |
/// | |
/// +-------------------+
/// ```
///
/// After:
/// ```ignore
/// +-------------------+
/// | buffer |
/// +-------------------+
/// +-------------------+
/// | |
/// | viewport |
/// | |
/// +-------------------+
/// ```
///
/// # Examples
///
/// ## Insert a single line before the current viewport
///
/// ```rust
/// # use ratatui::{backend::TestBackend, prelude::*, widgets::*};
/// # let backend = TestBackend::new(10, 10);
/// # let mut terminal = Terminal::new(backend).unwrap();
/// terminal.insert_before(1, |buf| {
/// Paragraph::new(Line::from(vec![
/// Span::raw("This line will be added "),
/// Span::styled("before", Style::default().fg(Color::Blue)),
/// Span::raw(" the current viewport"),
/// ]))
/// .render(buf.area, buf);
/// });
/// ```
pub fn insert_before<F>(&mut self, height: u16, draw_fn: F) -> io::Result<()>
where
F: FnOnce(&mut Buffer),
{
if !matches!(self.viewport, Viewport::Inline(_)) {
return Ok(());
}
// Clear the viewport off the screen
self.clear()?;
// Move the viewport by height, but don't move it past the bottom of the terminal
let viewport_at_bottom = self.last_known_size.bottom() - self.viewport_area.height;
self.set_viewport_area(Rect {
y: self
.viewport_area
.y
.saturating_add(height)
.min(viewport_at_bottom),
..self.viewport_area
});
// Draw contents into buffer
let area = Rect {
x: self.viewport_area.left(),
y: 0,
width: self.viewport_area.width,
height,
};
let mut buffer = Buffer::empty(area);
draw_fn(&mut buffer);
// Split buffer into screen-sized chunks and draw
let max_chunk_size = (self.viewport_area.top() * area.width).into();
for buffer_content_chunk in buffer.content.chunks(max_chunk_size) {
let chunk_size = buffer_content_chunk.len() as u16 / area.width;
self.backend
.append_lines(self.viewport_area.height.saturating_sub(1) + chunk_size)?;
let iter = buffer_content_chunk.iter().enumerate().map(|(i, c)| {
let (x, y) = buffer.pos_of(i);
(
x,
self.viewport_area.top().saturating_sub(chunk_size) + y,
c,
)
});
self.backend.draw(iter)?;
self.backend.flush()?;
self.set_cursor(self.viewport_area.left(), self.viewport_area.top())?;
}
Ok(())
}
}
fn compute_inline_size<B: Backend>(
backend: &mut B,
height: u16,
size: Rect,
offset_in_previous_viewport: u16,
) -> io::Result<(Rect, (u16, u16))> {
let pos = backend.get_cursor()?;
let mut row = pos.1;
let max_height = size.height.min(height);
let lines_after_cursor = height
.saturating_sub(offset_in_previous_viewport)
.saturating_sub(1);
backend.append_lines(lines_after_cursor)?;
let available_lines = size.height.saturating_sub(row).saturating_sub(1);
let missing_lines = lines_after_cursor.saturating_sub(available_lines);
if missing_lines > 0 {
row = row.saturating_sub(missing_lines);
}
row = row.saturating_sub(offset_in_previous_viewport);
Ok((
Rect {
x: 0,
y: row,
width: size.width,
height: max_height,
},
pos,
))
}
/// A consistent view into the terminal state for rendering a single frame.
///
/// 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 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.
///
/// [`Buffer`]: crate::buffer::Buffer
#[derive(Debug, Hash)]
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 Frame<'_> {
/// The size of the current frame
///
/// This is guaranteed not to change during rendering, so may be called multiple times.
///
/// If your app listens for a resize event from the backend, it should ignore the values from
/// the event for any calculations that are used to render the current frame and use this value
/// instead as this is the size of the buffer that is used to render the current frame.
pub fn size(&self) -> Rect {
self.viewport_area
}
/// Render a [`Widget`] to the current buffer using [`Widget::render`].
///
/// Usually the area argument is the size of the current frame or a sub-area of the current
/// frame (which can be obtained using [`Layout`] to split the total area).
///
/// # Example
///
/// ```rust
/// # use ratatui::{backend::TestBackend, prelude::*, widgets::Block};
/// # let backend = TestBackend::new(5, 5);
/// # let mut terminal = Terminal::new(backend).unwrap();
/// # let mut frame = terminal.get_frame();
/// let block = Block::default();
/// let area = Rect::new(0, 0, 5, 5);
/// frame.render_widget(block, area);
/// ```
///
/// [`Layout`]: crate::layout::Layout
pub fn render_widget<W>(&mut self, widget: W, area: Rect)
where
W: Widget,
{
widget.render(area, self.buffer);
}
/// Render a [`StatefulWidget`] to the current buffer using [`StatefulWidget::render`].
///
/// Usually the area argument is the size of the current frame or a sub-area of the current
/// frame (which can be obtained using [`Layout`] to split the total area).
///
/// The last argument should be an instance of the [`StatefulWidget::State`] associated to the
/// given [`StatefulWidget`].
///
/// # Examples
///
/// ```rust
/// # use ratatui::{backend::TestBackend, prelude::*, widgets::*};
/// # let backend = TestBackend::new(5, 5);
/// # let mut terminal = Terminal::new(backend).unwrap();
/// # let mut frame = terminal.get_frame();
/// let mut state = ListState::default().with_selected(Some(1));
/// let list = List::new(vec![ListItem::new("Item 1"), ListItem::new("Item 2")]);
/// let area = Rect::new(0, 0, 5, 5);
/// frame.render_stateful_widget(list, area, &mut state);
/// ```
///
/// [`Layout`]: crate::layout::Layout
pub fn render_stateful_widget<W>(&mut self, widget: W, area: Rect, state: &mut W::State)
where
W: StatefulWidget,
{
widget.render(area, self.buffer, state);
}
/// After drawing this frame, make the cursor visible and put it at the specified (x, y)
/// coordinates. If this method is not called, the cursor will be hidden.
///
/// Note that this will interfere with calls to `Terminal::hide_cursor()`,
/// `Terminal::show_cursor()`, and `Terminal::set_cursor()`. Pick one of the APIs and stick
/// with it.
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
/// [`Terminal::draw`] call have been applied. Therefore, it is only valid until the next call to
/// [`Terminal::draw`].
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct CompletedFrame<'a> {
/// The buffer that was used to draw the last frame.
pub buffer: &'a Buffer,
/// The size of the last frame.
pub area: Rect,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn viewport_to_string() {
assert_eq!(Viewport::Fullscreen.to_string(), "Fullscreen");
assert_eq!(Viewport::Inline(5).to_string(), "Inline(5)");
assert_eq!(
Viewport::Fixed(Rect::new(0, 0, 5, 5)).to_string(),
"Fixed(5x5+0+0)"
);
}
}

View File

@@ -1,155 +0,0 @@
use crate::{
prelude::*,
widgets::{StatefulWidget, Widget},
};
/// A consistent view into the terminal state for rendering a single frame.
///
/// 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 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.
///
/// [`Buffer`]: crate::buffer::Buffer
#[derive(Debug, Hash)]
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()`.
pub(crate) cursor_position: Option<(u16, u16)>,
/// The area of the viewport
pub(crate) viewport_area: Rect,
/// The buffer that is used to draw the current frame
pub(crate) buffer: &'a mut Buffer,
/// The frame count indicating the sequence number of this frame.
pub(crate) count: usize,
}
/// `CompletedFrame` represents the state of the terminal after all changes performed in the last
/// [`Terminal::draw`] call have been applied. Therefore, it is only valid until the next call to
/// [`Terminal::draw`].
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct CompletedFrame<'a> {
/// The buffer that was used to draw the last frame.
pub buffer: &'a Buffer,
/// The size of the last frame.
pub area: Rect,
/// The frame count indicating the sequence number of this frame.
pub count: usize,
}
impl Frame<'_> {
/// The size of the current frame
///
/// This is guaranteed not to change during rendering, so may be called multiple times.
///
/// If your app listens for a resize event from the backend, it should ignore the values from
/// the event for any calculations that are used to render the current frame and use this value
/// instead as this is the size of the buffer that is used to render the current frame.
pub fn size(&self) -> Rect {
self.viewport_area
}
/// Render a [`Widget`] to the current buffer using [`Widget::render`].
///
/// Usually the area argument is the size of the current frame or a sub-area of the current
/// frame (which can be obtained using [`Layout`] to split the total area).
///
/// # Example
///
/// ```rust
/// # use ratatui::{backend::TestBackend, prelude::*, widgets::Block};
/// # let backend = TestBackend::new(5, 5);
/// # let mut terminal = Terminal::new(backend).unwrap();
/// # let mut frame = terminal.get_frame();
/// let block = Block::default();
/// let area = Rect::new(0, 0, 5, 5);
/// frame.render_widget(block, area);
/// ```
///
/// [`Layout`]: crate::layout::Layout
pub fn render_widget<W>(&mut self, widget: W, area: Rect)
where
W: Widget,
{
widget.render(area, self.buffer);
}
/// Render a [`StatefulWidget`] to the current buffer using [`StatefulWidget::render`].
///
/// Usually the area argument is the size of the current frame or a sub-area of the current
/// frame (which can be obtained using [`Layout`] to split the total area).
///
/// The last argument should be an instance of the [`StatefulWidget::State`] associated to the
/// given [`StatefulWidget`].
///
/// # Examples
///
/// ```rust
/// # use ratatui::{backend::TestBackend, prelude::*, widgets::*};
/// # let backend = TestBackend::new(5, 5);
/// # let mut terminal = Terminal::new(backend).unwrap();
/// # let mut frame = terminal.get_frame();
/// let mut state = ListState::default().with_selected(Some(1));
/// let list = List::new(vec![ListItem::new("Item 1"), ListItem::new("Item 2")]);
/// let area = Rect::new(0, 0, 5, 5);
/// frame.render_stateful_widget(list, area, &mut state);
/// ```
///
/// [`Layout`]: crate::layout::Layout
pub fn render_stateful_widget<W>(&mut self, widget: W, area: Rect, state: &mut W::State)
where
W: StatefulWidget,
{
widget.render(area, self.buffer, state);
}
/// After drawing this frame, make the cursor visible and put it at the specified (x, y)
/// coordinates. If this method is not called, the cursor will be hidden.
///
/// Note that this will interfere with calls to `Terminal::hide_cursor()`,
/// `Terminal::show_cursor()`, and `Terminal::set_cursor()`. Pick one of the APIs and stick
/// with it.
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
}
/// Returns the current frame count.
///
/// This method provides access to the frame count, which is a sequence number indicating
/// how many frames have been rendered up to (but not including) this one. It can be used
/// for purposes such as animation, performance tracking, or debugging.
///
/// Each time a frame has been rendered, this count is incremented,
/// providing a consistent way to reference the order and number of frames processed by the
/// terminal. When count reaches its maximum value (usize::MAX), it wraps around to zero.
///
/// This count is particularly useful when dealing with dynamic content or animations where the
/// state of the display changes over time. By tracking the frame count, developers can
/// synchronize updates or changes to the content with the rendering process.
///
/// # Examples
///
/// ```rust
/// # use ratatui::{backend::TestBackend, prelude::*, widgets::*};
/// # let backend = TestBackend::new(5, 5);
/// # let mut terminal = Terminal::new(backend).unwrap();
/// # let mut frame = terminal.get_frame();
/// let current_count = frame.count();
/// println!("Current frame count: {}", current_count);
/// ```
pub fn count(&self) -> usize {
self.count
}
}

View File

@@ -1,494 +0,0 @@
use std::io;
use crate::{backend::ClearType, prelude::*};
/// 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.
///
/// The [`Terminal`] 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.
///
/// 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.
///
/// Applications should detect terminal resizes and call [`Terminal::draw`] to redraw the
/// application with the new size. This will automatically resize the internal buffers to match the
/// new size for inline and fullscreen viewports. Fixed viewports are not resized automatically.
///
/// # Examples
///
/// ```rust,no_run
/// use std::io::stdout;
///
/// use ratatui::{prelude::*, widgets::Paragraph};
///
/// let backend = CrosstermBackend::new(stdout());
/// let mut terminal = Terminal::new(backend)?;
/// terminal.draw(|frame| {
/// let area = frame.size();
/// frame.render_widget(Paragraph::new("Hello World!"), area);
/// frame.set_cursor(0, 0);
/// })?;
/// # std::io::Result::Ok(())
/// ```
///
/// [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, Default, Clone, Eq, PartialEq, Hash)]
pub struct Terminal<B>
where
B: Backend,
{
/// The backend used to interface with the terminal
backend: B,
/// Holds the results of the current and previous draw calls. The two are compared at the end
/// of each draw pass to output the necessary updates to the terminal
buffers: [Buffer; 2],
/// Index of the current buffer in the previous array
current: usize,
/// Whether the cursor is currently hidden
hidden_cursor: bool,
/// Viewport
viewport: Viewport,
/// Area of the viewport
viewport_area: Rect,
/// Last known size of the terminal. Used to detect if the internal buffers have to be resized.
last_known_size: Rect,
/// Last known position of the cursor. Used to find the new area when the viewport is inlined
/// and the terminal resized.
last_known_cursor_pos: (u16, u16),
/// Number of frames rendered up until current time.
frame_count: usize,
}
/// Options to pass to [`Terminal::with_options`]
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Options {
/// Viewport used to draw to the terminal
pub viewport: Viewport,
}
impl<B> Drop for Terminal<B>
where
B: Backend,
{
fn drop(&mut self) {
// Attempt to restore the cursor state
if self.hidden_cursor {
if let Err(err) = self.show_cursor() {
eprintln!("Failed to show the cursor: {err}");
}
}
}
}
impl<B> Terminal<B>
where
B: Backend,
{
/// Creates a new [`Terminal`] with the given [`Backend`] with a full screen viewport.
///
/// # Example
///
/// ```rust,no_run
/// # use std::io::stdout;
/// # use ratatui::prelude::*;
/// let backend = CrosstermBackend::new(stdout());
/// let terminal = Terminal::new(backend)?;
/// # std::io::Result::Ok(())
/// ```
pub fn new(backend: B) -> io::Result<Terminal<B>> {
Terminal::with_options(
backend,
TerminalOptions {
viewport: Viewport::Fullscreen,
},
)
}
/// Creates a new [`Terminal`] with the given [`Backend`] and [`TerminalOptions`].
///
/// # Example
///
/// ```rust
/// # use std::io::stdout;
/// # use ratatui::{prelude::*, backend::TestBackend};
/// let backend = CrosstermBackend::new(stdout());
/// let viewport = Viewport::Fixed(Rect::new(0, 0, 10, 10));
/// let terminal = Terminal::with_options(backend, TerminalOptions { viewport })?;
/// # std::io::Result::Ok(())
/// ```
pub fn with_options(mut backend: B, options: TerminalOptions) -> io::Result<Terminal<B>> {
let size = match options.viewport {
Viewport::Fullscreen | Viewport::Inline(_) => backend.size()?,
Viewport::Fixed(area) => area,
};
let (viewport_area, cursor_pos) = match options.viewport {
Viewport::Fullscreen => (size, (0, 0)),
Viewport::Inline(height) => compute_inline_size(&mut backend, height, size, 0)?,
Viewport::Fixed(area) => (area, (area.left(), area.top())),
};
Ok(Terminal {
backend,
buffers: [Buffer::empty(viewport_area), Buffer::empty(viewport_area)],
current: 0,
hidden_cursor: false,
viewport: options.viewport,
viewport_area,
last_known_size: size,
last_known_cursor_pos: cursor_pos,
frame_count: 0,
})
}
/// Get a Frame object which provides a consistent view into the terminal state for rendering.
pub fn get_frame(&mut self) -> Frame {
let count = self.frame_count;
Frame {
cursor_position: None,
viewport_area: self.viewport_area,
buffer: self.current_buffer_mut(),
count,
}
}
/// Gets the current buffer as a mutable reference.
pub fn current_buffer_mut(&mut self) -> &mut Buffer {
&mut self.buffers[self.current]
}
/// Gets the backend
pub fn backend(&self) -> &B {
&self.backend
}
/// Gets the backend as a mutable reference
pub fn backend_mut(&mut self) -> &mut B {
&mut self.backend
}
/// Obtains a difference between the previous and the current buffer and passes it to the
/// current backend for drawing.
pub fn flush(&mut self) -> io::Result<()> {
let previous_buffer = &self.buffers[1 - self.current];
let current_buffer = &self.buffers[self.current];
let updates = previous_buffer.diff(current_buffer);
if let Some((col, row, _)) = updates.last() {
self.last_known_cursor_pos = (*col, *row);
}
self.backend.draw(updates.into_iter())
}
/// Updates the Terminal so that internal buffers match the requested size.
///
/// Requested size will be saved so the size can remain consistent when rendering. This leads
/// to a full clear of the screen.
pub fn resize(&mut self, size: Rect) -> io::Result<()> {
let next_area = match self.viewport {
Viewport::Fullscreen => size,
Viewport::Inline(height) => {
let offset_in_previous_viewport = self
.last_known_cursor_pos
.1
.saturating_sub(self.viewport_area.top());
compute_inline_size(&mut self.backend, height, size, offset_in_previous_viewport)?.0
}
Viewport::Fixed(area) => area,
};
self.set_viewport_area(next_area);
self.clear()?;
self.last_known_size = size;
Ok(())
}
fn set_viewport_area(&mut self, area: Rect) {
self.buffers[self.current].resize(area);
self.buffers[1 - self.current].resize(area);
self.viewport_area = area;
}
/// Queries the backend for size and resizes if it doesn't match the previous size.
pub fn autoresize(&mut self) -> io::Result<()> {
// fixed viewports do not get autoresized
if matches!(self.viewport, Viewport::Fullscreen | Viewport::Inline(_)) {
let size = self.size()?;
if size != self.last_known_size {
self.resize(size)?;
}
};
Ok(())
}
/// Synchronizes terminal size, calls the rendering closure, flushes the current internal state
/// and prepares for the next draw call.
///
/// This is the main entry point for drawing to the terminal.
///
/// # Examples
///
/// ```rust,no_run
/// # use std::io::stdout;
/// # use ratatui::{prelude::*, widgets::Paragraph};
/// let backend = CrosstermBackend::new(stdout());
/// let mut terminal = Terminal::new(backend)?;
/// terminal.draw(|frame| {
/// let area = frame.size();
/// frame.render_widget(Paragraph::new("Hello World!"), area);
/// frame.set_cursor(0, 0);
/// })?;
/// # std::io::Result::Ok(())
/// ```
pub fn draw<F>(&mut self, f: F) -> io::Result<CompletedFrame>
where
F: FnOnce(&mut Frame),
{
// Autoresize - otherwise we get glitches if shrinking or potential desync between widgets
// and the terminal (if growing), which may OOB.
self.autoresize()?;
let mut frame = self.get_frame();
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
// Buffer. Thus, we're taking the important data out of the Frame and dropping it.
let cursor_position = frame.cursor_position;
// Draw to stdout
self.flush()?;
match cursor_position {
None => self.hide_cursor()?,
Some((x, y)) => {
self.show_cursor()?;
self.set_cursor(x, y)?;
}
}
self.swap_buffers();
// Flush
self.backend.flush()?;
let completed_frame = CompletedFrame {
buffer: &self.buffers[1 - self.current],
area: self.last_known_size,
count: self.frame_count,
};
// increment frame count before returning from draw
self.frame_count = self.frame_count.wrapping_add(1);
Ok(completed_frame)
}
/// Hides the cursor.
pub fn hide_cursor(&mut self) -> io::Result<()> {
self.backend.hide_cursor()?;
self.hidden_cursor = true;
Ok(())
}
/// Shows the cursor.
pub fn show_cursor(&mut self) -> io::Result<()> {
self.backend.show_cursor()?;
self.hidden_cursor = false;
Ok(())
}
/// Gets the current cursor position.
///
/// This is the position of the cursor after the last draw call and is returned as a tuple of
/// `(x, y)` coordinates.
pub fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
self.backend.get_cursor()
}
/// Sets the cursor position.
pub fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
self.backend.set_cursor(x, y)?;
self.last_known_cursor_pos = (x, y);
Ok(())
}
/// Clear the terminal and force a full redraw on the next draw call.
pub fn clear(&mut self) -> io::Result<()> {
match self.viewport {
Viewport::Fullscreen => self.backend.clear_region(ClearType::All)?,
Viewport::Inline(_) => {
self.backend
.set_cursor(self.viewport_area.left(), self.viewport_area.top())?;
self.backend.clear_region(ClearType::AfterCursor)?;
}
Viewport::Fixed(area) => {
for row in area.top()..area.bottom() {
self.backend.set_cursor(0, row)?;
self.backend.clear_region(ClearType::AfterCursor)?;
}
}
}
// Reset the back buffer to make sure the next update will redraw everything.
self.buffers[1 - self.current].reset();
Ok(())
}
/// Clears the inactive buffer and swaps it with the current buffer
pub fn swap_buffers(&mut self) {
self.buffers[1 - self.current].reset();
self.current = 1 - self.current;
}
/// Queries the real size of the backend.
pub fn size(&self) -> io::Result<Rect> {
self.backend.size()
}
/// Insert some content before the current inline viewport. This has no effect when the
/// viewport is fullscreen.
///
/// This function scrolls down the current viewport by the given height. The newly freed space
/// is then made available to the `draw_fn` closure through a writable `Buffer`.
///
/// Before:
/// ```ignore
/// +-------------------+
/// | |
/// | viewport |
/// | |
/// +-------------------+
/// ```
///
/// After:
/// ```ignore
/// +-------------------+
/// | buffer |
/// +-------------------+
/// +-------------------+
/// | |
/// | viewport |
/// | |
/// +-------------------+
/// ```
///
/// # Examples
///
/// ## Insert a single line before the current viewport
///
/// ```rust
/// # use ratatui::{backend::TestBackend, prelude::*, widgets::*};
/// # let backend = TestBackend::new(10, 10);
/// # let mut terminal = Terminal::new(backend).unwrap();
/// terminal.insert_before(1, |buf| {
/// Paragraph::new(Line::from(vec![
/// Span::raw("This line will be added "),
/// Span::styled("before", Style::default().fg(Color::Blue)),
/// Span::raw(" the current viewport"),
/// ]))
/// .render(buf.area, buf);
/// });
/// ```
pub fn insert_before<F>(&mut self, height: u16, draw_fn: F) -> io::Result<()>
where
F: FnOnce(&mut Buffer),
{
if !matches!(self.viewport, Viewport::Inline(_)) {
return Ok(());
}
// Clear the viewport off the screen
self.clear()?;
// Move the viewport by height, but don't move it past the bottom of the terminal
let viewport_at_bottom = self.last_known_size.bottom() - self.viewport_area.height;
self.set_viewport_area(Rect {
y: self
.viewport_area
.y
.saturating_add(height)
.min(viewport_at_bottom),
..self.viewport_area
});
// Draw contents into buffer
let area = Rect {
x: self.viewport_area.left(),
y: 0,
width: self.viewport_area.width,
height,
};
let mut buffer = Buffer::empty(area);
draw_fn(&mut buffer);
// Split buffer into screen-sized chunks and draw
let max_chunk_size = (self.viewport_area.top() * area.width).into();
for buffer_content_chunk in buffer.content.chunks(max_chunk_size) {
let chunk_size = buffer_content_chunk.len() as u16 / area.width;
self.backend
.append_lines(self.viewport_area.height.saturating_sub(1) + chunk_size)?;
let iter = buffer_content_chunk.iter().enumerate().map(|(i, c)| {
let (x, y) = buffer.pos_of(i);
(
x,
self.viewport_area.top().saturating_sub(chunk_size) + y,
c,
)
});
self.backend.draw(iter)?;
self.backend.flush()?;
self.set_cursor(self.viewport_area.left(), self.viewport_area.top())?;
}
Ok(())
}
}
fn compute_inline_size<B: Backend>(
backend: &mut B,
height: u16,
size: Rect,
offset_in_previous_viewport: u16,
) -> io::Result<(Rect, (u16, u16))> {
let pos = backend.get_cursor()?;
let mut row = pos.1;
let max_height = size.height.min(height);
let lines_after_cursor = height
.saturating_sub(offset_in_previous_viewport)
.saturating_sub(1);
backend.append_lines(lines_after_cursor)?;
let available_lines = size.height.saturating_sub(row).saturating_sub(1);
let missing_lines = lines_after_cursor.saturating_sub(available_lines);
if missing_lines > 0 {
row = row.saturating_sub(missing_lines);
}
row = row.saturating_sub(offset_in_previous_viewport);
Ok((
Rect {
x: 0,
y: row,
width: size.width,
height: max_height,
},
pos,
))
}

View File

@@ -1,54 +0,0 @@
use std::fmt;
use crate::prelude::*;
/// Represents the viewport of the terminal. The viewport is the area of the terminal that is
/// currently visible to the user. It can be either fullscreen, inline or fixed.
///
/// When the viewport is fullscreen, the whole terminal is used to draw the application.
///
/// When the viewport is inline, it is drawn inline with the rest of the terminal. The height of
/// the viewport is fixed, but the width is the same as the terminal width.
///
/// When the viewport is fixed, it is drawn in a fixed area of the terminal. The area is specified
/// by a [`Rect`].
///
/// See [`Terminal::with_options`] for more information.
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub enum Viewport {
/// The viewport is fullscreen
#[default]
Fullscreen,
/// The viewport is inline with the rest of the terminal.
///
/// The viewport's height is fixed and specified in number of lines. The width is the same as
/// the terminal's width. The viewport is drawn below the cursor position.
Inline(u16),
/// The viewport is drawn in a fixed area of the terminal. The area is specified by a [`Rect`].
Fixed(Rect),
}
impl fmt::Display for Viewport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Viewport::Fullscreen => write!(f, "Fullscreen"),
Viewport::Inline(height) => write!(f, "Inline({})", height),
Viewport::Fixed(area) => write!(f, "Fixed({})", area),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn viewport_to_string() {
assert_eq!(Viewport::Fullscreen.to_string(), "Fullscreen");
assert_eq!(Viewport::Inline(5).to_string(), "Inline(5)");
assert_eq!(
Viewport::Fixed(Rect::new(0, 0, 5, 5)).to_string(),
"Fixed(5x5+0+0)"
);
}
}

View File

@@ -269,51 +269,61 @@ impl<'a> Line<'a> {
.flat_map(move |span| span.styled_graphemes(style))
}
/// Patches the style of this Line, adding modifiers from the given style.
/// Patches the style of each Span in an existing Line, adding modifiers from the given style.
///
/// This is useful for when you want to apply a style to a line that already has some styling.
/// In contrast to [`Line::style`], this method will not overwrite the existing style, but
/// instead will add the given style's modifiers to this Line's style.
/// instead will add the given style's modifiers to the existing style of each `Span`.
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// let line = Line::styled("My text", Modifier::ITALIC);
/// let style = Style::default()
/// .fg(Color::Yellow)
/// .add_modifier(Modifier::ITALIC);
/// let mut raw_line = Line::from(vec![Span::raw("My"), Span::raw(" text")]);
/// let mut styled_line = Line::from(vec![
/// Span::styled("My", style),
/// Span::styled(" text", style),
/// ]);
///
/// let styled_line = Line::styled("My text", (Color::Yellow, Modifier::ITALIC));
/// assert_ne!(raw_line, styled_line);
///
/// assert_eq!(styled_line, line.patch_style(Color::Yellow));
/// raw_line.patch_style(style);
/// assert_eq!(raw_line, styled_line);
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn patch_style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = self.style.patch(style);
self
pub fn patch_style<S: Into<Style>>(&mut self, style: S) {
let style = style.into();
for span in &mut self.spans {
span.patch_style(style);
}
}
/// Resets the style of this Line.
/// Resets the style of each Span in the Line.
///
/// Equivalent to calling `patch_style(Style::reset())`.
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// # let style = Style::default().yellow();
/// let line = Line::styled("My text", style);
/// let mut line = Line::from(vec![
/// Span::styled("My", Style::default().fg(Color::Yellow)),
/// Span::styled(" text", Style::default().add_modifier(Modifier::BOLD)),
/// ]);
///
/// assert_eq!(Style::reset(), line.reset_style().style);
/// line.reset_style();
/// assert_eq!(Style::reset(), line.spans[0].style);
/// assert_eq!(Style::reset(), line.spans[1].style);
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn reset_style(self) -> Self {
self.patch_style(Style::reset())
pub fn reset_style(&mut self) {
for span in &mut self.spans {
span.reset_style();
}
}
}
@@ -369,7 +379,7 @@ impl Widget for Line<'_> {
let span_width = span.width() as u16;
let span_area = Rect {
x,
width: span_width.min(area.right() - x),
width: span_width,
..area
};
span.render(span_area, buf);
@@ -381,15 +391,6 @@ impl Widget for Line<'_> {
}
}
impl std::fmt::Display for Line<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for span in &self.spans {
write!(f, "{span}")?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -486,21 +487,31 @@ mod tests {
#[test]
fn patch_style() {
let raw_line = Line::styled("foobar", Color::Yellow);
let styled_line = Line::styled("foobar", (Color::Yellow, Modifier::ITALIC));
let style = Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::ITALIC);
let mut raw_line = Line::from(vec![Span::raw("My"), Span::raw(" text")]);
let styled_line = Line::from(vec![
Span::styled("My", style),
Span::styled(" text", style),
]);
assert_ne!(raw_line, styled_line);
let raw_line = raw_line.patch_style(Modifier::ITALIC);
raw_line.patch_style(style);
assert_eq!(raw_line, styled_line);
}
#[test]
fn reset_style() {
let line =
Line::styled("foobar", Style::default().yellow().on_red().italic()).reset_style();
let mut line = Line::from(vec![
Span::styled("My", Style::default().fg(Color::Yellow)),
Span::styled(" text", Style::default().add_modifier(Modifier::BOLD)),
]);
assert_eq!(Style::reset(), line.style);
line.reset_style();
assert_eq!(Style::reset(), line.spans[0].style);
assert_eq!(Style::reset(), line.spans[1].style);
}
#[test]
@@ -574,28 +585,6 @@ mod tests {
);
}
#[test]
fn display_line_from_vec() {
let line_from_vec = Line::from(vec![Span::raw("Hello,"), Span::raw(" world!")]);
assert_eq!(format!("{line_from_vec}"), "Hello, world!");
}
#[test]
fn display_styled_line() {
let styled_line = Line::styled("Hello, world!", Style::new().green().italic());
assert_eq!(format!("{styled_line}"), "Hello, world!");
}
#[test]
fn display_line_from_styled_span() {
let styled_span = Span::styled("Hello, world!", Style::new().green().italic());
let line_from_styled_span = Line::from(styled_span);
assert_eq!(format!("{line_from_styled_span}"), "Hello, world!");
}
mod widget {
use super::*;
use crate::assert_buffer_eq;
@@ -635,9 +624,11 @@ mod tests {
#[test]
fn render_truncates() {
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 1));
Line::from("Hello world!").render(Rect::new(0, 0, 5, 1), &mut buf);
let expected = Buffer::with_lines(vec!["Hello "]);
let mut buf = Buffer::empty(Rect::new(0, 0, 11, 1));
hello_world().render(Rect::new(0, 0, 11, 1), &mut buf);
let mut expected = Buffer::with_lines(vec!["Hello world"]);
expected.set_style(Rect::new(0, 0, 6, 1), BLUE.italic());
expected.set_style(Rect::new(6, 0, 5, 1), GREEN.italic());
assert_buffer_eq!(buf, expected);
}

View File

@@ -193,42 +193,32 @@ impl<'a> Span<'a> {
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Example
///
/// ```rust
/// # use ratatui::prelude::*;
/// let span = Span::styled("test content", Style::new().green().italic())
/// .patch_style(Style::new().red().on_yellow().bold());
/// let mut span = Span::styled("test content", Style::new().green().italic());
/// span.patch_style(Style::new().red().on_yellow().bold());
/// assert_eq!(span.style, Style::new().red().on_yellow().italic().bold());
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn patch_style<S: Into<Style>>(mut self, style: S) -> Self {
pub fn patch_style<S: Into<Style>>(&mut self, style: S) {
self.style = self.style.patch(style);
self
}
/// Resets the style of the Span.
///
/// This is Equivalent to calling `patch_style(Style::reset())`.
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Example
///
/// ```rust
/// # use ratatui::prelude::*;
/// let span = Span::styled(
/// "Test Content",
/// Style::new().dark_gray().on_yellow().italic(),
/// )
/// .reset_style();
/// let mut span = Span::styled("Test Content", Style::new().green().on_yellow().italic());
/// span.reset_style();
/// assert_eq!(span.style, Style::reset());
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn reset_style(self) -> Self {
self.patch_style(Style::reset())
pub fn reset_style(&mut self) {
self.patch_style(Style::reset());
}
/// Returns the unicode width of the content held by this span.
@@ -331,12 +321,6 @@ impl Widget for Span<'_> {
}
}
impl std::fmt::Display for Span<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", &self.content)
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -426,14 +410,15 @@ mod tests {
#[test]
fn reset_style() {
let span = Span::styled("test content", Style::new().green()).reset_style();
let mut span = Span::styled("test content", Style::new().green());
span.reset_style();
assert_eq!(span.style, Style::reset());
}
#[test]
fn patch_style() {
let span = Span::styled("test content", Style::new().green().on_yellow())
.patch_style(Style::new().red().bold());
let mut span = Span::styled("test content", Style::new().green().on_yellow());
span.patch_style(Style::new().red().bold());
assert_eq!(span.style, Style::new().red().on_yellow().bold());
}
@@ -455,19 +440,6 @@ mod tests {
assert_eq!(stylized.content, Cow::Borrowed("test content"));
assert_eq!(stylized.style, Style::new().green().on_yellow().bold());
}
#[test]
fn display_span() {
let span = Span::raw("test content");
assert_eq!(format!("{span}"), "test content");
}
#[test]
fn display_styled_span() {
let stylized_span = Span::styled("stylized test content", Style::new().green());
assert_eq!(format!("{stylized_span}"), "stylized test content");
}
mod widget {
use super::*;
@@ -493,13 +465,11 @@ mod tests {
fn render_truncates_too_long_content() {
let style = Style::new().green().on_yellow();
let span = Span::styled("test content", style);
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 1));
span.render(Rect::new(0, 0, 5, 1), &mut buf);
let mut expected = Buffer::with_lines(vec![Line::from("test ")]);
expected.set_style(Rect::new(0, 0, 5, 1), (Color::Green, Color::Yellow));
span.render(buf.area, &mut buf);
let expected =
Buffer::with_lines(vec![Line::from(vec!["test conte".green().on_yellow()])]);
assert_buffer_eq!(buf, expected);
}

View File

@@ -1,32 +1,14 @@
#![warn(missing_docs)]
use std::borrow::Cow;
use itertools::{Itertools, Position};
use crate::{prelude::*, widgets::Widget};
use crate::prelude::*;
/// A string split over multiple lines where each line is composed of several clusters, each with
/// their own style.
///
/// A [`Text`], like a [`Line`], can be constructed using one of the many `From` implementations
/// A [`Text`], like a [`Span`], can be constructed using one of the many `From` implementations
/// or via the [`Text::raw`] and [`Text::styled`] methods. Helpfully, [`Text`] also implements
/// [`core::iter::Extend`] which enables the concatenation of several [`Text`] blocks.
///
/// The text's [`Style`] is used by the rendering widget to determine how to style the text. Each
/// [`Line`] in the text will be styled with the [`Style`] of the text, and then with its own
/// [`Style`]. `Text` also implements [`Styled`] which means you can use the methods of the
/// [`Stylize`] trait.
///
/// The text's [`Alignment`] can be set using [`Text::alignment`]. Lines composing the text can
/// also be individually aligned with [`Line::alignment`].
///
/// `Text` implements the [`Widget`] trait, which means it can be rendered to a [`Buffer`].
/// Usually apps will use the [`Paragraph`] widget instead of rendering a `Text` directly as it
/// provides more functionality.
///
/// [`Paragraph`]: crate::widgets::Paragraph
/// [`Widget`]: crate::widgets::Widget
///
/// ```rust
/// use ratatui::prelude::*;
///
@@ -48,12 +30,7 @@ use crate::{prelude::*, widgets::Widget};
/// ```
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Text<'a> {
/// The lines that make up this piece of text.
pub lines: Vec<Line<'a>>,
/// The style of this text.
pub style: Style,
/// The alignment of this text.
pub alignment: Option<Alignment>,
}
impl<'a> Text<'a> {
@@ -100,7 +77,9 @@ impl<'a> Text<'a> {
T: Into<Cow<'a, str>>,
S: Into<Style>,
{
Text::raw(content).patch_style(style)
let mut text = Text::raw(content);
text.patch_style(style);
text
}
/// Returns the max width of all the lines.
@@ -129,131 +108,54 @@ impl<'a> Text<'a> {
self.lines.len()
}
/// Sets the style of this text.
///
/// Defaults to [`Style::default()`].
///
/// Note: This field was added in v0.26.0. Prior to that, the style of a text was determined
/// only by the style of each [`Line`] contained in the line. For this reason, this field may
/// not be supported by all widgets (outside of the `ratatui` crate itself).
/// Patches the style of each line in an existing Text, adding modifiers from the given style.
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// # Examples
/// ```rust
/// # use ratatui::prelude::*;
/// let mut line = Text::from("foo").style(Style::new().red());
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = style.into();
self
}
/// Patches the style of this Text, adding modifiers from the given style.
///
/// This is useful for when you want to apply a style to a text that already has some styling.
/// In contrast to [`Text::style`], this method will not overwrite the existing style, but
/// instead will add the given style's modifiers to this text's style.
///
/// `Text` also implements [`Styled`] which means you can use the methods of the [`Stylize`]
/// trait.
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// let raw_text = Text::styled("The first line\nThe second line", Modifier::ITALIC);
/// let styled_text = Text::styled(
/// String::from("The first line\nThe second line"),
/// (Color::Yellow, Modifier::ITALIC),
/// );
/// let style = Style::default()
/// .fg(Color::Yellow)
/// .add_modifier(Modifier::ITALIC);
/// let mut raw_text = Text::raw("The first line\nThe second line");
/// let styled_text = Text::styled(String::from("The first line\nThe second line"), style);
/// assert_ne!(raw_text, styled_text);
///
/// let raw_text = raw_text.patch_style(Color::Yellow);
/// raw_text.patch_style(style);
/// assert_eq!(raw_text, styled_text);
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn patch_style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = self.style.patch(style);
self
pub fn patch_style<S: Into<Style>>(&mut self, style: S) {
let style = style.into();
for line in &mut self.lines {
line.patch_style(style);
}
}
/// Resets the style of the Text.
///
/// Equivalent to calling [`patch_style(Style::reset())`](Text::patch_style).
///
/// This is a fluent setter method which must be chained or used as it consumes self
/// Equivalent to calling `patch_style(Style::reset())`.
///
/// ## Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// let text = Text::styled(
/// "The first line\nThe second line",
/// (Color::Yellow, Modifier::ITALIC),
/// );
/// let style = Style::default()
/// .fg(Color::Yellow)
/// .add_modifier(Modifier::ITALIC);
/// let mut text = Text::styled("The first line\nThe second line", style);
///
/// let text = text.reset_style();
/// assert_eq!(Style::reset(), text.style);
/// text.reset_style();
/// for line in &text.lines {
/// for span in &line.spans {
/// assert_eq!(Style::reset(), span.style);
/// }
/// }
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn reset_style(self) -> Self {
self.patch_style(Style::reset())
}
/// Sets the alignment for this text.
///
/// Defaults to: [`None`], meaning the alignment is determined by the rendering widget.
///
/// Alignment can be set individually on each line to override this text's alignment.
///
/// # Examples
///
/// Set alignment to the whole text.
///
/// ```rust
/// # use ratatui::prelude::*;
/// let mut text = Text::from("Hi, what's up?");
/// assert_eq!(None, text.alignment);
/// assert_eq!(
/// Some(Alignment::Right),
/// text.alignment(Alignment::Right).alignment
/// )
/// ```
///
/// Set a default alignment and override it on a per line basis.
///
/// ```rust
/// # use ratatui::prelude::*;
/// let text = Text::from(vec![
/// Line::from("left").alignment(Alignment::Left),
/// Line::from("default"),
/// Line::from("default"),
/// Line::from("right").alignment(Alignment::Right),
/// ])
/// .alignment(Alignment::Center);
/// ```
///
/// Will render the following
///
/// ```plain
/// left
/// default
/// default
/// right
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn alignment(self, alignment: Alignment) -> Self {
Self {
alignment: Some(alignment),
..self
pub fn reset_style(&mut self) {
for line in &mut self.lines {
line.reset_style();
}
}
}
@@ -280,26 +182,19 @@ impl<'a> From<Span<'a>> for Text<'a> {
fn from(span: Span<'a>) -> Text<'a> {
Text {
lines: vec![Line::from(span)],
..Default::default()
}
}
}
impl<'a> From<Line<'a>> for Text<'a> {
fn from(line: Line<'a>) -> Text<'a> {
Text {
lines: vec![line],
..Default::default()
}
Text { lines: vec![line] }
}
}
impl<'a> From<Vec<Line<'a>>> for Text<'a> {
fn from(lines: Vec<Line<'a>>) -> Text<'a> {
Text {
lines,
..Default::default()
}
Text { lines }
}
}
@@ -322,55 +217,6 @@ where
}
}
impl std::fmt::Display for Text<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for (position, line) in self.lines.iter().with_position() {
if position == Position::Last {
write!(f, "{line}")?;
} else {
writeln!(f, "{line}")?;
}
}
Ok(())
}
}
impl<'a> Widget for Text<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
buf.set_style(area, self.style);
for (line, row) in self.lines.into_iter().zip(area.rows()) {
let line_width = line.width() as u16;
let x_offset = match (self.alignment, line.alignment) {
(Some(Alignment::Center), None) => area.width.saturating_sub(line_width) / 2,
(Some(Alignment::Right), None) => area.width.saturating_sub(line_width),
_ => 0,
};
let line_area = Rect {
x: area.x + x_offset,
y: row.y,
width: area.width - x_offset,
height: 1,
};
line.render(line_area, buf);
}
}
}
impl<'a> Styled for Text<'a> {
type Item = Text<'a>;
fn style(&self) -> Style {
self.style
}
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
self.style(style)
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -388,12 +234,14 @@ mod tests {
#[test]
fn styled() {
let style = Style::new().yellow().italic();
let styled_text = Text::styled("The first line\nThe second line", style);
let mut text = Text::raw("The first line\nThe second line");
text.style = style;
assert_eq!(styled_text, text);
let text = Text::styled("The first line\nThe second line", style);
assert_eq!(
text.lines,
vec![
Line::from(Span::styled("The first line", style)),
Line::from(Span::styled("The second line", style))
]
);
}
#[test]
@@ -412,20 +260,32 @@ mod tests {
fn patch_style() {
let style = Style::new().yellow().italic();
let style2 = Style::new().red().underlined();
let text = Text::styled("The first line\nThe second line", style).patch_style(style2);
let mut text = Text::styled("The first line\nThe second line", style);
text.patch_style(style2);
let expected_style = Style::new().red().italic().underlined();
let expected_text = Text::styled("The first line\nThe second line", expected_style);
assert_eq!(text, expected_text);
assert_eq!(
text.lines,
vec![
Line::from(Span::styled("The first line", expected_style)),
Line::from(Span::styled("The second line", expected_style))
]
);
}
#[test]
fn reset_style() {
let style = Style::new().yellow().italic();
let text = Text::styled("The first line\nThe second line", style).reset_style();
let mut text = Text::styled("The first line\nThe second line", style);
assert_eq!(text.style, Style::reset());
text.reset_style();
assert_eq!(
text.lines,
vec![
Line::from(Span::styled("The first line", Style::reset())),
Line::from(Span::styled("The second line", Style::reset()))
]
);
}
#[test]
@@ -545,158 +405,4 @@ mod tests {
]
);
}
#[test]
fn display_raw_text() {
let text = Text::raw("The first line\nThe second line");
assert_eq!(format!("{text}"), "The first line\nThe second line");
}
#[test]
fn display_styled_text() {
let styled_text = Text::styled(
"The first line\nThe second line",
Style::new().yellow().italic(),
);
assert_eq!(format!("{styled_text}"), "The first line\nThe second line");
}
#[test]
fn display_text_from_vec() {
let text_from_vec = Text::from(vec![
Line::from("The first line"),
Line::from("The second line"),
]);
assert_eq!(
format!("{text_from_vec}"),
"The first line\nThe second line"
);
}
#[test]
fn display_extended_text() {
let mut text = Text::from("The first line\nThe second line");
assert_eq!(format!("{text}"), "The first line\nThe second line");
text.extend(vec![
Line::from("The third line"),
Line::from("The fourth line"),
]);
assert_eq!(
format!("{text}"),
"The first line\nThe second line\nThe third line\nThe fourth line"
);
}
#[test]
fn stylize() {
assert_eq!(Text::default().green().style, Color::Green.into());
assert_eq!(
Text::default().on_green().style,
Style::new().bg(Color::Green)
);
assert_eq!(Text::default().italic().style, Modifier::ITALIC.into());
}
mod widget {
use super::*;
use crate::{assert_buffer_eq, style::Color};
#[test]
fn render() {
let text = Text::from("foo");
let area = Rect::new(0, 0, 5, 1);
let mut buf = Buffer::empty(area);
text.render(area, &mut buf);
let expected_buf = Buffer::with_lines(vec!["foo "]);
assert_buffer_eq!(buf, expected_buf);
}
#[test]
fn render_right_aligned() {
let text = Text::from("foo").alignment(Alignment::Right);
let area = Rect::new(0, 0, 5, 1);
let mut buf = Buffer::empty(area);
text.render(area, &mut buf);
let expected_buf = Buffer::with_lines(vec![" foo"]);
assert_buffer_eq!(buf, expected_buf);
}
#[test]
fn render_centered_odd() {
let text = Text::from("foo").alignment(Alignment::Center);
let area = Rect::new(0, 0, 5, 1);
let mut buf = Buffer::empty(area);
text.render(area, &mut buf);
let expected_buf = Buffer::with_lines(vec![" foo "]);
assert_buffer_eq!(buf, expected_buf);
}
#[test]
fn render_centered_even() {
let text = Text::from("foo").alignment(Alignment::Center);
let area = Rect::new(0, 0, 6, 1);
let mut buf = Buffer::empty(area);
text.render(area, &mut buf);
let expected_buf = Buffer::with_lines(vec![" foo "]);
assert_buffer_eq!(buf, expected_buf);
}
#[test]
fn render_one_line_right() {
let text = Text::from(vec![
"foo".into(),
Line::from("bar").alignment(Alignment::Center),
])
.alignment(Alignment::Right);
let area = Rect::new(0, 0, 5, 2);
let mut buf = Buffer::empty(area);
text.render(area, &mut buf);
let expected_buf = Buffer::with_lines(vec![" foo", " bar "]);
assert_buffer_eq!(buf, expected_buf);
}
#[test]
fn render_only_styles_line_area() {
let area = Rect::new(0, 0, 5, 1);
let mut buf = Buffer::empty(area);
Text::from("foo".on_blue()).render(area, &mut buf);
let mut expected = Buffer::with_lines(vec!["foo "]);
expected.set_style(Rect::new(0, 0, 3, 1), Style::new().bg(Color::Blue));
assert_buffer_eq!(buf, expected);
}
#[test]
fn render_truncates() {
let mut buf = Buffer::empty(Rect::new(0, 0, 6, 1));
Text::from("foobar".on_blue()).render(Rect::new(0, 0, 3, 1), &mut buf);
let mut expected = Buffer::with_lines(vec!["foo "]);
expected.set_style(Rect::new(0, 0, 3, 1), Style::new().bg(Color::Blue));
assert_buffer_eq!(buf, expected);
}
}
}

View File

@@ -22,7 +22,6 @@
//! [`Canvas`]: crate::widgets::canvas::Canvas
mod barchart;
pub mod block;
mod borders;
#[cfg(feature = "widget-calendar")]
pub mod calendar;
pub mod canvas;
@@ -37,10 +36,13 @@ mod sparkline;
mod table;
mod tabs;
use std::fmt::{self, Debug};
use bitflags::bitflags;
pub use self::{
barchart::{Bar, BarChart, BarGroup},
block::{Block, BorderType, Padding},
borders::*,
chart::{Axis, Chart, Dataset, GraphType, LegendPosition},
clear::Clear,
gauge::{Gauge, LineGauge},
@@ -53,6 +55,55 @@ pub use self::{
};
use crate::{buffer::Buffer, layout::Rect};
bitflags! {
/// Bitflags that can be composed to set the visible borders essentially on the block widget.
#[derive(Default, Clone, Copy, Eq, PartialEq, Hash)]
pub struct Borders: u8 {
/// Show no border (default)
const NONE = 0b0000;
/// Show the top border
const TOP = 0b0001;
/// Show the right border
const RIGHT = 0b0010;
/// Show the bottom border
const BOTTOM = 0b0100;
/// Show the left border
const LEFT = 0b1000;
/// Show all borders
const ALL = Self::TOP.bits() | Self::RIGHT.bits() | Self::BOTTOM.bits() | Self::LEFT.bits();
}
}
/// Implement the `Debug` trait for the `Borders` bitflags. This is a manual implementation to
/// display the flags in a more readable way. The default implementation would display the
/// flags as 'Border(0x0)' for `Borders::NONE` for example.
impl Debug for Borders {
/// Display the Borders bitflags as a list of names. For example, `Borders::NONE` will be
/// displayed as `NONE` and `Borders::ALL` will be displayed as `ALL`. If multiple flags are
/// set, they will be displayed separated by a pipe character.
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.is_empty() {
return write!(f, "NONE");
}
if self.is_all() {
return write!(f, "ALL");
}
let mut first = true;
for (name, border) in self.iter_names() {
if border == Borders::NONE {
continue;
}
if first {
write!(f, "{name}")?;
first = false;
} else {
write!(f, " | {name}")?;
}
}
Ok(())
}
}
/// Base requirements for a Widget
pub trait Widget {
/// Draws the current state of the widget in the given buffer. That is the only method required
@@ -175,3 +226,60 @@ pub trait StatefulWidget {
type State;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State);
}
/// Macro that constructs and returns a [`Borders`] object from TOP, BOTTOM, LEFT, RIGHT, NONE, and
/// ALL. Internally it creates an empty `Borders` object and then inserts each bit flag specified
/// into it using `Borders::insert()`.
///
/// ## Examples
///
///```
/// use ratatui::{border, prelude::*, widgets::*};
///
/// Block::default()
/// //Construct a `Borders` object and use it in place
/// .borders(border!(TOP, BOTTOM));
///
/// //`border!` can be called with any order of individual sides
/// let bottom_first = border!(BOTTOM, LEFT, TOP);
/// //with the ALL keyword which works as expected
/// let all = border!(ALL);
/// //or with nothing to return a `Borders::NONE' bitflag.
/// let none = border!(NONE);
/// ```
#[cfg(feature = "macros")]
#[macro_export]
macro_rules! border {
( $($b:tt), +) => {{
let mut border = Borders::empty();
$(
border.insert(Borders::$b);
)*
border
}};
() =>{
Borders::NONE
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_borders_debug() {
assert_eq!(format!("{:?}", Borders::empty()), "NONE");
assert_eq!(format!("{:?}", Borders::NONE), "NONE");
assert_eq!(format!("{:?}", Borders::TOP), "TOP");
assert_eq!(format!("{:?}", Borders::BOTTOM), "BOTTOM");
assert_eq!(format!("{:?}", Borders::LEFT), "LEFT");
assert_eq!(format!("{:?}", Borders::RIGHT), "RIGHT");
assert_eq!(format!("{:?}", Borders::ALL), "ALL");
assert_eq!(format!("{:?}", Borders::all()), "ALL");
assert_eq!(
format!("{:?}", Borders::TOP | Borders::BOTTOM),
"TOP | BOTTOM"
);
}
}

View File

@@ -6,20 +6,18 @@
//! In its simplest form, a `Block` is a [border](Borders) around another widget. It can have a
//! [title](Block::title) and [padding](Block::padding).
#[path = "../title.rs"]
pub mod title;
use strum::{Display, EnumString};
pub use self::title::{Position, Title};
use crate::{
prelude::*,
symbols::border,
widgets::{Borders, Widget},
};
mod padding;
pub mod title;
pub use padding::Padding;
pub use title::{Position, Title};
/// The type of border of a [`Block`].
///
/// See the [`borders`](Block::borders) method of `Block` to configure its borders.
@@ -112,6 +110,93 @@ impl BorderType {
}
}
/// Defines the padding of a [`Block`].
///
/// See the [`padding`](Block::padding) method of [`Block`] to configure its padding.
///
/// This concept is similar to [CSS padding](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_box_model/Introduction_to_the_CSS_box_model#padding_area).
///
/// **NOTE**: Terminal cells are often taller than they are wide, so to make horizontal and vertical
/// padding seem equal, doubling the horizontal padding is usually pretty good.
///
/// # Example
///
/// ```
/// use ratatui::{prelude::*, widgets::*};
///
/// Padding::uniform(1);
/// Padding::horizontal(2);
/// ```
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct Padding {
/// Left padding
pub left: u16,
/// Right padding
pub right: u16,
/// Top padding
pub top: u16,
/// Bottom padding
pub bottom: u16,
}
impl Padding {
/// Creates a new `Padding` by specifying every field individually.
pub const fn new(left: u16, right: u16, top: u16, bottom: u16) -> Self {
Padding {
left,
right,
top,
bottom,
}
}
/// Creates a `Padding` of 0.
///
/// This is also the default.
pub const fn zero() -> Self {
Padding {
left: 0,
right: 0,
top: 0,
bottom: 0,
}
}
/// Defines the [`left`](Padding::left) and [`right`](Padding::right) padding.
///
/// This leaves [`top`](Padding::top) and [`bottom`](Padding::bottom) to `0`.
pub const fn horizontal(value: u16) -> Self {
Padding {
left: value,
right: value,
top: 0,
bottom: 0,
}
}
/// Defines the [`top`](Padding::top) and [`bottom`](Padding::bottom) padding.
///
/// This leaves [`left`](Padding::left) and [`right`](Padding::right) at `0`.
pub const fn vertical(value: u16) -> Self {
Padding {
left: 0,
right: 0,
top: value,
bottom: value,
}
}
/// Applies the same value to every `Padding` field.
pub const fn uniform(value: u16) -> Self {
Padding {
left: value,
right: value,
top: value,
bottom: value,
}
}
}
/// Base widget to be used to display a box border around all [upper level ones](crate::widgets).
///
/// The borders can be configured with [`Block::borders`] and others. A block can have multiple
@@ -182,13 +267,6 @@ impl<'a> Block<'a> {
}
}
/// Create a new block with [all borders](Borders::ALL) shown
pub const fn bordered() -> Self {
let mut block = Block::new();
block.borders = Borders::ALL;
block
}
/// Adds a title to the block.
///
/// The `title` function allows you to add a title to the block. You can call this function
@@ -291,6 +369,12 @@ impl<'a> Block<'a> {
self
}
#[deprecated(since = "0.22.0", note = "You should use a `title_position` instead.")]
/// This method just calls `title_position` with Position::Bottom
pub fn title_on_bottom(self) -> Block<'a> {
self.title_position(Position::Bottom)
}
/// Sets the default [`Position`] for all block [titles](Title).
///
/// Titles that explicitly set a [`Position`] will ignore this.
@@ -736,12 +820,6 @@ mod tests {
style::{Color, Modifier, Stylize},
};
#[test]
fn create_with_all_borders() {
let block = Block::bordered();
assert_eq!(block.borders, Borders::all());
}
#[test]
fn inner_takes_into_account_the_borders() {
// No borders
@@ -976,6 +1054,36 @@ mod tests {
const _PLAIN: border::Set = BorderType::border_symbols(BorderType::Plain);
}
#[test]
fn padding_new() {
assert_eq!(
Padding::new(1, 2, 3, 4),
Padding {
left: 1,
right: 2,
top: 3,
bottom: 4
}
)
}
#[test]
fn padding_constructors() {
assert_eq!(Padding::zero(), Padding::new(0, 0, 0, 0));
assert_eq!(Padding::horizontal(1), Padding::new(1, 1, 0, 0));
assert_eq!(Padding::vertical(1), Padding::new(0, 0, 1, 1));
assert_eq!(Padding::uniform(1), Padding::new(1, 1, 1, 1));
}
#[test]
fn padding_can_be_const() {
const _PADDING: Padding = Padding::new(1, 1, 1, 1);
const _UNI_PADDING: Padding = Padding::uniform(1);
const _NO_PADDING: Padding = Padding::zero();
const _HORIZONTAL: Padding = Padding::horizontal(1);
const _VERTICAL: Padding = Padding::vertical(1);
}
#[test]
fn block_new() {
assert_eq!(
@@ -1100,6 +1208,17 @@ mod tests {
}
}
#[test]
fn title_on_bottom() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 2));
#[allow(deprecated)]
Block::default()
.title("test")
.title_on_bottom()
.render(buffer.area, &mut buffer);
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" ", "test"]));
}
#[test]
fn title_position() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 2));

View File

@@ -1,202 +0,0 @@
/// Defines the padding for a [`Block`].
///
/// See the [`padding`] method of [`Block`] to configure its padding.
///
/// This concept is similar to [CSS padding].
///
/// **NOTE**: Terminal cells are often taller than they are wide, so to make horizontal and vertical
/// padding seem equal, doubling the horizontal padding is usually pretty good.
///
/// # Example
///
/// ```
/// use ratatui::{prelude::*, widgets::*};
///
/// Padding::uniform(1);
/// Padding::horizontal(2);
/// Padding::left(3);
/// Padding::proportional(4);
/// Padding::symmetric(5, 6);
/// ```
///
/// [`Block`]: crate::widgets::Block
/// [`padding`]: crate::widgets::Block::padding
/// [CSS padding]: https://developer.mozilla.org/en-US/docs/Web/CSS/padding
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct Padding {
/// Left padding
pub left: u16,
/// Right padding
pub right: u16,
/// Top padding
pub top: u16,
/// Bottom padding
pub bottom: u16,
}
impl Padding {
/// Creates a new `Padding` by specifying every field individually.
///
/// Note: the order of the fields does not match the order of the CSS properties.
pub const fn new(left: u16, right: u16, top: u16, bottom: u16) -> Self {
Padding {
left,
right,
top,
bottom,
}
}
/// Creates a `Padding` with all fields set to `0`.
pub const fn zero() -> Self {
Padding {
left: 0,
right: 0,
top: 0,
bottom: 0,
}
}
/// Creates a `Padding` with the same value for `left` and `right`.
pub const fn horizontal(value: u16) -> Self {
Padding {
left: value,
right: value,
top: 0,
bottom: 0,
}
}
/// Creates a `Padding` with the same value for `top` and `bottom`.
pub const fn vertical(value: u16) -> Self {
Padding {
left: 0,
right: 0,
top: value,
bottom: value,
}
}
/// Creates a `Padding` with the same value for all fields.
pub const fn uniform(value: u16) -> Self {
Padding {
left: value,
right: value,
top: value,
bottom: value,
}
}
/// Creates a `Padding` that is visually proportional to the terminal.
///
/// This represents a padding of 2x the value for `left` and `right` and 1x the value for
/// `top` and `bottom`.
pub const fn proportional(value: u16) -> Self {
Padding {
left: 2 * value,
right: 2 * value,
top: value,
bottom: value,
}
}
/// Creates a `Padding` that is symmetric.
///
/// The `x` value is used for `left` and `right` and the `y` value is used for `top` and
/// `bottom`.
pub const fn symmetric(x: u16, y: u16) -> Self {
Padding {
left: x,
right: x,
top: y,
bottom: y,
}
}
/// Creates a `Padding` that only sets the `left` padding.
pub const fn left(value: u16) -> Self {
Padding {
left: value,
right: 0,
top: 0,
bottom: 0,
}
}
/// Creates a `Padding` that only sets the `right` padding.
pub const fn right(value: u16) -> Self {
Padding {
left: 0,
right: value,
top: 0,
bottom: 0,
}
}
/// Creates a `Padding` that only sets the `top` padding.
pub const fn top(value: u16) -> Self {
Padding {
left: 0,
right: 0,
top: value,
bottom: 0,
}
}
/// Creates a `Padding` that only sets the `bottom` padding.
pub const fn bottom(value: u16) -> Self {
Padding {
left: 0,
right: 0,
top: 0,
bottom: value,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new() {
assert_eq!(
Padding::new(1, 2, 3, 4),
Padding {
left: 1,
right: 2,
top: 3,
bottom: 4
}
)
}
#[test]
fn constructors() {
assert_eq!(Padding::zero(), Padding::new(0, 0, 0, 0));
assert_eq!(Padding::horizontal(1), Padding::new(1, 1, 0, 0));
assert_eq!(Padding::vertical(1), Padding::new(0, 0, 1, 1));
assert_eq!(Padding::uniform(1), Padding::new(1, 1, 1, 1));
assert_eq!(Padding::proportional(1), Padding::new(2, 2, 1, 1));
assert_eq!(Padding::symmetric(1, 2), Padding::new(1, 1, 2, 2));
assert_eq!(Padding::left(1), Padding::new(1, 0, 0, 0));
assert_eq!(Padding::right(1), Padding::new(0, 1, 0, 0));
assert_eq!(Padding::top(1), Padding::new(0, 0, 1, 0));
assert_eq!(Padding::bottom(1), Padding::new(0, 0, 0, 1));
}
#[test]
fn can_be_const() {
const _PADDING: Padding = Padding::new(1, 1, 1, 1);
const _UNI_PADDING: Padding = Padding::uniform(1);
const _NO_PADDING: Padding = Padding::zero();
const _HORIZONTAL: Padding = Padding::horizontal(1);
const _VERTICAL: Padding = Padding::vertical(1);
const _PROPORTIONAL: Padding = Padding::proportional(1);
const _SYMMETRIC: Padding = Padding::symmetric(1, 1);
const _LEFT: Padding = Padding::left(1);
const _RIGHT: Padding = Padding::right(1);
const _TOP: Padding = Padding::top(1);
const _BOTTOM: Padding = Padding::bottom(1);
}
}

View File

@@ -1,109 +0,0 @@
use std::fmt::{self, Debug};
use bitflags::bitflags;
bitflags! {
/// Bitflags that can be composed to set the visible borders essentially on the block widget.
#[derive(Default, Clone, Copy, Eq, PartialEq, Hash)]
pub struct Borders: u8 {
/// Show no border (default)
const NONE = 0b0000;
/// Show the top border
const TOP = 0b0001;
/// Show the right border
const RIGHT = 0b0010;
/// Show the bottom border
const BOTTOM = 0b0100;
/// Show the left border
const LEFT = 0b1000;
/// Show all borders
const ALL = Self::TOP.bits() | Self::RIGHT.bits() | Self::BOTTOM.bits() | Self::LEFT.bits();
}
}
/// Implement the `Debug` trait for the `Borders` bitflags. This is a manual implementation to
/// display the flags in a more readable way. The default implementation would display the
/// flags as 'Border(0x0)' for `Borders::NONE` for example.
impl Debug for Borders {
/// Display the Borders bitflags as a list of names. For example, `Borders::NONE` will be
/// displayed as `NONE` and `Borders::ALL` will be displayed as `ALL`. If multiple flags are
/// set, they will be displayed separated by a pipe character.
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.is_empty() {
return write!(f, "NONE");
}
if self.is_all() {
return write!(f, "ALL");
}
let mut first = true;
for (name, border) in self.iter_names() {
if border == Borders::NONE {
continue;
}
if first {
write!(f, "{name}")?;
first = false;
} else {
write!(f, " | {name}")?;
}
}
Ok(())
}
}
/// Macro that constructs and returns a [`Borders`] object from TOP, BOTTOM, LEFT, RIGHT, NONE, and
/// ALL. Internally it creates an empty `Borders` object and then inserts each bit flag specified
/// into it using `Borders::insert()`.
///
/// ## Examples
///
///```
/// use ratatui::{border, prelude::*, widgets::*};
///
/// Block::default()
/// //Construct a `Borders` object and use it in place
/// .borders(border!(TOP, BOTTOM));
///
/// //`border!` can be called with any order of individual sides
/// let bottom_first = border!(BOTTOM, LEFT, TOP);
/// //with the ALL keyword which works as expected
/// let all = border!(ALL);
/// //or with nothing to return a `Borders::NONE' bitflag.
/// let none = border!(NONE);
/// ```
#[cfg(feature = "macros")]
#[macro_export]
macro_rules! border {
( $($b:tt), +) => {{
let mut border = Borders::empty();
$(
border.insert(Borders::$b);
)*
border
}};
() =>{
Borders::NONE
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_borders_debug() {
assert_eq!(format!("{:?}", Borders::empty()), "NONE");
assert_eq!(format!("{:?}", Borders::NONE), "NONE");
assert_eq!(format!("{:?}", Borders::TOP), "TOP");
assert_eq!(format!("{:?}", Borders::BOTTOM), "BOTTOM");
assert_eq!(format!("{:?}", Borders::LEFT), "LEFT");
assert_eq!(format!("{:?}", Borders::RIGHT), "RIGHT");
assert_eq!(format!("{:?}", Borders::ALL), "ALL");
assert_eq!(format!("{:?}", Borders::all()), "ALL");
assert_eq!(
format!("{:?}", Borders::TOP | Borders::BOTTOM),
"TOP | BOTTOM"
);
}
}

View File

@@ -192,12 +192,7 @@ impl CalendarEventStore {
/// your own type that implements [`Into<Style>`]).
pub fn today<S: Into<Style>>(style: S) -> Self {
let mut res = Self::default();
res.add(
OffsetDateTime::now_local()
.unwrap_or(OffsetDateTime::now_utc())
.date(),
style.into(),
);
res.add(OffsetDateTime::now_local().unwrap().date(), style.into());
res
}
@@ -265,9 +260,4 @@ mod tests {
"Date added to styler should return the provided style"
);
}
#[test]
fn test_today() {
CalendarEventStore::today(Style::default());
}
}

View File

@@ -1,14 +1,15 @@
#![warn(missing_docs)]
use std::cmp::max;
use std::{borrow::Cow, cmp::max};
use strum::{Display, EnumString};
use unicode_width::UnicodeWidthStr;
use crate::{
buffer::Buffer,
layout::Flex,
prelude::*,
layout::{Alignment, Constraint, Rect},
style::{Color, Style, Styled},
symbols,
text::{Line, Span},
widgets::{
canvas::{Canvas, Line as CanvasLine, Points},
Block, Borders, Widget,
@@ -275,7 +276,8 @@ impl LegendPosition {
///
/// This is the main element composing a [`Chart`].
///
/// A dataset can be [named](Dataset::name). Only named datasets will be rendered in the legend.
/// A dataset can be [named](Dataset::name) to be referenced in the legend (NOTE: Currently,
/// datasets with an empty name will show an empty line in the legend, see [PR 527]).
///
/// After that, you can pass it data with [`Dataset::data`]. Data is an array of `f64` tuples
/// (`(f64, f64)`), the first element being X and the second Y. It's also worth noting that, unlike
@@ -283,6 +285,8 @@ impl LegendPosition {
///
/// You can also customize the rendering by using [`Dataset::marker`] and [`Dataset::graph_type`].
///
/// [PR 527]: https://github.com/ratatui-org/ratatui/pull/527
///
/// # Example
///
/// This example draws a red line between two points.
@@ -300,7 +304,7 @@ impl LegendPosition {
#[derive(Debug, Default, Clone, PartialEq)]
pub struct Dataset<'a> {
/// Name of the dataset (used in the legend if shown)
name: Option<Line<'a>>,
name: Cow<'a, str>,
/// A reference to the actual data
data: &'a [(f64, f64)],
/// Symbol used for each points of this dataset
@@ -314,20 +318,18 @@ pub struct Dataset<'a> {
impl<'a> Dataset<'a> {
/// Sets the name of the dataset
///
/// The dataset's name is used when displaying the chart legend. Datasets don't require a name
/// and can be created without specifying one. Once assigned, a name can't be removed, only
/// changed
/// The dataset's name is used when displaying the chart legend. Currently, datasets with an
/// empty name will show an empty line in the legend, see [PR 527]).
///
/// The name can be styled (see [`Line`] for that), but the dataset's style will always have
/// precedence.
/// [PR 527]: https://github.com/ratatui-org/ratatui/pull/527
///
/// This is a fluent setter method which must be chained or used as it consumes self
#[must_use = "method moves the value of self and returns the modified value"]
pub fn name<S>(mut self, name: S) -> Dataset<'a>
where
S: Into<Line<'a>>,
S: Into<Cow<'a, str>>,
{
self.name = Some(name.into());
self.name = name.into();
self
}
@@ -736,25 +738,20 @@ impl<'a> Chart<'a> {
}
if let Some(legend_position) = self.legend_position {
let legends = self
.datasets
.iter()
.filter_map(|d| Some(d.name.as_ref()?.width() as u16));
if let Some(inner_width) = legends.clone().max() {
if let Some(inner_width) = self.datasets.iter().map(|d| d.name.width() as u16).max() {
let legend_width = inner_width + 2;
let legend_height = legends.count() as u16 + 2;
let [max_legend_width] = layout.graph_area.split(
&Layout::horizontal([self.hidden_legend_constraints.0]).flex(Flex::Start),
);
let [max_legend_height] = layout
.graph_area
.split(&Layout::vertical([self.hidden_legend_constraints.1]).flex(Flex::Start));
let legend_height = self.datasets.len() as u16 + 2;
let max_legend_width = self
.hidden_legend_constraints
.0
.apply(layout.graph_area.width);
let max_legend_height = self
.hidden_legend_constraints
.1
.apply(layout.graph_area.height);
if inner_width > 0
&& legend_width <= max_legend_width.width
&& legend_height <= max_legend_height.height
&& legend_width <= max_legend_width
&& legend_height <= max_legend_height
{
layout.legend_area = legend_position.layout(
layout.graph_area,
@@ -1036,22 +1033,12 @@ impl<'a> Widget for Chart<'a> {
Block::default()
.borders(Borders::ALL)
.render(legend_area, buf);
for (i, (dataset_name, dataset_style)) in self
.datasets
.iter()
.filter_map(|ds| Some((ds.name.as_ref()?, ds.style())))
.enumerate()
{
let name = dataset_name.clone().patch_style(dataset_style);
name.render(
Rect {
x: legend_area.x + 1,
y: legend_area.y + 1 + i as u16,
width: legend_area.width - 2,
height: 1,
},
buf,
for (i, dataset) in self.datasets.iter().enumerate() {
buf.set_string(
legend_area.x + 1,
legend_area.y + 1 + i as u16,
&dataset.name,
dataset.style,
);
}
}
@@ -1099,10 +1086,7 @@ mod tests {
use strum::ParseError;
use super::*;
use crate::{
assert_buffer_eq,
style::{Modifier, Stylize},
};
use crate::style::{Modifier, Stylize};
struct LegendTestCase {
chart_area: Rect,
@@ -1201,50 +1185,6 @@ mod tests {
assert_eq!(buffer, Buffer::with_lines(vec![" ".repeat(8); 4]))
}
#[test]
fn datasets_without_name_dont_contribute_to_legend_height() {
let data_named_1 = Dataset::default().name("data1"); // must occupy a row in legend
let data_named_2 = Dataset::default().name(""); // must occupy a row in legend, even if name is empty
let data_unnamed = Dataset::default(); // must not occupy a row in legend
let widget = Chart::new(vec![data_named_1, data_unnamed, data_named_2]);
let buffer = Buffer::empty(Rect::new(0, 0, 50, 25));
let layout = widget.layout(buffer.area);
assert!(layout.legend_area.is_some());
assert_eq!(layout.legend_area.unwrap().height, 4); // 2 for borders, 2 for rows
}
#[test]
fn no_legend_if_no_named_datasets() {
let dataset = Dataset::default();
let widget = Chart::new(vec![dataset; 3]);
let buffer = Buffer::empty(Rect::new(0, 0, 50, 25));
let layout = widget.layout(buffer.area);
assert!(layout.legend_area.is_none());
}
#[test]
fn dataset_legend_style_is_patched() {
let long_dataset_name = Dataset::default().name("Very long name");
let short_dataset =
Dataset::default().name(Line::from("Short name").alignment(Alignment::Right));
let widget = Chart::new(vec![long_dataset_name, short_dataset])
.hidden_legend_constraints((100.into(), 100.into()));
let mut buffer = Buffer::empty(Rect::new(0, 0, 20, 5));
widget.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines(vec![
" ┌──────────────┐",
" │Very long name│",
" │ Short name│",
" └──────────────┘",
" ",
]);
assert_buffer_eq!(buffer, expected);
}
#[test]
fn test_chart_have_a_topleft_legend() {
let chart = Chart::new(vec![Dataset::default().name("Ds1")])

106
src/widgets/list.rs Executable file → Normal file
View File

@@ -21,10 +21,10 @@ use crate::{
/// [`offset`]: ListState::offset()
/// [`selected`]: ListState::selected()
///
/// See the list in the [Examples] directory for a more in depth example of the various
/// configuration options and for how to handle state.
/// See the [list example] for a more in depth example of the various configuration options and
/// for how to handle state.
///
/// [Examples]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
/// [list example]: https://github.com/ratatui-org/ratatui/blob/main/examples/list.rs
///
/// # Example
///
@@ -45,7 +45,6 @@ use crate::{
/// # }
/// ```
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ListState {
offset: usize,
selected: Option<usize>,
@@ -169,10 +168,6 @@ impl ListState {
/// This [`Style`] will be combined with the [`Style`] of the inner [`Text`]. The [`Style`]
/// of the [`Text`] will be added to the [`Style`] of the [`ListItem`].
///
/// You can also align a `ListItem` by aligning its underlying [`Text`] and [`Line`]s. For that,
/// see [`Text::alignment`] and [`Line::alignment`]. On a multiline `Text`, one `Line` can override
/// the alignment by setting it explicitly.
///
/// # Examples
///
/// You can create [`ListItem`]s from simple `&str`
@@ -207,13 +202,6 @@ impl ListState {
/// let item = ListItem::new(text);
/// ```
///
/// A right-aligned `ListItem`
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// ListItem::new(Text::from("foo").alignment(Alignment::Right));
/// ```
///
/// [`Stylize`]: crate::style::Stylize
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct ListItem<'a> {
@@ -349,14 +337,12 @@ where
///
/// A list is a collection of [`ListItem`]s.
///
/// This is different from a [`Table`] because it does not handle columns, headers or footers and
/// the item's height is automatically determined. A `List` can also be put in reverse order (i.e.
/// *bottom to top*) whereas a [`Table`] cannot.
/// This is different from a [`Table`] because it does not handle columns or headers and the item's
/// height is automatically determined. A `List` can also be put in reverse order (i.e. *bottom to
/// top*) whereas a [`Table`] cannot.
///
/// [`Table`]: crate::widgets::Table
///
/// List items can be aligned using [`Text::alignment`], for more details see [`ListItem`].
///
/// [`List`] implements [`Widget`] and so it can be drawn using
/// [`Frame::render_widget`](crate::terminal::Frame::render_widget).
///
@@ -364,10 +350,10 @@ where
/// the user to [scroll](ListState::offset) through items and [select](ListState::select) one of
/// them.
///
/// See the list in the [Examples] directory for a more in depth example of the various
/// configuration options and for how to handle state.
/// See the [list example] for a more in depth example of the various configuration options and for
/// how to handle state.
///
/// [Examples]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
/// [list example]: https://github.com/ratatui-org/ratatui/blob/main/examples/list.rs
///
/// # Fluent setters
///
@@ -413,16 +399,6 @@ where
///
/// frame.render_stateful_widget(list, area, &mut state);
/// # }
/// ```
///
/// In addition to `List::new`, any iterator whose element is convertible to `ListItem` can be
/// collected into `List`.
///
/// ```
/// use ratatui::widgets::List;
///
/// (0..5).map(|i| format!("Item{i}")).collect::<List>();
/// ```
#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)]
pub struct List<'a> {
block: Option<Block<'a>>,
@@ -823,32 +799,17 @@ impl<'a> StatefulWidget for List<'a> {
current_height += item.height() as u16;
pos
};
let row_area = Rect {
let area = Rect {
x,
y,
width: list_area.width,
height: item.height() as u16,
};
let item_style = self.style.patch(item.style);
buf.set_style(row_area, item_style);
buf.set_style(area, item_style);
let is_selected = state.selected.map_or(false, |s| s == i);
let item_area = if selection_spacing {
let highlight_symbol_width = self.highlight_symbol.unwrap_or("").len() as u16;
Rect {
x: row_area.x + highlight_symbol_width,
width: row_area.width - highlight_symbol_width,
..row_area
}
} else {
row_area
};
item.content.clone().render(item_area, buf);
for j in 0..item.content.height() {
for (j, line) in item.content.lines.iter().enumerate() {
// if the item is selected, we need to display the highlight symbol:
// - either for the first line of the item only,
// - or for each line of the item if the appropriate option is set
@@ -857,19 +818,29 @@ impl<'a> StatefulWidget for List<'a> {
} else {
&blank_symbol
};
if selection_spacing {
buf.set_stringn(
let (elem_x, max_element_width) = if selection_spacing {
let (elem_x, _) = buf.set_stringn(
x,
y + j as u16,
symbol,
list_area.width as usize,
item_style,
);
}
(elem_x, (list_area.width - (elem_x - x)))
} else {
(x, list_area.width)
};
let x_offset = match line.alignment {
Some(Alignment::Center) => {
(area.width / 2).saturating_sub(line.width() as u16 / 2)
}
Some(Alignment::Right) => area.width.saturating_sub(line.width() as u16),
_ => 0,
};
buf.set_line(elem_x + x_offset, y + j as u16, line, max_element_width);
}
if is_selected {
buf.set_style(row_area, self.highlight_style);
buf.set_style(area, self.highlight_style);
}
}
}
@@ -906,15 +877,6 @@ impl<'a> Styled for ListItem<'a> {
}
}
impl<'a, Item> FromIterator<Item> for List<'a>
where
Item: Into<ListItem<'a>>,
{
fn from_iter<Iter: IntoIterator<Item = Item>>(iter: Iter) -> Self {
List::new(iter)
}
}
#[cfg(test)]
mod tests {
use std::borrow::Cow;
@@ -1365,13 +1327,6 @@ mod tests {
assert_buffer_eq!(buffer, expected);
}
#[test]
fn test_collect_list_from_iterator() {
let collected: List = (0..3).map(|i| format!("Item{i}")).collect();
let expected = List::new(["Item0", "Item1", "Item2"]);
assert_eq!(collected, expected);
}
#[test]
fn test_list_block() {
let items = list_items(vec!["Item 0", "Item 1", "Item 2"]);
@@ -1863,11 +1818,14 @@ mod tests {
#[test]
fn test_render_list_alignment_line_less_than_width() {
let items = [Line::from("Small").alignment(Alignment::Center)];
let items = [Line::from("Small").alignment(Alignment::Center)]
.into_iter()
.map(ListItem::new)
.collect::<Vec<ListItem>>();
let list = List::new(items);
let buffer = render_widget(list, 10, 5);
let expected = Buffer::with_lines(vec![
" Small ",
" Small ",
" ",
" ",
" ",

View File

@@ -1,4 +1,3 @@
#![warn(missing_docs)]
use strum::{Display, EnumString};
use super::StatefulWidget;
@@ -7,11 +6,7 @@ use crate::{
symbols::scrollbar::{Set, DOUBLE_HORIZONTAL, DOUBLE_VERTICAL},
};
/// An enum representing a scrolling direction.
///
/// This is used with [`ScrollbarState::scroll`].
///
/// It is useful for example when you want to store in which direction to scroll.
/// An enum representing the direction of scrolling in a Scrollbar widget.
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
pub enum ScrollDirection {
/// Forward scroll direction, usually corresponds to scrolling downwards or rightwards.
@@ -47,54 +42,38 @@ pub enum ScrollDirection {
/// If you don't have multi-line content, you can leave the `viewport_content_length` set to the
/// default of 0 and it'll use the track size as a `viewport_content_length`.
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ScrollbarState {
/// The total length of the scrollable content.
// The total length of the scrollable content.
content_length: usize,
/// The current position within the scrollable content.
// The current position within the scrollable content.
position: usize,
/// The length of content in current viewport.
// The length of content in current viewport.
viewport_content_length: usize,
}
impl ScrollbarState {
/// Constructs a new ScrollbarState with the specified content length.
///
/// `content_length` is the total number of element, that can be scrolled. See
/// [`ScrollbarState`] for more details.
pub fn new(content_length: usize) -> Self {
Self {
content_length,
..Default::default()
}
}
/// Sets the scroll position of the scrollbar.
///
/// This represents the number of scrolled items.
///
/// This is a fluent setter method which must be chained or used as it consumes self
/// Sets the scroll position of the scrollbar and returns the modified ScrollbarState.
#[must_use = "method moves the value of self and returns the modified value"]
pub fn position(mut self, position: usize) -> Self {
self.position = position;
self
}
/// Sets the length of the scrollable content.
///
/// This is the number of scrollable items. If items have a length of one, then this is the
/// same as the number of scrollable cells.
///
/// This is a fluent setter method which must be chained or used as it consumes self
/// Sets the length of the scrollable content and returns the modified ScrollbarState.
#[must_use = "method moves the value of self and returns the modified value"]
pub fn content_length(mut self, content_length: usize) -> Self {
self.content_length = content_length;
self
}
/// Sets the items' size.
///
/// This is a fluent setter method which must be chained or used as it consumes self
/// Sets the length of the viewport content and returns the modified ScrollbarState.
#[must_use = "method moves the value of self and returns the modified value"]
pub fn viewport_content_length(mut self, viewport_content_length: usize) -> Self {
self.viewport_content_length = viewport_content_length;
@@ -111,7 +90,7 @@ impl ScrollbarState {
self.position = self
.position
.saturating_add(1)
.min(self.content_length.saturating_sub(1))
.clamp(0, self.content_length.saturating_sub(1))
}
/// Sets the scroll position to the start of the scrollable content.
@@ -124,7 +103,7 @@ impl ScrollbarState {
self.position = self.content_length.saturating_sub(1)
}
/// Changes the scroll position based on the provided [`ScrollDirection`].
/// Changes the scroll position based on the provided ScrollDirection.
pub fn scroll(&mut self, direction: ScrollDirection) {
match direction {
ScrollDirection::Forward => {
@@ -137,33 +116,19 @@ impl ScrollbarState {
}
}
/// This is the position of the scrollbar around a given area.
///
/// ```plain
/// HorizontalTop
/// ┌───────┐
/// VerticalLeft│ │VerticalRight
/// └───────┘
/// HorizontalBottom
/// ```
/// Scrollbar Orientation
#[derive(Debug, Default, Display, EnumString, Clone, Eq, PartialEq, Hash)]
pub enum ScrollbarOrientation {
/// Positions the scrollbar on the right, scrolling vertically
#[default]
VerticalRight,
/// Positions the scrollbar on the left, scrolling vertically
VerticalLeft,
/// Positions the scrollbar on the bottom, scrolling horizontally
HorizontalBottom,
/// Positions the scrollbar on the top, scrolling horizontally
HorizontalTop,
}
/// A widget to display a scrollbar
///
/// The following components of the scrollbar are customizable in symbol and style. Note the
/// scrollbar is represented horizontally but it can also be set vertically (which is actually the
/// default).
/// The following components of the scrollbar are customizable in symbol and style.
///
/// ```text
/// <--▮------->
@@ -180,6 +145,7 @@ pub enum ScrollbarOrientation {
/// use ratatui::{prelude::*, widgets::*};
///
/// # fn render_paragraph_with_scrollbar(frame: &mut Frame, area: Rect) {
///
/// let vertical_scroll = 0; // from app state
///
/// let items = vec![
@@ -191,23 +157,20 @@ pub enum ScrollbarOrientation {
/// .scroll((vertical_scroll as u16, 0))
/// .block(Block::new().borders(Borders::RIGHT)); // to show a background for the scrollbar
///
/// let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight)
/// let scrollbar = Scrollbar::default()
/// .orientation(ScrollbarOrientation::VerticalRight)
/// .begin_symbol(Some("↑"))
/// .end_symbol(Some("↓"));
///
/// let mut scrollbar_state = ScrollbarState::new(items.len()).position(vertical_scroll);
/// let mut scrollbar_state = ScrollbarState::new(items.iter().len()).position(vertical_scroll);
///
/// let area = frame.size();
/// // Note we render the paragraph
/// frame.render_widget(paragraph, area);
/// // and the scrollbar, those are separate widgets
/// frame.render_stateful_widget(
/// scrollbar,
/// area.inner(&Margin {
/// // using an inner vertical margin of 1 unit makes the scrollbar inside the block
/// vertical: 1,
/// horizontal: 0,
/// }),
/// }), // using a inner vertical margin of 1 unit makes the scrollbar inside the block
/// &mut scrollbar_state,
/// );
/// # }
@@ -242,22 +205,12 @@ impl<'a> Default for Scrollbar<'a> {
}
impl<'a> Scrollbar<'a> {
/// Creates a new scrollbar with the given position.
///
/// Most of the time you'll want [`ScrollbarOrientation::VerticalLeft`] or
/// [`ScrollbarOrientation::HorizontalBottom`]. See [`ScrollbarOrientation`] for more options.
pub fn new(orientation: ScrollbarOrientation) -> Self {
Self::default().orientation(orientation)
}
/// Sets the position of the scrollbar.
///
/// The orientation of the scrollbar is the position it will take around a [`Rect`]. See
/// [`ScrollbarOrientation`] for more details.
///
/// Resets the symbols to [`DOUBLE_VERTICAL`] or [`DOUBLE_HORIZONTAL`] based on orientation.
///
/// This is a fluent setter method which must be chained or used as it consumes self
/// Sets the orientation of the scrollbar.
/// Resets the symbols to [`DOUBLE_VERTICAL`] or [`DOUBLE_HORIZONTAL`] based on orientation
#[must_use = "method moves the value of self and returns the modified value"]
pub fn orientation(mut self, orientation: ScrollbarOrientation) -> Self {
self.orientation = orientation;
@@ -270,11 +223,6 @@ impl<'a> Scrollbar<'a> {
}
/// Sets the orientation and symbols for the scrollbar from a [`Set`].
///
/// This has the same effect as calling [`Scrollbar::orientation`] and then
/// [`Scrollbar::symbols`]. See those for more details.
///
/// This is a fluent setter method which must be chained or used as it consumes self
#[must_use = "method moves the value of self and returns the modified value"]
pub fn orientation_and_symbol(mut self, orientation: ScrollbarOrientation, set: Set) -> Self {
self.orientation = orientation;
@@ -282,26 +230,16 @@ impl<'a> Scrollbar<'a> {
}
/// Sets the symbol that represents the thumb of the scrollbar.
///
/// The thumb is the handle representing the progression on the scrollbar. See [`Scrollbar`]
/// for a visual example of what this represents.
///
/// This is a fluent setter method which must be chained or used as it consumes self
#[must_use = "method moves the value of self and returns the modified value"]
pub fn thumb_symbol(mut self, thumb_symbol: &'a str) -> Self {
self.thumb_symbol = thumb_symbol;
self
}
/// Sets the style on the scrollbar thumb.
///
/// The thumb is the handle representing the progression on the scrollbar. See [`Scrollbar`]
/// for a visual example of what this represents.
/// Sets the style that represents the thumb of the scrollbar.
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// This is a fluent setter method which must be chained or used as it consumes self
#[must_use = "method moves the value of self and returns the modified value"]
pub fn thumb_style<S: Into<Style>>(mut self, thumb_style: S) -> Self {
self.thumb_style = thumb_style.into();
@@ -309,10 +247,6 @@ impl<'a> Scrollbar<'a> {
}
/// Sets the symbol that represents the track of the scrollbar.
///
/// See [`Scrollbar`] for a visual example of what this represents.
///
/// This is a fluent setter method which must be chained or used as it consumes self
#[must_use = "method moves the value of self and returns the modified value"]
pub fn track_symbol(mut self, track_symbol: Option<&'a str>) -> Self {
self.track_symbol = track_symbol;
@@ -321,12 +255,8 @@ impl<'a> Scrollbar<'a> {
/// Sets the style that is used for the track of the scrollbar.
///
/// See [`Scrollbar`] for a visual example of what this represents.
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// This is a fluent setter method which must be chained or used as it consumes self
#[must_use = "method moves the value of self and returns the modified value"]
pub fn track_style<S: Into<Style>>(mut self, track_style: S) -> Self {
self.track_style = track_style.into();
@@ -334,10 +264,6 @@ impl<'a> Scrollbar<'a> {
}
/// Sets the symbol that represents the beginning of the scrollbar.
///
/// See [`Scrollbar`] for a visual example of what this represents.
///
/// This is a fluent setter method which must be chained or used as it consumes self
#[must_use = "method moves the value of self and returns the modified value"]
pub fn begin_symbol(mut self, begin_symbol: Option<&'a str>) -> Self {
self.begin_symbol = begin_symbol;
@@ -346,12 +272,8 @@ impl<'a> Scrollbar<'a> {
/// Sets the style that is used for the beginning of the scrollbar.
///
/// See [`Scrollbar`] for a visual example of what this represents.
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// This is a fluent setter method which must be chained or used as it consumes self
#[must_use = "method moves the value of self and returns the modified value"]
pub fn begin_style<S: Into<Style>>(mut self, begin_style: S) -> Self {
self.begin_style = begin_style.into();
@@ -359,10 +281,6 @@ impl<'a> Scrollbar<'a> {
}
/// Sets the symbol that represents the end of the scrollbar.
///
/// See [`Scrollbar`] for a visual example of what this represents.
///
/// This is a fluent setter method which must be chained or used as it consumes self
#[must_use = "method moves the value of self and returns the modified value"]
pub fn end_symbol(mut self, end_symbol: Option<&'a str>) -> Self {
self.end_symbol = end_symbol;
@@ -371,12 +289,8 @@ impl<'a> Scrollbar<'a> {
/// Sets the style that is used for the end of the scrollbar.
///
/// See [`Scrollbar`] for a visual example of what this represents.
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// This is a fluent setter method which must be chained or used as it consumes self
#[must_use = "method moves the value of self and returns the modified value"]
pub fn end_style<S: Into<Style>>(mut self, end_style: S) -> Self {
self.end_style = end_style.into();
@@ -395,10 +309,7 @@ impl<'a> Scrollbar<'a> {
/// ```
///
/// Only sets begin_symbol, end_symbol and track_symbol if they already contain a value.
/// If they were set to `None` explicitly, this function will respect that choice. Use their
/// respective setters to change their value.
///
/// This is a fluent setter method which must be chained or used as it consumes self
/// If they were set to `None` explicitly, this function will respect that choice.
#[must_use = "method moves the value of self and returns the modified value"]
pub fn symbols(mut self, symbol: Set) -> Self {
self.thumb_symbol = symbol.thumb;
@@ -427,8 +338,6 @@ impl<'a> Scrollbar<'a> {
/// │ └──────── thumb
/// └─────────── begin
/// ```
///
/// This is a fluent setter method which must be chained or used as it consumes self
#[must_use = "method moves the value of self and returns the modified value"]
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
let style = style.into();

View File

@@ -1,4 +1,4 @@
use crate::{prelude::*, widgets::Widget};
use crate::prelude::*;
/// A [`Cell`] contains the [`Text`] to be displayed in a [`Row`] of a [`Table`].
///
@@ -7,8 +7,6 @@ use crate::{prelude::*, widgets::Widget};
/// [`Style`] of the [`Cell`] by adding the [`Style`] of the [`Text`] content to the [`Style`] of
/// the [`Cell`]. Styles set on the text content will only affect the content.
///
/// You can use [`Text::alignment`] when creating a cell to align its content.
///
/// # Examples
///
/// You can create a `Cell` from anything that can be converted to a [`Text`].
@@ -134,7 +132,24 @@ impl<'a> Cell<'a> {
impl Cell<'_> {
pub(crate) fn render(&self, area: Rect, buf: &mut Buffer) {
buf.set_style(area, self.style);
self.content.clone().render(area, buf);
for (i, line) in self.content.lines.iter().enumerate() {
if i as u16 >= area.height {
break;
}
let x_offset = match line.alignment {
Some(Alignment::Center) => (area.width / 2).saturating_sub(line.width() as u16 / 2),
Some(Alignment::Right) => area.width.saturating_sub(line.width() as u16),
_ => 0,
};
let x = area.x + x_offset;
if x >= area.right() {
continue;
}
buf.set_line(x, area.y + i as u16, line, area.width);
}
}
}

View File

@@ -45,14 +45,6 @@ use crate::prelude::*;
/// ]);
/// ```
///
/// An iterator whose item type is convertible into [`Text`] can be collected into a row.
///
/// ```rust
/// use ratatui::widgets::Row;
///
/// (0..10).map(|i| format!("{i}")).collect::<Row>();
/// ```
///
/// `Row` implements [`Styled`] which means you can use style shorthands from the [`Stylize`] trait
/// to set the style of the row concisely.
///
@@ -243,15 +235,6 @@ impl<'a> Styled for Row<'a> {
}
}
impl<'a, Item> FromIterator<Item> for Row<'a>
where
Item: Into<Cell<'a>>,
{
fn from_iter<IterCells: IntoIterator<Item = Item>>(cells: IterCells) -> Self {
Row::new(cells)
}
}
#[cfg(test)]
mod tests {
use std::vec;
@@ -266,13 +249,6 @@ mod tests {
assert_eq!(row.cells, cells);
}
#[test]
fn collect() {
let cells = vec![Cell::from("")];
let row: Row = cells.iter().cloned().collect();
assert_eq!(row.cells, cells);
}
#[test]
fn cells() {
let cells = vec![Cell::from("")];

File diff suppressed because it is too large Load Diff

View File

@@ -12,11 +12,11 @@
/// [`offset`]: TableState::offset()
/// [`selected`]: TableState::selected()
///
/// See the `table`` example and the `recipe`` and `traceroute`` tabs in the demo2 example in the
/// [Examples] directory for a more in depth example of the various configuration options and for
/// how to handle state.
/// See the [table example] and the recipe and traceroute tabs in the [demo2 example] for a more in
/// depth example of the various configuration options and for how to handle state.
///
/// [Examples]: https://github.com/ratatui-org/ratatui/blob/master/examples/README.md
/// [table example]: https://github.com/ratatui-org/ratatui/blob/master/examples/table.rs
/// [demo2 example]: https://github.com/ratatui-org/ratatui/blob/master/examples/demo2/
///
/// # Example
///
@@ -45,7 +45,6 @@
/// [`Table::widths`]: crate::widgets::Table::widths
/// [`Frame::render_stateful_widget`]: crate::Frame::render_stateful_widget
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TableState {
pub(crate) offset: usize,
pub(crate) selected: Option<usize>,

43
src/widgets/tabs.rs Executable file → Normal file
View File

@@ -28,15 +28,6 @@ const DEFAULT_HIGHLIGHT_STYLE: Style = Style::new().add_modifier(Modifier::REVER
/// .divider(symbols::DOT)
/// .padding("->", "<-");
/// ```
///
/// In addition to `Tabs::new`, any iterator whose element is convertible to `Line` can be collected
/// into `Tabs`.
///
/// ```
/// use ratatui::widgets::Tabs;
///
/// (0..5).map(|i| format!("Tab{i}")).collect::<Tabs>();
/// ```
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Tabs<'a> {
/// A block to wrap this widget in if necessary
@@ -88,10 +79,9 @@ impl<'a> Tabs<'a> {
/// # use ratatui::{prelude::*, widgets::Tabs};
/// let tabs = Tabs::new(vec!["Tab 1".red(), "Tab 2".blue()]);
/// ```
pub fn new<Iter>(titles: Iter) -> Tabs<'a>
pub fn new<T>(titles: Vec<T>) -> Tabs<'a>
where
Iter: IntoIterator,
Iter::Item: Into<Line<'a>>,
T: Into<Line<'a>>,
{
Tabs {
block: None,
@@ -316,15 +306,6 @@ impl<'a> Widget for Tabs<'a> {
}
}
impl<'a, Item> FromIterator<Item> for Tabs<'a>
where
Item: Into<Line<'a>>,
{
fn from_iter<Iter: IntoIterator<Item = Item>>(iter: Iter) -> Self {
Self::new(iter)
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -354,26 +335,6 @@ mod tests {
);
}
#[test]
fn new_from_vec_of_str() {
Tabs::new(vec!["a", "b"]);
}
#[test]
fn collect() {
let tabs: Tabs = (0..5).map(|i| format!("Tab{i}")).collect();
assert_eq!(
tabs.titles,
vec![
Line::from("Tab0"),
Line::from("Tab1"),
Line::from("Tab2"),
Line::from("Tab3"),
Line::from("Tab4"),
],
);
}
fn render(tabs: Tabs, area: Rect) -> Buffer {
let mut buffer = Buffer::empty(area);
tabs.render(area, &mut buffer);

View File

@@ -1,204 +0,0 @@
//! State like [`ListState`], [`TableState`] and [`ScrollbarState`] can be serialized and
//! deserialized through serde. This allows saving your entire state to disk when the user exits the
//! the app, and restore it again upon re-opening the app.
//! This way, they get right back to where they were, without having to re-seek to their previous
//! position, if that's applicable for the app at hand.
//!
//! **Note**: For this pattern to work easily, you need to have some toplevel struct which stores
//! _only_ state and not any draw commands.
//!
//! **Note**: For many applications, it might be beneficial to instead keep your own state and
//! instead construct the state for widgets on the fly instead, if that allows you to express you
//! the semantic meaning of your state better or only fetch part of a dataset.
// not too happy about the redundancy in these tests,
// but if that helps readability then it's ok i guess /shrug
use ratatui::{backend::TestBackend, prelude::*, widgets::*};
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
struct AppState {
list_state: ListState,
table_state: TableState,
scrollbar_state: ScrollbarState,
}
impl Default for AppState {
fn default() -> Self {
Self {
list_state: ListState::default(),
table_state: TableState::default(),
scrollbar_state: ScrollbarState::new(10),
}
}
}
impl AppState {
fn select(&mut self, index: usize) {
self.list_state.select(Some(index));
self.table_state.select(Some(index));
self.scrollbar_state = self.scrollbar_state.position(index);
}
}
/// Renders the list to a TestBackend and asserts that the result matches the expected buffer.
#[track_caller]
fn assert_buffer(state: &mut AppState, expected: &Buffer) {
let backend = TestBackend::new(21, 5);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let items = vec![
"awa", "banana", "Cats!!", "d20", "Echo", "Foxtrot", "Golf", "Hotel", "IwI",
"Juliett",
];
use Constraint::*;
let layout = Layout::default()
.direction(Direction::Horizontal)
.constraints([Length(10), Length(10), Length(1)])
.split(f.size());
let list = List::new(items.clone())
.highlight_symbol(">>")
.block(Block::default().borders(Borders::RIGHT));
f.render_stateful_widget(list, layout[0], &mut state.list_state);
let table = Table::new(
items.iter().map(|i| Row::new(vec![*i])),
[Constraint::Length(10); 1],
)
.highlight_symbol(">>");
f.render_stateful_widget(table, layout[1], &mut state.table_state);
let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight);
f.render_stateful_widget(scrollbar, layout[2], &mut state.scrollbar_state);
})
.unwrap();
terminal.backend().assert_buffer(expected);
}
const DEFAULT_STATE_BUFFER: [&str; 5] = [
"awa │awa ▲",
"banana │banana █",
"Cats!! │Cats!! ║",
"d20 │d20 ║",
"Echo │Echo ▼",
];
const DEFAULT_STATE_REPR: &str = r#"{
"list_state": {
"offset": 0,
"selected": null
},
"table_state": {
"offset": 0,
"selected": null
},
"scrollbar_state": {
"content_length": 10,
"position": 0,
"viewport_content_length": 0
}
}"#;
#[test]
fn default_state_serialize() {
let mut state = AppState::default();
let expected = Buffer::with_lines(DEFAULT_STATE_BUFFER.to_vec());
assert_buffer(&mut state, &expected);
let state = serde_json::to_string_pretty(&state).unwrap();
assert_eq!(state, DEFAULT_STATE_REPR);
}
#[test]
fn default_state_deserialize() {
let expected = Buffer::with_lines(DEFAULT_STATE_BUFFER.to_vec());
let mut state: AppState = serde_json::from_str(DEFAULT_STATE_REPR).unwrap();
assert_buffer(&mut state, &expected);
}
const SELECTED_STATE_BUFFER: [&str; 5] = [
" awa │ awa ▲",
">>banana │>>banana █",
" Cats!! │ Cats!! ║",
" d20 │ d20 ║",
" Echo │ Echo ▼",
];
const SELECTED_STATE_REPR: &str = r#"{
"list_state": {
"offset": 0,
"selected": 1
},
"table_state": {
"offset": 0,
"selected": 1
},
"scrollbar_state": {
"content_length": 10,
"position": 1,
"viewport_content_length": 0
}
}"#;
#[test]
fn selected_state_serialize() {
let mut state = AppState::default();
state.select(1);
let expected = Buffer::with_lines(SELECTED_STATE_BUFFER.to_vec());
assert_buffer(&mut state, &expected);
let state = serde_json::to_string_pretty(&state).unwrap();
assert_eq!(state, SELECTED_STATE_REPR);
}
#[test]
fn selected_state_deserialize() {
let expected = Buffer::with_lines(SELECTED_STATE_BUFFER.to_vec());
let mut state: AppState = serde_json::from_str(SELECTED_STATE_REPR).unwrap();
assert_buffer(&mut state, &expected);
}
const SCROLLED_STATE_BUFFER: [&str; 5] = [
" Echo │ Echo ▲",
" Foxtrot│ Foxtrot ║",
" Golf │ Golf ║",
" Hotel │ Hotel █",
">>IwI │>>IwI ▼",
];
const SCROLLED_STATE_REPR: &str = r#"{
"list_state": {
"offset": 4,
"selected": 8
},
"table_state": {
"offset": 4,
"selected": 8
},
"scrollbar_state": {
"content_length": 10,
"position": 8,
"viewport_content_length": 0
}
}"#;
#[test]
fn scrolled_state_serialize() {
let mut state = AppState::default();
state.select(8);
let expected = Buffer::with_lines(SCROLLED_STATE_BUFFER.to_vec());
assert_buffer(&mut state, &expected);
let state = serde_json::to_string_pretty(&state).unwrap();
assert_eq!(state, SCROLLED_STATE_REPR);
}
#[test]
fn scrolled_state_deserialize() {
let expected = Buffer::with_lines(SCROLLED_STATE_BUFFER.to_vec());
let mut state: AppState = serde_json::from_str(SCROLLED_STATE_REPR).unwrap();
assert_buffer(&mut state, &expected);
}

View File

@@ -50,31 +50,6 @@ fn terminal_draw_returns_the_completed_frame() -> Result<(), Box<dyn Error>> {
Ok(())
}
#[test]
fn terminal_draw_increments_frame_count() -> Result<(), Box<dyn Error>> {
let backend = TestBackend::new(10, 10);
let mut terminal = Terminal::new(backend)?;
let frame = terminal.draw(|f| {
assert_eq!(f.count(), 0);
let paragraph = Paragraph::new("Test");
f.render_widget(paragraph, f.size());
})?;
assert_eq!(frame.count, 0);
let frame = terminal.draw(|f| {
assert_eq!(f.count(), 1);
let paragraph = Paragraph::new("test");
f.render_widget(paragraph, f.size());
})?;
assert_eq!(frame.count, 1);
let frame = terminal.draw(|f| {
assert_eq!(f.count(), 2);
let paragraph = Paragraph::new("test");
f.render_widget(paragraph, f.size());
})?;
assert_eq!(frame.count, 2);
Ok(())
}
#[test]
fn terminal_insert_before_moves_viewport() -> Result<(), Box<dyn Error>> {
// When we have a terminal with 5 lines, and a single line viewport, if we insert a

44
tests/widgets_table.rs Executable file → Normal file
View File

@@ -399,28 +399,26 @@ fn widgets_table_columns_widths_can_use_mixed_constraints() {
]),
);
// This test is unstable and should not be in the test suite
//
// // columns of large size (>100% total) hide the last column
// test_case(
// &[
// Constraint::Percentage(60),
// Constraint::Length(10),
// Constraint::Proportional(60),
// ],
// Buffer::with_lines(vec![
// "┌────────────────────────────┐",
// "│Head1 Head2 │",
// "│ │",
// "│Row11 Row12 │",
// "│Row21 Row22 │",
// "│Row31 Row32",
// "│Row41 Row42",
// "│ │",
// "│ │",
// "└────────────────────────────┘",
// ]),
// );
// columns of large size (>100% total) hide the last column
test_case(
&[
Constraint::Percentage(60),
Constraint::Length(10),
Constraint::Percentage(60),
],
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│Head1 Head2 │",
"│ │",
"│Row11 Row12 │",
"│Row21 Row22 │",
"│Row31 Row32 │",
"│Row41 Row42 │",
"",
"",
"└────────────────────────────┘",
]),
);
}
#[test]
@@ -851,7 +849,7 @@ fn widgets_table_should_render_even_if_empty() {
.draw(|f| {
let size = f.size();
let table = Table::new(
Vec::<Row>::new(),
vec![],
[
Constraint::Length(6),
Constraint::Length(6),

View File

@@ -4,6 +4,7 @@ use ratatui::{
layout::Rect,
style::{Style, Stylize},
symbols,
text::Line,
widgets::Tabs,
Terminal,
};
@@ -14,7 +15,7 @@ fn widgets_tabs_should_not_panic_on_narrow_areas() {
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let tabs = Tabs::new(["Tab1", "Tab2"]);
let tabs = Tabs::new(["Tab1", "Tab2"].iter().cloned().map(Line::from).collect());
f.render_widget(
tabs,
Rect {
@@ -36,7 +37,7 @@ fn widgets_tabs_should_truncate_the_last_item() {
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let tabs = Tabs::new(["Tab1", "Tab2"]);
let tabs = Tabs::new(["Tab1", "Tab2"].iter().cloned().map(Line::from).collect());
f.render_widget(
tabs,
Rect {