Compare commits

..

2 Commits

Author SHA1 Message Date
Orhun Parmaksız
c70a84b3fe chore(release): configure git-cliff 2023-07-31 16:37:23 +03:00
Orhun Parmaksız
8ed244eeb9 feat(release): add automation for creating nightly releases 2023-07-31 16:30:39 +03:00
101 changed files with 2418 additions and 9391 deletions

View File

@@ -15,11 +15,9 @@ defaults:
shell: bash
jobs:
publish-alpha:
name: Create an alpha release
publish-nightly:
name: Create a nightly release
runs-on: ubuntu-latest
permissions:
contents: write
if: ${{ !startsWith(github.event.ref, 'refs/tags/v') }}
steps:
- name: Checkout the repository
@@ -29,38 +27,34 @@ jobs:
- name: Calculate the next release
run: |
suffix="alpha"
last_tag="$(git tag --sort=committerdate | tail -1)"
if [[ "${last_tag}" = *"-${suffix}"* ]]; then
suffix="-alpha"
last_tag="$(git describe --abbrev=0 --tags `git rev-list --tags --max-count=1`)"
if [[ "${last_tag}" = *"${suffix}"* ]]; then
# increment the alpha version
# e.g. v0.22.1-alpha.12 -> v0.22.1-alpha.13
alpha="${last_tag##*-${suffix}.}"
next_alpha="$((alpha + 1))"
next_tag="${last_tag/%${alpha}/${next_alpha}}"
alpha=$(echo "${last_tag}" | grep -oE '([0-9]+)$')
next_alpha=$((alpha + 1))
next_tag=$(echo "${last_tag}" | sed "s/\.[0-9]\+$/\.${next_alpha}/")
else
# increment the patch and start the alpha version from 0
# e.g. v0.22.0 -> v0.22.1-alpha.0
patch="${last_tag##*.}"
next_patch="$((patch + 1))"
next_tag="${last_tag/%${patch}/${next_patch}}-${suffix}.0"
# start the alpha version from 0
next_tag="${last_tag}${suffix}.0"
fi
# update the crate version
msg="# crate version"
sed -E -i "s/^version = .* ${msg}$/version = \"${next_tag#v}\" ${msg}/" Cargo.toml
echo "NEXT_TAG=${next_tag}" >> $GITHUB_ENV
echo "Next alpha release: ${next_tag} 🐭"
echo "Next nightly release: ${next_tag} 🐭"
- name: Publish on crates.io
uses: actions-rs/cargo@v1
with:
command: publish
args: --allow-dirty --token ${{ secrets.CARGO_TOKEN }}
args: --dry-run --allow-dirty
- name: Generate a changelog
uses: orhun/git-cliff-action@v2
with:
config: cliff.toml
args: --unreleased --tag ${{ env.NEXT_TAG }} --strip header
args: --unreleased --strip header
env:
OUTPUT: BODY.md
@@ -83,4 +77,4 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: publish
args: --token ${{ secrets.CARGO_TOKEN }}
args: --dry-run

View File

@@ -1,83 +0,0 @@
name: Check Pull Requests
on:
pull_request_target:
types:
- opened
- edited
- synchronize
- labeled
- unlabeled
merge_group:
permissions:
pull-requests: write
jobs:
check-title:
runs-on: ubuntu-latest
steps:
- name: Check PR title
if: github.event_name == 'pull_request_target'
uses: amannn/action-semantic-pull-request@v5
id: check_pr_title
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Add comment indicating we require pull request titles to follow conventional commits specification
- uses: marocchino/sticky-pull-request-comment@v2
if: always() && (steps.check_pr_title.outputs.error_message != null)
with:
header: pr-title-lint-error
message: |
Thank you for opening this pull request!
We require pull request titles to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and it looks like your proposed title needs to be adjusted.
Details:
> ${{ steps.check_pr_title.outputs.error_message }}
# Delete a previous comment when the issue has been resolved
- if: ${{ steps.check_pr_title.outputs.error_message == null }}
uses: marocchino/sticky-pull-request-comment@v2
with:
header: pr-title-lint-error
delete: true
check-breaking-change-label:
runs-on: ubuntu-latest
steps:
- name: Check breaking change label
id: check_breaking_change
run: |
pattern='^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\(\w+\))?!:'
# Check if pattern matches
if echo "${{ github.event.pull_request.title }}" | grep -qE "$pattern"; then
echo "breaking_change=true" >> $GITHUB_OUTPUT
else
echo "breaking_change=false" >> $GITHUB_OUTPUT
fi
- name: Add label
if: steps.check_breaking_change.outputs.breaking_change == 'true'
uses: actions/github-script@v6
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
github.rest.issues.addLabels({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: ['breaking change']
})
do-not-merge:
if: ${{ contains(github.event.*.labels.*.name, 'do not merge') }}
name: Prevent Merging
runs-on: ubuntu-latest
steps:
- name: Check for label
run: |
echo "Pull request is labeled as 'do not merge'"
echo "This workflow fails so that the pull request cannot be merged"
exit 1

View File

@@ -1,4 +1,4 @@
name: Continuous Integration
name: CI
on:
# Allows you to run this workflow manually from the Actions tab
@@ -36,16 +36,6 @@ jobs:
uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Install Rust nightly
uses: dtolnay/rust-toolchain@nightly
with:
components: rustfmt
- name: Install cargo-make
uses: taiki-e/install-action@cargo-make
- name: Check formatting
run: cargo make lint-format
- name: Check documentation
run: cargo make lint-docs
- name: Check conventional commits
uses: crate-ci/committed@master
with:
@@ -55,6 +45,14 @@ jobs:
uses: crate-ci/typos@master
- name: Lint dependencies
uses: EmbarkStudios/cargo-deny-action@v1
- name: Install Rust nightly
uses: dtolnay/rust-toolchain@nightly
with:
components: rustfmt
- name: Install cargo-make
uses: taiki-e/install-action@cargo-make
- name: Check formatting
run: cargo make fmt
clippy:
runs-on: ubuntu-latest
@@ -93,10 +91,9 @@ jobs:
check:
strategy:
fail-fast: false
matrix:
os: [ ubuntu-latest, windows-latest, macos-latest ]
toolchain: [ "1.67.0", "stable" ]
toolchain: [ "1.65.0", "stable" ]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
@@ -114,7 +111,6 @@ jobs:
test-doc:
strategy:
fail-fast: false
matrix:
os: [ ubuntu-latest, windows-latest, macos-latest ]
runs-on: ${{ matrix.os }}
@@ -132,10 +128,9 @@ jobs:
test:
strategy:
fail-fast: false
matrix:
os: [ ubuntu-latest, windows-latest, macos-latest ]
toolchain: [ "1.67.0", "stable" ]
toolchain: [ "1.65.0", "stable" ]
backend: [ crossterm, termion, termwiz ]
exclude:
# termion is not supported on windows

View File

@@ -7,6 +7,3 @@ no-inline-html:
- summary
line-length:
line_length: 100
# to support repeated headers in the changelog
no-duplicate-heading: false

File diff suppressed because it is too large Load Diff

View File

@@ -29,11 +29,6 @@ change becomes a place where a bug may have been introduced. Consider splitting
reformatting changes into a separate PR from those that make a behavioral change, as the tests help
guarantee that the behavior is unchanged.
### Code formatting
Run `cargo make format` before committing to ensure that code is consistently formatted with
rustfmt. Configuration is in [rustfmt.toml](./rustfmt.toml).
### Search `tui-rs` for similar work
The original fork of Ratatui, [`tui-rs`](https://github.com/fdehau/tui-rs/), has a large amount of
@@ -118,59 +113,6 @@ exist to show coverage directly in your editor. E.g.:
- <https://marketplace.visualstudio.com/items?itemName=ryanluker.vscode-coverage-gutters>
- <https://github.com/alepez/vim-llvmcov>
### Documentation
Here are some guidelines for writing documentation in Ratatui.
Every public API **must** be documented.
Keep in mind that Ratatui tends to attract beginner Rust users that may not be familiar with Rust
concepts.
#### Content
The main doc comment should talk about the general features that the widget supports and introduce
the concepts pointing to the various methods. Focus on interaction with various features and giving
enough information that helps understand why you might want something.
Examples should help users understand a particular usage, not test a feature. They should be as
simple as possible.
Prefer hiding imports and using wildcards to keep things concise. Some imports may still be shown
to demonstrate a particular non-obvious import (e.g. `Stylize` trait to use style methods).
Speaking of `Stylize`, you should use it over the more verbose style setters:
```rust
let style = Style::new().red().bold();
// not
let style = Style::default().fg(Color::Red).add_modifier(Modifiers::BOLD);
```
#### Format
- First line is summary, second is blank, third onward is more detail
```rust
/// Summary
///
/// A detailed description
/// with examples.
fn foo() {}
```
- Max line length is 100 characters
See [vscode rewrap extension](https://marketplace.visualstudio.com/items?itemName=stkb.rewrap)
- Doc comments are above macros
i.e.
```rust
/// doc comment
#[derive(Debug)]
struct Foo {}
```
- Code items should be between backticks
i.e. ``[`Block`]``, **NOT** ``[Block]``
### 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

@@ -1,8 +1,8 @@
[package]
name = "ratatui"
version = "0.23.0" # crate version
version = "0.22.0" # crate version
authors = ["Florian Dehau <work@fdehau.com>", "The Ratatui Developers"]
description = "A library that's all about cooking up terminal user interfaces"
description = "A library to build rich terminal user interfaces or dashboards"
documentation = "https://docs.rs/ratatui/latest/ratatui/"
keywords = ["tui", "terminal", "dashboard"]
repository = "https://github.com/ratatui-org/ratatui"
@@ -18,63 +18,16 @@ exclude = [
]
autoexamples = true
edition = "2021"
rust-version = "1.67.0"
rust-version = "1.65.0"
[badges]
[dependencies]
#! The crate provides a set of optional features that can be enabled in your `cargo.toml` file.
#!
#! Generally an application will only use one backend, so you should only enable one of the following features:
## enables the [`CrosstermBackend`] backend and adds a dependency on the [Crossterm crate].
crossterm = { version = "0.27", optional = true }
## enables the [`TermionBackend`] backend and adds a dependency on the [Termion crate].
termion = { version = "2.0", optional = true }
## enables the [`TermwizBackend`] backend and adds a dependency on the [Termwiz crate].
termwiz = { version = "0.20.0", optional = true }
serde = { version = "1", optional = true, features = ["derive"] }
bitflags = "2.3"
cassowary = "0.3"
indoc = "2.0"
itertools = "0.11"
paste = "1.0.2"
strum = { version = "0.25", features = ["derive"] }
time = { version = "0.3.11", optional = true, features = ["local-offset"] }
unicode-segmentation = "1.10"
unicode-width = "0.1"
document-features = { version = "0.2.7", optional = true }
lru = "0.11.1"
[dev-dependencies]
anyhow = "1.0.71"
argh = "0.1"
better-panic = "0.3.0"
cargo-husky = { version = "1.5.0", default-features = false, features = [
"user-hooks",
] }
criterion = { version = "0.5", features = ["html_reports"] }
fakeit = "1.1"
rand = "0.8"
pretty_assertions = "1.4.0"
[features]
default = ["crossterm"]
#! The following optional features are available for all backends:
## enables serialization and deserialization of style and color types using the [Serde crate].
## This is useful if you want to save themes to a file.
serde = ["dep:serde", "bitflags/serde"]
## enables the [`border!`] macro.
macros = []
## enables all widgets.
all-widgets = ["widget-calendar"]
#! Widgets that add dependencies are gated behind feature flags to prevent unused transitive
#! dependencies. The available features are:
## enables the [`calendar`] widget module and adds a dependency on the [Time crate].
widget-calendar = ["dep:time"]
widget-calendar = ["time"]
macros = []
serde = ["dep:serde", "bitflags/serde"]
[package.metadata.docs.rs]
all-features = true
@@ -82,27 +35,32 @@ all-features = true
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]
rustdoc-args = ["--cfg", "docsrs"]
[[bench]]
name = "barchart"
harness = false
[dependencies]
bitflags = "2.3"
cassowary = "0.3"
crossterm = { version = "0.26", optional = true }
indoc = "2.0"
paste = "1.0.2"
serde = { version = "1", optional = true, features = ["derive"] }
termion = { version = "2.0", optional = true }
termwiz = { version = "0.20.0", optional = true }
time = { version = "0.3.11", optional = true, features = ["local-offset"] }
unicode-segmentation = "1.10"
unicode-width = "0.1"
[[bench]]
name = "block"
harness = false
[[bench]]
name = "list"
harness = false
[dev-dependencies]
anyhow = "1.0.71"
argh = "0.1"
cargo-husky = { version = "1.5.0", default-features = false, features = ["user-hooks"] }
criterion = { version = "0.5", features = ["html_reports"] }
fakeit = "1.1"
itertools = "0.10"
rand = "0.8"
[[bench]]
name = "paragraph"
harness = false
[[bench]]
name = "sparkline"
harness = false
[[example]]
name = "barchart"
required-features = ["crossterm"]
@@ -128,17 +86,6 @@ name = "chart"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "colors"
required-features = ["crossterm"]
# this example is a bit verbose, so we don't want to include it in the docs
doc-scrape-examples = false
[[example]]
name = "colors_rgb"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "custom_widget"
required-features = ["crossterm"]
@@ -169,12 +116,6 @@ name = "list"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "modifiers"
required-features = ["crossterm"]
# this example is a bit verbose, so we don't want to include it in the docs
doc-scrape-examples = false
[[example]]
name = "panic"
required-features = ["crossterm"]

View File

@@ -7,149 +7,159 @@ skip_core_tasks = true
# all features except the backend ones
ALL_FEATURES = "all-widgets,macros,serde"
# Windows does not support building termion, so this avoids the build failure by providing two
# sets of flags, one for Windows and one for other platforms.
# Windows: --features=all-widgets,macros,serde,crossterm,termwiz
# Other: --all-features
ALL_FEATURES_FLAG = { source = "${CARGO_MAKE_RUST_TARGET_OS}", default_value = "--all-features", mapping = { "windows" = "--features=all-widgets,macros,serde,crossterm,termwiz" } }
[tasks.default]
alias = "ci"
[tasks.ci]
description = "Run continuous integration tasks"
dependencies = ["lint-style", "clippy", "check", "test"]
dependencies = [
"style-check",
"clippy",
"check",
"test",
]
[tasks.lint-style]
description = "Lint code style (formatting, typos, docs)"
dependencies = ["lint-format", "lint-typos", "lint-docs"]
[tasks.style-check]
dependencies = ["fmt", "typos"]
[tasks.lint-format]
description = "Lint code formatting"
[tasks.fmt]
toolchain = "nightly"
command = "cargo"
args = ["fmt", "--all", "--check"]
[tasks.format]
description = "Fix code formatting"
toolchain = "nightly"
command = "cargo"
args = ["fmt", "--all"]
[tasks.lint-typos]
description = "Run typo checks"
[tasks.typos]
install_crate = { crate_name = "typos-cli", binary = "typos", test_arg = "--version" }
command = "typos"
[tasks.lint-docs]
description = "Check documentation for errors and warnings"
toolchain = "nightly"
command = "cargo"
args = [
"rustdoc",
"--no-default-features",
"${ALL_FEATURES_FLAG}",
"--",
"-Zunstable-options",
"--check",
"-Dwarnings",
]
[tasks.check]
description = "Check code for errors and warnings"
command = "cargo"
args = [
"check",
"--all-targets",
"--no-default-features",
"${ALL_FEATURES_FLAG}",
"--all-features"
]
[tasks.check.windows]
args = [
"check",
"--all-targets",
"--no-default-features", "--features", "${ALL_FEATURES},crossterm,termwiz"
]
[tasks.build]
description = "Compile the project"
command = "cargo"
args = [
"build",
"--all-targets",
"--no-default-features",
"${ALL_FEATURES_FLAG}",
"--all-features",
]
[tasks.build.windows]
args = [
"build",
"--all-targets",
"--no-default-features", "--features", "${ALL_FEATURES},crossterm,termwiz"
]
[tasks.clippy]
description = "Run Clippy for linting"
command = "cargo"
args = [
"clippy",
"--all-targets",
"--tests",
"--benches",
"--no-default-features",
"${ALL_FEATURES_FLAG}",
"--all-features",
"--",
"-D",
"warnings",
]
[tasks.clippy.windows]
args = [
"clippy",
"--all-targets",
"--tests",
"--benches",
"--no-default-features", "--features", "${ALL_FEATURES},crossterm,termwiz",
"--",
"-D",
"warnings",
]
[tasks.test]
description = "Run tests"
dependencies = ["test-doc"]
dependencies = [
"test-doc",
]
command = "cargo"
args = [
"test",
"--all-targets",
"--no-default-features",
"${ALL_FEATURES_FLAG}",
"--all-features",
]
[tasks.test-windows]
dependencies = [
"test-doc",
]
args = [
"test",
"--all-targets",
"--no-default-features", "--features", "${ALL_FEATURES},crossterm,termwiz"
]
[tasks.test-doc]
description = "Run documentation tests"
command = "cargo"
args = ["test", "--doc", "--no-default-features", "${ALL_FEATURES_FLAG}"]
args = [
"test", "--doc",
"--all-features",
]
[tasks.test-doc.windows]
args = [
"test", "--doc",
"--no-default-features", "--features", "${ALL_FEATURES},crossterm,termwiz"
]
[tasks.test-backend]
# takes a command line parameter to specify the backend to test (e.g. "crossterm")
description = "Run backend-specific tests"
command = "cargo"
args = [
"test",
"--all-targets",
"--no-default-features",
"--features",
"${ALL_FEATURES},${@}",
"--no-default-features", "--features", "${ALL_FEATURES},${@}"
]
[tasks.coverage]
description = "Generate code coverage report"
command = "cargo"
args = [
"llvm-cov",
"--lcov",
"--output-path",
"target/lcov.info",
"--output-path", "target/lcov.info",
"--all-features",
]
[tasks.coverage.windows]
command = "cargo"
args = [
"llvm-cov",
"--lcov",
"--output-path", "target/lcov.info",
"--no-default-features",
"${ALL_FEATURES_FLAG}",
"--features", "${ALL_FEATURES},crossterm,termwiz",
]
[tasks.run-example]
private = true
condition = { env_set = ["TUI_EXAMPLE_NAME"] }
command = "cargo"
args = [
"run",
"--release",
"--example",
"${TUI_EXAMPLE_NAME}",
"--features",
"all-widgets",
]
args = ["run", "--release", "--example", "${TUI_EXAMPLE_NAME}", "--features", "all-widgets"]
[tasks.build-examples]
description = "Compile project examples"
command = "cargo"
args = ["build", "--examples", "--release", "--features", "all-widgets"]
[tasks.run-examples]
description = "Run project examples"
dependencies = ["build-examples"]
script = '''
#!@duckscript

View File

@@ -2,8 +2,8 @@
<img align="left" src="https://avatars.githubusercontent.com/u/125200832?s=128&v=4">
`ratatui` is a [Rust](https://www.rust-lang.org) library that is all about cooking up terminal user interfaces.
It is a community fork of the original [tui-rs](https://github.com/fdehau/tui-rs)
`ratatui` is a [Rust](https://www.rust-lang.org) library to build rich terminal user interfaces and
dashboards. It is a community fork of the original [tui-rs](https://github.com/fdehau/tui-rs)
project.
[![Crates.io](https://img.shields.io/crates/v/ratatui?logo=rust&style=flat-square)](https://crates.io/crates/ratatui)
@@ -14,10 +14,9 @@ Status](https://img.shields.io/github/actions/workflow/status/ratatui-org/ratatu
Status](https://deps.rs/repo/github/ratatui-org/ratatui/status.svg?style=flat-square)](https://deps.rs/repo/github/ratatui-org/ratatui)
[![Codecov](https://img.shields.io/codecov/c/github/ratatui-org/ratatui?logo=codecov&style=flat-square&token=BAQ8SOKEST)](https://app.codecov.io/gh/ratatui-org/ratatui)
[![Discord](https://img.shields.io/discord/1070692720437383208?label=discord&logo=discord&style=flat-square)](https://discord.gg/pMCEU9hNEj)
[![Matrix](https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix)](https://matrix.to/#/#ratatui:matrix.org)
<!-- See RELEASE.md for instructions on creating the demo gif --->
![Demo of Ratatui](https://vhs.charm.sh/vhs-tF0QbuPbtHgUeG0sTVgFr.gif)
![Demo of Ratatui](https://github.com/ratatui-org/ratatui/assets/24392180/93ab0e38-93e0-4ae0-a31b-91ae6c393185)
<details>
<summary>Table of Contents</summary>
@@ -52,7 +51,16 @@ Or modify your `Cargo.toml`
```toml
[dependencies]
ratatui = { version = "0.23.0", features = ["all-widgets"]}
ratatui = { version = "0.22.0", features = ["all-widgets"]}
```
Ratatui is mostly backwards compatible with `tui-rs`. To migrate an existing project, it may be
easier to rename the ratatui dependency to `tui` rather than updating every usage of the crate.
E.g.:
```toml
[dependencies]
tui = { package = "ratatui", version = "0.22.0", features = ["all-widgets"]}
```
## Introduction
@@ -132,19 +140,17 @@ the community forked the project and created this crate. We look forward to cont
started by Florian 🚀
In order to organize ourselves, we currently use a [Discord server](https://discord.gg/pMCEU9hNEj),
feel free to join and come chat! There is also a [Matrix](https://matrix.org/) bridge available at
[#ratatui:matrix.org](https://matrix.to/#/#ratatui:matrix.org).
While we do utilize Discord for coordinating, it's not essential for contributing.
Our primary open-source workflow is centered around GitHub.
For significant discussions, we rely on GitHub — please open an issue, a discussion or a PR.
feel free to join and come chat! There are also plans to implement a [Matrix](https://matrix.org/)
bridge in the near future. **Discord is not a MUST to contribute**. We follow a pretty standard
github centered open source workflow keeping the most important conversations on GitHub, open an
issue or PR and it will be addressed. 😄
Please make sure you read the updated [contributing](./CONTRIBUTING.md) guidelines, especially if
you are interested in working on a PR or issue opened in the previous repository.
## Rust version requirements
Since version 0.23.0, The Minimum Supported Rust Version (MSRV) of `ratatui` is 1.67.0.
Since version 0.21.0, The Minimum Supported Rust Version (MSRV) of `ratatui` is 1.65.0.
## Documentation
@@ -208,9 +214,9 @@ be installed with `cargo install cargo-make`).
### Third-party libraries, bootstrapping templates and widgets
* [ansi-to-tui](https://github.com/uttarayan21/ansi-to-tui) — Convert ansi colored text to
`ratatui::text::Text`
`tui::text::Text`
* [color-to-tui](https://github.com/uttarayan21/color-to-tui) — Parse hex colors to
`ratatui::style::Color`
`tui::style::Color`
* [rust-tui-template](https://github.com/ratatui-org/rust-tui-template) — A template for bootstrapping a
Rust TUI application with Tui-rs & crossterm
* [simple-tui-rs](https://github.com/pmsanford/simple-tui-rs) — A simple example tui-rs app

View File

@@ -3,19 +3,24 @@
[crates.io](https://crates.io/crates/ratatui) releases are automated via [GitHub
actions](.github/workflows/cd.yml) and triggered by pushing a tag.
1. Record a new demo gif if necessary. The preferred tool for this is
[vhs](https://github.com/charmbracelet/vhs) (installation instructions in README).
1. Record a new demo gif. The preferred tool for this is [ttyrec](http://0xcc.net/ttyrec/) and
[ttygif](https://github.com/icholy/ttygif). [Asciinema](https://asciinema.org/) handles block
character height poorly, [termanilizer](https://www.terminalizer.com/) takes forever to render,
[vhs](https://github.com/charmbracelet/vhs) handles braille
characters poorly (though if <https://github.com/charmbracelet/vhs/issues/322> is fixed, then
it's probably the best option).
```shell
cargo build --example demo
vhs examples/demo.tape --publish --quiet
ttyrec -e 'cargo --quiet run --release --example demo -- --tick-rate 100' demo.rec
ttygif demo.rec
```
Then update the link in the [examples README](./examples/README) and the main README. Avoid
adding the gif to the git repo as binary files tend to bloat repositories.
Then upload it somewhere (e.g. use `vhs publish tty.gif` to publish it or upload it to a GitHub
wiki page as an attachment). Avoid adding the gif to the git repo as binary files tend to bloat
repositories.
1. Bump the version in [Cargo.toml](Cargo.toml).
1. Bump versions in the doc comments of [lib.rs](src/lib.rs).
1. Ensure [CHANGELOG.md](CHANGELOG.md) is updated. [git-cliff](https://github.com/orhun/git-cliff)
can be used for generating the entries.
1. Commit and push the changes.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,73 +0,0 @@
use criterion::{criterion_group, criterion_main, Bencher, BenchmarkId, Criterion};
use rand::Rng;
use ratatui::{
buffer::Buffer,
layout::Rect,
prelude::Direction,
widgets::{Bar, BarChart, BarGroup, Widget},
};
/// Benchmark for rendering a barchart.
pub fn barchart(c: &mut Criterion) {
let mut group = c.benchmark_group("barchart");
let mut rng = rand::thread_rng();
for data_count in [64, 256, 2048] {
let data: Vec<Bar> = (0..data_count)
.map(|i| {
Bar::default()
.label(format!("B{i}").into())
.value(rng.gen_range(0..data_count))
})
.collect();
let bargroup = BarGroup::default().bars(&data);
// Render a basic barchart
group.bench_with_input(
BenchmarkId::new("render", data_count),
&BarChart::default().data(bargroup.clone()),
render,
);
// Render an horizontal barchart
group.bench_with_input(
BenchmarkId::new("render_horizontal", data_count),
&BarChart::default()
.direction(Direction::Horizontal)
.data(bargroup.clone()),
render,
);
// Render a barchart with multiple groups
group.bench_with_input(
BenchmarkId::new("render_grouped", data_count),
&BarChart::default()
// We call `data` multiple time to add multiple groups.
// This is not a duplicated call.
.data(bargroup.clone())
.data(bargroup.clone())
.data(bargroup.clone()),
render,
);
}
group.finish();
}
/// Render the widget in a classical size buffer
fn render(bencher: &mut Bencher, barchart: &BarChart) {
let mut buffer = Buffer::empty(Rect::new(0, 0, 200, 50));
// We use `iter_batched` to clone the value in the setup function.
// See https://github.com/ratatui-org/ratatui/pull/377.
bencher.iter_batched(
|| barchart.clone(),
|bench_barchart| {
bench_barchart.render(buffer.area, &mut buffer);
},
criterion::BatchSize::LargeInput,
)
}
criterion_group!(benches, barchart);
criterion_main!(benches);

View File

@@ -1,64 +0,0 @@
use criterion::{criterion_group, criterion_main, BatchSize, Bencher, BenchmarkId, Criterion};
use ratatui::{
buffer::Buffer,
layout::Rect,
prelude::Alignment,
widgets::{
block::{Position, Title},
Block, Borders, Padding, Widget,
},
};
/// Benchmark for rendering a block.
pub fn block(c: &mut Criterion) {
let mut group = c.benchmark_group("block");
for buffer_size in &[
Rect::new(0, 0, 100, 50), // vertically split screen
Rect::new(0, 0, 200, 50), // 1080p fullscreen with medium font
Rect::new(0, 0, 256, 256), // Max sized area
] {
let buffer_area = buffer_size.area();
// Render an empty block
group.bench_with_input(
BenchmarkId::new("render_empty", buffer_area),
&Block::new(),
|b, block| render(b, block, buffer_size),
);
// Render with all features
group.bench_with_input(
BenchmarkId::new("render_all_feature", buffer_area),
&Block::new()
.borders(Borders::ALL)
.title("test title")
.title(
Title::from("bottom left title")
.alignment(Alignment::Right)
.position(Position::Bottom),
)
.padding(Padding::new(5, 5, 2, 2)),
|b, block| render(b, block, buffer_size),
);
}
group.finish();
}
/// render the block into a buffer of the given `size`
fn render(bencher: &mut Bencher, block: &Block, size: &Rect) {
let mut buffer = Buffer::empty(*size);
// We use `iter_batched` to clone the value in the setup function.
// See https://github.com/ratatui-org/ratatui/pull/377.
bencher.iter_batched(
|| block.to_owned(),
|bench_block| {
bench_block.render(buffer.area, &mut buffer);
},
BatchSize::SmallInput,
)
}
criterion_group!(benches, block);
criterion_main!(benches);

View File

@@ -1,73 +0,0 @@
use criterion::{criterion_group, criterion_main, BatchSize, Bencher, BenchmarkId, Criterion};
use ratatui::{
buffer::Buffer,
layout::Rect,
widgets::{List, ListItem, ListState, StatefulWidget, Widget},
};
/// Benchmark for rendering a list.
/// It only benchmarks the render with a different amount of items.
pub fn list(c: &mut Criterion) {
let mut group = c.benchmark_group("list");
for line_count in [64, 2048, 16384] {
let lines: Vec<ListItem> = (0..line_count)
.map(|_| ListItem::new(fakeit::words::sentence(10)))
.collect();
// Render default list
group.bench_with_input(
BenchmarkId::new("render", line_count),
&List::new(lines.clone()),
render,
);
// Render with an offset to the middle of the list and a selected item
group.bench_with_input(
BenchmarkId::new("render_scroll_half", line_count),
&List::new(lines.clone()).highlight_symbol(">>"),
|b, list| {
render_stateful(
b,
list,
ListState::default()
.with_offset(line_count / 2)
.with_selected(Some(line_count / 2)),
)
},
);
}
group.finish();
}
/// render the list into a common size buffer
fn render(bencher: &mut Bencher, list: &List) {
let mut buffer = Buffer::empty(Rect::new(0, 0, 200, 50));
// We use `iter_batched` to clone the value in the setup function.
// See https://github.com/ratatui-org/ratatui/pull/377.
bencher.iter_batched(
|| list.to_owned(),
|bench_list| {
Widget::render(bench_list, buffer.area, &mut buffer);
},
BatchSize::LargeInput,
)
}
/// render the list into a common size buffer with a state
fn render_stateful(bencher: &mut Bencher, list: &List, mut state: ListState) {
let mut buffer = Buffer::empty(Rect::new(0, 0, 200, 50));
// We use `iter_batched` to clone the value in the setup function.
// See https://github.com/ratatui-org/ratatui/pull/377.
bencher.iter_batched(
|| list.to_owned(),
|bench_list| {
StatefulWidget::render(bench_list, buffer.area, &mut buffer, &mut state);
},
BatchSize::LargeInput,
)
}
criterion_group!(benches, list);
criterion_main!(benches);

View File

@@ -1,6 +1,4 @@
use criterion::{
black_box, criterion_group, criterion_main, BatchSize, Bencher, BenchmarkId, Criterion,
};
use criterion::{black_box, criterion_group, criterion_main, Bencher, BenchmarkId, Criterion};
use ratatui::{
buffer::Buffer,
layout::Rect,
@@ -71,15 +69,9 @@ pub fn paragraph(c: &mut Criterion) {
/// render the paragraph into a buffer with the given width
fn render(bencher: &mut Bencher, paragraph: &Paragraph, width: u16) {
let mut buffer = Buffer::empty(Rect::new(0, 0, width, 50));
// We use `iter_batched` to clone the value in the setup function.
// See https://github.com/ratatui-org/ratatui/pull/377.
bencher.iter_batched(
|| paragraph.to_owned(),
|bench_paragraph| {
bench_paragraph.render(buffer.area, &mut buffer);
},
BatchSize::LargeInput,
)
bencher.iter(|| {
paragraph.clone().render(buffer.area, &mut buffer);
})
}
/// Create a string with the given number of lines filled with nonsense words

View File

@@ -1,45 +0,0 @@
use criterion::{criterion_group, criterion_main, Bencher, BenchmarkId, Criterion};
use rand::Rng;
use ratatui::{
buffer::Buffer,
layout::Rect,
widgets::{Sparkline, Widget},
};
/// Benchmark for rendering a sparkline.
pub fn sparkline(c: &mut Criterion) {
let mut group = c.benchmark_group("sparkline");
let mut rng = rand::thread_rng();
for data_count in [64, 256, 2048] {
let data: Vec<u64> = (0..data_count)
.map(|_| rng.gen_range(0..data_count))
.collect();
// Render a basic sparkline
group.bench_with_input(
BenchmarkId::new("render", data_count),
&Sparkline::default().data(&data),
render,
);
}
group.finish();
}
/// render the block into a buffer of the given `size`
fn render(bencher: &mut Bencher, sparkline: &Sparkline) {
let mut buffer = Buffer::empty(Rect::new(0, 0, 200, 50));
// We use `iter_batched` to clone the value in the setup function.
// See https://github.com/ratatui-org/ratatui/pull/377.
bencher.iter_batched(
|| sparkline.clone(),
|bench_sparkline| {
bench_sparkline.render(buffer.area, &mut buffer);
},
criterion::BatchSize::LargeInput,
)
}
criterion_group!(benches, sparkline);
criterion_main!(benches);

View File

@@ -3,51 +3,35 @@
[changelog]
# changelog header
header = """
# Changelog
All notable changes to this project will be documented in this file.
# Changelog\n
All notable changes to this project will be documented in this file.\n
"""
# template for the changelog body
# https://keats.github.io/tera/docs/#introduction
# note that the - before / after the % controls whether whitespace is rendered between each line.
# Getting this right so that the markdown renders with the correct number of lines between headings
# code fences and list items is pretty finicky. Note also that the 4 backticks in the commit macro
# is intentional as this escapes any backticks in the commit body.
# https://tera.netlify.app/docs/#introduction
body = """
{%- if not version %}
## [unreleased]
{% else -%}
## [{{ version }}](https://github.com/ratatui-org/ratatui/releases/tag/{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }}
{% endif -%}
{% macro commit(commit) -%}
- [{{ commit.id | truncate(length=7, end="") }}]({{ "https://github.com/ratatui-org/ratatui/commit/" ~ commit.id }})
*({{commit.scope | default(value = "uncategorized") | lower }})* {{ commit.message | upper_first }}
{%- if commit.breaking %} [**breaking**]{% endif %}
{%- if commit.body %}
````text {#- 4 backticks escape any backticks in body #}
{{commit.body | indent(prefix=" ") }}
````
{%- endif %}
{% endmacro -%}
{% if version %}\
## {{ version }} - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\
## [unreleased]
{% endif %}\
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | striptags | trim | upper_first }}
{% for commit in commits | filter(attribute="scope") | sort(attribute="scope") %}
{{ self::commit(commit=commit) }}
{%- endfor -%}
{% for commit in commits %}
{%- if not commit.scope %}
{{ self::commit(commit=commit) }}
{%- endif -%}
{%- endfor -%}
{%- endfor %}
### {{ group | striptags | trim | upper_first }}
{% for commit in commits
| filter(attribute="scope")
| sort(attribute="scope") %}
- *({{commit.scope}})* {{ commit.message | upper_first }}{% if commit.breaking %} [**breaking**]{% endif %}
{%- endfor -%}
{% raw %}\n{% endraw %}\
{%- for commit in commits %}
{%- if commit.scope -%}
{% else -%}
- *(uncategorized)* {{ commit.message | upper_first }}{% if commit.breaking %} [**breaking**]{% endif %}
{% endif -%}
{% endfor -%}
{% endfor %}\n
"""
# remove the leading and trailing whitespace from the template
trim = false
trim = true
# changelog footer
footer = """
<!-- generated by git-cliff -->
@@ -62,32 +46,29 @@ filter_unconventional = true
split_commits = false
# regex for preprocessing the commit messages
commit_preprocessors = [
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/ratatui-org/ratatui/issues/${2}))" },
{ pattern = '(better safe shared layout cache)', replace = "perf(layout): ${1}" },
{ pattern = '(Clarify README.md)', replace = "docs(readme): ${1}" },
{ pattern = '(Update README.md)', replace = "docs(readme): ${1}" },
{ pattern = '(fix typos|Fix typos)', replace = "fix: ${1}" },
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/ratatui-org/ratatui/issues/${2}))" },
{ pattern = '(better safe shared layout cache)', replace = "perf(layout): ${1}" },
{ pattern = '(Clarify README.md)', replace = "docs(readme): ${1}" },
{ pattern = '(Update README.md)', replace = "docs(readme): ${1}" },
{ pattern = '(fix typos|Fix typos)', replace = "fix: ${1}" },
]
# regex for parsing and grouping commits
commit_parsers = [
{ message = "^feat", group = "<!-- 00 -->Features" },
{ message = "^[fF]ix", group = "<!-- 01 -->Bug Fixes" },
{ message = "^refactor", group = "<!-- 02 -->Refactor" },
{ message = "^doc", group = "<!-- 03 -->Documentation" },
{ message = "^perf", group = "<!-- 04 -->Performance" },
{ message = "^style", group = "<!-- 05 -->Styling" },
{ message = "^test", group = "<!-- 06 -->Testing" },
{ message = "^chore\\(release\\): prepare for", skip = true },
{ message = "^chore\\(pr\\)", skip = true },
{ message = "^chore\\(pull\\)", skip = true },
{ message = "^[cC]hore", group = "<!-- 07 -->Miscellaneous Tasks" },
{ body = ".*security", group = "<!-- 08 -->Security" },
{ message = "^build", group = "<!-- 09 -->Build" },
{ message = "^ci", group = "<!-- 10 -->Continuous Integration" },
{ message = "^revert", group = "<!-- 11 -->Reverted Commits" },
# handle some old commits styles from pre 0.4
{ message = "^(Buffer|buffer|Frame|frame|Gauge|gauge|Paragraph|paragraph):", group = "<!-- 07 -->Miscellaneous Tasks" },
{ message = "^\\[", group = "<!-- 07 -->Miscellaneous Tasks" },
{ message = "^feat", group = "<!-- 00 -->Features" },
{ message = "^[fF]ix", group = "<!-- 01 -->Bug Fixes" },
{ message = "^refactor", group = "<!-- 02 -->Refactor" },
{ message = "^doc", group = "<!-- 03 -->Documentation" },
{ message = "^perf", group = "<!-- 04 -->Performance" },
{ message = "^style", group = "<!-- 05 -->Styling" },
{ message = "^test", group = "<!-- 06 -->Testing" },
{ message = "^chore\\(release\\): prepare for", skip = true },
{ message = "^chore\\(pr\\)", skip = true },
{ message = "^chore\\(pull\\)", skip = true },
{ message = "^chore", group = "<!-- 07 -->Miscellaneous Tasks" },
{ body = ".*security", group = "<!-- 08 -->Security" },
{ message = "^build", group = "<!-- 09 -->Build" },
{ message = "^ci", group = "<!-- 10 -->Continuous Integration" },
{ message = "^revert", group = "<!-- 11 -->Reverted Commits" },
]
# protect breaking changes from being skipped due to matching a skipping commit_parser
protect_breaking_commits = false
@@ -98,7 +79,7 @@ tag_pattern = "v[0-9]*"
# regex for skipping tags
skip_tags = "v0.1.0-rc.1"
# regex for ignoring tags
ignore_tags = "alpha"
ignore_tags = ""
# sort the tags topologically
topo_order = false
# sort the commits inside sections by oldest/newest order

View File

@@ -1,3 +0,0 @@
ignore:
- "examples"
- "benches"

View File

@@ -6,34 +6,7 @@ VHS has a problem rendering some background color transitions, which shows up in
below. See <https://github.com/charmbracelet/vhs/issues/344> for more info. These problems don't
occur in a terminal.
## Demo
This is the demo example from the main README. It is available for each of the backends. Source:
[demo.rs](./demo/).
```shell
cargo run --example=demo --features=crossterm
cargo run --example=demo --no-default-features --features=termion
cargo run --example=demo --no-default-features --features=termwiz
```
![Demo][demo.gif]
## Hello World
This is a pretty boring example, but it contains some good documentation
on writing tui apps. Source: [hello_world.rs](./hello_world.rs).
```shell
cargo run --example=hello_world --features=crossterm
```
![Hello World][hello_world.gif]
## Barchart
Demonstrates the [`BarChart`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.BarChart.html)
widget. Source: [barchart.rs](./barchart.rs).
## Barchart ([barchart.rs](./barchart.rs)
```shell
cargo run --example=barchart --features=crossterm
@@ -41,10 +14,7 @@ cargo run --example=barchart --features=crossterm
![Barchart][barchart.gif]
## Block
Demonstrates the [`Block`](https://docs.rs/ratatui/latest/ratatui/widgets/block/struct.Block.html)
widget. Source: [block.rs](./block.rs).
## Block ([block.rs](./block.rs))
```shell
cargo run --example=block --features=crossterm
@@ -52,10 +22,7 @@ cargo run --example=block --features=crossterm
![Block][block.gif]
## Calendar
Demonstrates the [`Calendar`](https://docs.rs/ratatui/latest/ratatui/widgets/calendar/index.html)
widget. Source: [calendar.rs](./calendar.rs).
## Calendar ([calendar.rs](./calendar.rs))
```shell
cargo run --example=calendar --features=crossterm widget-calendar
@@ -63,12 +30,7 @@ cargo run --example=calendar --features=crossterm widget-calendar
![Calendar][calendar.gif]
## Canvas
Demonstrates the [`Canvas`](https://docs.rs/ratatui/latest/ratatui/widgets/canvas/index.html) widget
and related shapes in the
[`canvas`](https://docs.rs/ratatui/latest/ratatui/widgets/canvas/index.html) module. Source:
[canvas.rs](./canvas.rs).
## Canvas ([canvas.rs](./canvas.rs))
```shell
cargo run --example=canvas --features=crossterm
@@ -76,10 +38,7 @@ cargo run --example=canvas --features=crossterm
![Canvas][canvas.gif]
## Chart
Demonstrates the [`Chart`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Chart.html) widget.
Source: [chart.rs](./chart.rs).
## Chart ([chart.rs](./chart.rs))
```shell
cargo run --example=chart --features=crossterm
@@ -87,49 +46,21 @@ cargo run --example=chart --features=crossterm
![Chart][chart.gif]
## Colors
Demonstrates the available [`Color`](https://docs.rs/ratatui/latest/ratatui/style/enum.Color.html)
options. These can be used in any style field. Source: [colors.rs](./colors.rs).
```shell
cargo run --example=colors --features=crossterm
```
![Colors][colors.gif]
## Colors (RGB)
Demonstrates the available RGB
[`Color`](https://docs.rs/ratatui/latest/ratatui/style/enum.Color.html) options. These can be used
in any style field. Source: [colors_rgb.rs](./colors_rgb.rs).
```shell
cargo run --example=colors_rgb --features=crossterm
```
![Colors RGB][colors_rgb.gif]
## Custom Widget
Demonstrates how to implement the
[`Widget`](https://docs.rs/ratatui/latest/ratatui/widgets/trait.Widget.html) trait. Source:
[custom_widget.rs](./custom_widget.rs).
## Custom Widget ([custom_widget.rs](./custom_widget.rs))
```shell
cargo run --example=custom_widget --features=crossterm
```
This is not a particularly exciting example visually, but it demonstrates how to implement your own widget.
![Custom Widget][custom_widget.gif]
## Gauge
## Gauge ([gauge.rs](./gauge.rs))
Demonstrates the [`Gauge`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Gauge.html) widget.
Source: [gauge.rs](./gauge.rs).
> [!NOTE] The backgrounds render poorly when we generate this example using VHS. This problem
> doesn't generally happen during normal rendering in a terminal. See
> [vhs#344](https://github.com/charmbracelet/vhs/issues/344) for more details.
Please note: the background renders poorly when we generate this example using VHS.
This problem doesn't generally happen during normal rendering in a terminal.
See <https://github.com/charmbracelet/vhs/issues/344> for more details
```shell
cargo run --example=gauge --features=crossterm
@@ -137,11 +68,18 @@ cargo run --example=gauge --features=crossterm
![Gauge][gauge.gif]
## Inline
## Hello World ([hello_world.rs](./hello_world.rs))
Demonstrates the
[`Inline`](https://docs.rs/ratatui/latest/ratatui/terminal/enum.Viewport.html#variant.Inline)
Viewport mode for ratatui apps. Source: [inline.rs](./inline.rs).
```shell
cargo run --example=hello_world --features=crossterm
```
This is a pretty boring example, but it contains some good comments of documentation on some of the
standard approaches to writing tui apps.
![Hello World][hello_world.gif]
## Inline ([inline.rs](./inline.rs))
```shell
cargo run --example=inline --features=crossterm
@@ -149,10 +87,7 @@ cargo run --example=inline --features=crossterm
![Inline][inline.gif]
## Layout
Demonstrates the [`Layout`](https://docs.rs/ratatui/latest/ratatui/layout/struct.Layout.html) and
interaction between each constraint. Source: [layout.rs](./layout.rs).
## Layout ([layout.rs](./layout.rs))
```shell
cargo run --example=layout --features=crossterm
@@ -160,10 +95,7 @@ cargo run --example=layout --features=crossterm
![Layout][layout.gif]
## List
Demonstrates the [`List`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.List.html) widget.
Source: [list.rs](./list.rs).
## List ([list.rs](./list.rs))
```shell
cargo run --example=list --features=crossterm
@@ -171,22 +103,7 @@ cargo run --example=list --features=crossterm
![List][list.gif]
## Modifiers
Demonstrates the style
[`Modifiers`](https://docs.rs/ratatui/latest/ratatui/style/struct.Modifier.html). Source:
[modifiers.rs](./modifiers.rs).
```shell
cargo run --example=modifiers --features=crossterm
```
![Modifiers][modifiers.gif]
## Panic
Demonstrates how to handle panics by ensuring that panic messages are written correctly to the
screen. Source: [panic.rs](./panic.rs).
## Panic ([panic.rs](./panic.rs))
```shell
cargo run --example=panic --features=crossterm
@@ -194,10 +111,7 @@ cargo run --example=panic --features=crossterm
![Panic][panic.gif]
## Paragraph
Demonstrates the [`Paragraph`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Paragraph.html)
widget. Source: [paragraph.rs](./paragraph.rs)
## Paragraph ([paragraph.rs](./paragraph.rs))
```shell
cargo run --example=paragraph --features=crossterm
@@ -205,27 +119,19 @@ cargo run --example=paragraph --features=crossterm
![Paragraph][paragraph.gif]
## Popup
## Popup ([popup.rs](./popup.rs))
Demonstrates how to render a widget over the top of previously rendered widgets using the
[`Clear`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Clear.html) widget. Source:
[popup.rs](./popup.rs).
>
```shell
cargo run --example=popup --features=crossterm
```
> [!NOTE] The background renders poorly after the popup when we generate this example using VHS.
> This problem doesn't generally happen during normal rendering in a terminal. See
> [vhs#344](https://github.com/charmbracelet/vhs/issues/344) for more details.
Please note: the background renders poorly when we generate this example using VHS.
This problem doesn't generally happen during normal rendering in a terminal.
See <https://github.com/charmbracelet/vhs/issues/344> for more details
![Popup][popup.gif]
## Scrollbar
Demonstrates the [`Scrollbar`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Scrollbar.html)
widget. Source: [scrollbar.rs](./scrollbar.rs).
## Scrollbar ([scrollbar.rs](./scrollbar.rs))
```shell
cargo run --example=scrollbar --features=crossterm
@@ -233,14 +139,7 @@ cargo run --example=scrollbar --features=crossterm
![Scrollbar][scrollbar.gif]
## Sparkline
Demonstrates the [`Sparkline`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Sparkline.html)
widget. Source: [sparkline.rs](./sparkline.rs).
> [!NOTE] The background renders poorly in the second sparkline when we generate this example using
> VHS. This problem doesn't generally happen during normal rendering in a terminal. See
> [vhs#344](https://github.com/charmbracelet/vhs/issues/344) for more details.
## Sparkline ([sparkline.rs](./sparkline.rs))
```shell
cargo run --example=sparkline --features=crossterm
@@ -248,10 +147,7 @@ cargo run --example=sparkline --features=crossterm
![Sparkline][sparkline.gif]
## Table
Demonstrates the [`Table`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Table.html) widget.
Source: [table.rs](./table.rs).
## Table ([table.rs](./table.rs))
```shell
cargo run --example=table --features=crossterm
@@ -259,10 +155,7 @@ cargo run --example=table --features=crossterm
![Table][table.gif]
## Tabs
Demonstrates the [`Tabs`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Tabs.html) widget.
Source: [tabs.rs](./tabs.rs).
## Tabs ([tabs.rs](./tabs.rs))
```shell
cargo run --example=tabs --features=crossterm
@@ -270,12 +163,7 @@ cargo run --example=tabs --features=crossterm
![Tabs][tabs.gif]
## User Input
Demonstrates one approach to accepting user input. Source [user_input.rs](./user_input.rs).
> [!NOTE] Consider using [`tui-textarea`](https://crates.io/crates/tui-textarea) or
> [`tui-input`](https://crates.io/crates/tui-input) crates for more functional text entry UIs.
## User Input ([user_input.rs](./user_input.rs))
```shell
cargo run --example=user_input --features=crossterm
@@ -289,29 +177,33 @@ These are generated with `vhs publish examples/xxx.gif`
To update these examples in bulk:
```shell
examples/generate.bash
# build to ensure that running the examples doesn't have to wait so long
cargo build --examples --features=crossterm,all-widgets
for i in examples/*.tape
do
echo -n "[${i:s:examples/:::s:.tape:.gif:}]: "
vhs $i --publish --quiet
# may need to adjust this depending on if you see rate limiting from VHS
sleep 1
done
```
-->
[barchart.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/barchart.gif?raw=true
[block.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/block.gif?raw=true
[calendar.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/calendar.gif?raw=true
[canvas.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/canvas.gif?raw=true
[chart.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/chart.gif?raw=true
[colors.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/colors.gif?raw=true
[colors_rgb.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/colors_rgb.gif?raw=true
[custom_widget.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/custom_widget.gif?raw=true
[demo.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/demo.gif?raw=true
[gauge.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/gauge.gif?raw=true
[hello_world.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/hello_world.gif?raw=true
[inline.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/inline.gif?raw=true
[layout.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/layout.gif?raw=true
[list.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/list.gif?raw=true
[modifiers.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/modifiers.gif?raw=true
[panic.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/panic.gif?raw=true
[paragraph.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/paragraph.gif?raw=true
[popup.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/popup.gif?raw=true
[scrollbar.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/scrollbar.gif?raw=true
[sparkline.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/sparkline.gif?raw=true
[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
[barchart.gif]: https://vhs.charm.sh/vhs-6ioxdeRBVkVpyXcjIEVaJU.gif
[block.gif]: https://vhs.charm.sh/vhs-1sEo9vVkHRwFtu95MOXrTj.gif
[calendar.gif]: https://vhs.charm.sh/vhs-1dBcpMSSP80WkBgm4lBhNo.gif
[canvas.gif]: https://vhs.charm.sh/vhs-4zeWEPF6bLEFSHuJrvaHlN.gif
[chart.gif]: https://vhs.charm.sh/vhs-zRzsE2AwRixQhcWMTAeF1.gif
[custom_widget.gif]: https://vhs.charm.sh/vhs-32mW1TpkrovTcm79QXmBSu.gif
[gauge.gif]: https://vhs.charm.sh/vhs-2rvSeP5r4lRkGTzNCKpm9a.gif
[hello_world.gif]: https://vhs.charm.sh/vhs-3CKUwxFuQi8oKQMS5zkPfQ.gif
[inline.gif]: https://vhs.charm.sh/vhs-miRl1mosKFoJV7LjjvF4T.gif
[layout.gif]: https://vhs.charm.sh/vhs-5R8O3LQGQ5pQVWwlPVrdbQ.gif
[list.gif]: https://vhs.charm.sh/vhs-4goo9reeUM9r0nYb54R7SP.gif
[panic.gif]: https://vhs.charm.sh/vhs-HrvKCHV4yeN69fb1EadTH.gif
[paragraph.gif]: https://vhs.charm.sh/vhs-2qIPDi79DUmtmeNDEeHVEF.gif
[popup.gif]: https://vhs.charm.sh/vhs-2QnC682AUeNYNXcjNlKTyp.gif
[scrollbar.gif]: https://vhs.charm.sh/vhs-2p13MMFreW7Gwt1xIonIWu.gif
[sparkline.gif]: https://vhs.charm.sh/vhs-4t59Vxw5Za33Rtvt9QrftA.gif
[table.gif]: https://vhs.charm.sh/vhs-6IrGHgT385DqA6xnwGF9oD.gif
[tabs.gif]: https://vhs.charm.sh/vhs-61WkbfhyDk0kbkjncErdHT.gif
[user_input.gif]: https://vhs.charm.sh/vhs-4fxUgkpEWcVyBRXuyYKODY.gif

View File

@@ -62,7 +62,7 @@ impl<'a> App<'a> {
},
Company {
label: "Comp.B",
revenue: [1500, 2500, 3000, 500],
revenue: [1500, 2500, 3000, 4100],
bar_style: Style::default().fg(Color::Yellow),
},
Company {
@@ -139,7 +139,15 @@ fn run_app<B: Backend>(
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)].as_ref())
.margin(2)
.constraints(
[
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
]
.as_ref(),
)
.split(f.size());
let barchart = BarChart::default()
@@ -150,17 +158,16 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
.value_style(Style::default().fg(Color::Black).bg(Color::Yellow));
f.render_widget(barchart, chunks[0]);
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(chunks[1]);
draw_bar_with_group_labels(f, app, chunks[0]);
draw_horizontal_bars(f, app, chunks[1]);
draw_bar_with_group_labels(f, app, chunks[1], false);
draw_bar_with_group_labels(f, app, chunks[2], true);
}
fn create_groups<'a>(app: &'a App, combine_values_and_labels: bool) -> Vec<BarGroup<'a>> {
app.months
fn draw_bar_with_group_labels<B>(f: &mut Frame<B>, app: &App, area: Rect, bar_labels: bool)
where
B: Backend,
{
let groups: Vec<BarGroup> = app
.months
.iter()
.enumerate()
.map(|(i, &month)| {
@@ -175,34 +182,17 @@ fn create_groups<'a>(app: &'a App, combine_values_and_labels: bool) -> Vec<BarGr
Style::default()
.bg(c.bar_style.fg.unwrap())
.fg(Color::Black),
);
if combine_values_and_labels {
bar = bar.text_value(format!(
"{} ({:.1} M)",
c.label,
(c.revenue[i] as f64) / 1000.
));
} else {
bar = bar
.text_value(format!("{:.1}", (c.revenue[i] as f64) / 1000.))
.label(c.label.into());
)
.text_value(format!("{:.1}", (c.revenue[i] as f64) / 1000.));
if bar_labels {
bar = bar.label(c.label.into());
}
bar
})
.collect();
BarGroup::default()
.label(Line::from(month).alignment(Alignment::Center))
.bars(&bars)
BarGroup::default().label(month.into()).bars(&bars)
})
.collect()
}
fn draw_bar_with_group_labels<B>(f: &mut Frame<B>, app: &App, area: Rect)
where
B: Backend,
{
let groups = create_groups(app, false);
.collect();
let mut barchart = BarChart::default()
.block(Block::default().title("Data1").borders(Borders::ALL))
@@ -217,44 +207,11 @@ where
const LEGEND_HEIGHT: u16 = 6;
if area.height >= LEGEND_HEIGHT && area.width >= TOTAL_REVENUE.len() as u16 + 2 {
let legend_width = TOTAL_REVENUE.len() as u16 + 2;
let legend_area = Rect {
height: LEGEND_HEIGHT,
width: legend_width,
width: TOTAL_REVENUE.len() as u16 + 2,
y: area.y,
x: area.right() - legend_width,
};
draw_legend(f, legend_area);
}
}
fn draw_horizontal_bars<B>(f: &mut Frame<B>, app: &App, area: Rect)
where
B: Backend,
{
let groups = create_groups(app, true);
let mut barchart = BarChart::default()
.block(Block::default().title("Data1").borders(Borders::ALL))
.bar_width(1)
.group_gap(1)
.bar_gap(0)
.direction(Direction::Horizontal);
for group in groups {
barchart = barchart.data(group)
}
f.render_widget(barchart, area);
const LEGEND_HEIGHT: u16 = 6;
if area.height >= LEGEND_HEIGHT && area.width >= TOTAL_REVENUE.len() as u16 + 2 {
let legend_width = TOTAL_REVENUE.len() as u16 + 2;
let legend_area = Rect {
height: LEGEND_HEIGHT,
width: legend_width,
y: area.y,
x: area.right() - legend_width,
x: area.x,
};
draw_legend(f, legend_area);
}

View File

@@ -1,7 +1,6 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/barchart.tape`
Output "target/barchart.gif"
Set Theme "OceanicMaterial"
Set Width 1200
Set Height 800
Hide

View File

@@ -1,253 +1,123 @@
use std::{
error::Error,
io::{stdout, Stdout},
ops::ControlFlow,
time::Duration,
};
use std::{error::Error, io, time::Duration};
use crossterm::{
event::{self, Event, KeyCode},
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use itertools::Itertools;
use ratatui::{
prelude::*,
widgets::{
block::{Position, Title},
Block, BorderType, Borders, Padding, Paragraph, Wrap,
},
};
use ratatui::{prelude::*, widgets::*};
// These type aliases are used to make the code more readable by reducing repetition of the generic
// types. They are not necessary for the functionality of the code.
type Frame<'a> = ratatui::Frame<'a, CrosstermBackend<Stdout>>;
type Terminal = ratatui::Terminal<CrosstermBackend<Stdout>>;
type Result<T> = std::result::Result<T, Box<dyn Error>>;
fn main() -> Result<()> {
let mut terminal = setup_terminal()?;
let result = run(&mut terminal);
restore_terminal(terminal)?;
if let Err(err) = result {
eprintln!("{err:?}");
}
Ok(())
}
fn setup_terminal() -> Result<Terminal> {
fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
enable_raw_mode()?;
let mut stdout = stdout();
execute!(stdout, EnterAlternateScreen)?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let terminal = Terminal::new(backend)?;
Ok(terminal)
}
let mut terminal = Terminal::new(backend)?;
fn restore_terminal(mut terminal: Terminal) -> Result<()> {
// create app and run it
let res = run_app(&mut terminal);
// restore terminal
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.clear()?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
}
Ok(())
}
fn run(terminal: &mut Terminal) -> Result<()> {
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
loop {
terminal.draw(ui)?;
if handle_events()?.is_break() {
return Ok(());
}
}
}
fn handle_events() -> Result<ControlFlow<()>> {
if event::poll(Duration::from_millis(100))? {
if let Event::Key(key) = event::read()? {
if let KeyCode::Char('q') = key.code {
return Ok(ControlFlow::Break(()));
if event::poll(Duration::from_millis(250))? {
if let Event::Key(key) = event::read()? {
if let KeyCode::Char('q') = key.code {
return Ok(());
}
}
}
}
Ok(ControlFlow::Continue(()))
}
fn ui(frame: &mut Frame) {
let (title_area, layout) = calculate_layout(frame.size());
render_title(frame, title_area);
let paragraph = placeholder_paragraph();
render_borders(&paragraph, Borders::ALL, frame, layout[0][0]);
render_borders(&paragraph, Borders::NONE, frame, layout[0][1]);
render_borders(&paragraph, Borders::LEFT, frame, layout[1][0]);
render_borders(&paragraph, Borders::RIGHT, frame, layout[1][1]);
render_borders(&paragraph, Borders::TOP, frame, layout[2][0]);
render_borders(&paragraph, Borders::BOTTOM, frame, layout[2][1]);
render_border_type(&paragraph, BorderType::Plain, frame, layout[3][0]);
render_border_type(&paragraph, BorderType::Rounded, frame, layout[3][1]);
render_border_type(&paragraph, BorderType::Double, frame, layout[4][0]);
render_border_type(&paragraph, BorderType::Thick, frame, layout[4][1]);
render_styled_block(&paragraph, frame, layout[5][0]);
render_styled_borders(&paragraph, frame, layout[5][1]);
render_styled_title(&paragraph, frame, layout[6][0]);
render_styled_title_content(&paragraph, frame, layout[6][1]);
render_multiple_titles(&paragraph, frame, layout[7][0]);
render_multiple_title_positions(&paragraph, frame, layout[7][1]);
render_padding(&paragraph, frame, layout[8][0]);
render_nested_blocks(&paragraph, frame, layout[8][1]);
}
/// Calculate the layout of the UI elements.
///
/// Returns a tuple of the title area and the main areas.
fn calculate_layout(area: Rect) -> (Rect, Vec<Vec<Rect>>) {
let layout = Layout::default()
fn ui<B: Backend>(f: &mut Frame<B>) {
// Wrapping block for a group
// Just draw the block and the group on the same area and build the group
let outer = f.size();
let outer_block = Block::default()
.borders(Borders::ALL)
.title(block::Title::from("Main block with round corners").alignment(Alignment::Center))
.border_type(BorderType::Rounded);
let inner = outer_block.inner(outer);
let [top, bottom] = *Layout::default()
.direction(Direction::Vertical)
.constraints(vec![Constraint::Length(1), Constraint::Min(0)])
.split(area);
let title_area = layout[0];
let main_areas = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![Constraint::Max(4); 9])
.split(layout[1])
.iter()
.map(|&area| {
Layout::default()
.direction(Direction::Horizontal)
.constraints(vec![Constraint::Percentage(50), Constraint::Percentage(50)])
.split(area)
.to_vec()
})
.collect_vec();
(title_area, main_areas)
}
.margin(1)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(inner)
else {
return;
};
let [top_left, top_right] = *Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(top)
else {
return;
};
let [bottom_left, bottom_right] = *Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(bottom)
else {
return;
};
fn render_title(frame: &mut Frame, area: Rect) {
frame.render_widget(
Paragraph::new("Block example. Press q to quit")
.dark_gray()
.alignment(Alignment::Center),
area,
let top_left_block = Block::default()
.title("With Green Background")
.borders(Borders::all())
.on_green();
let top_right_block = Block::default()
.title(
block::Title::from("With styled title".white().on_red().bold())
.alignment(Alignment::Right),
)
.borders(Borders::ALL);
let bottom_left_block = Paragraph::new("Text inside padded block").block(
Block::default()
.title("With borders")
.borders(Borders::ALL)
.padding(Padding {
left: 4,
right: 4,
top: 2,
bottom: 2,
}),
);
}
let bottom_right_block = Block::default()
.title("With styled borders and doubled borders")
.border_style(Style::default().fg(Color::Cyan))
.borders(Borders::LEFT | Borders::RIGHT)
.border_type(BorderType::Double)
.padding(Padding::uniform(1));
let bottom_inner_block = Block::default()
.title("Block inside padded block")
.borders(Borders::ALL);
fn placeholder_paragraph() -> Paragraph<'static> {
let text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.";
Paragraph::new(text.dark_gray()).wrap(Wrap { trim: true })
}
fn render_borders(paragraph: &Paragraph, border: Borders, frame: &mut Frame, area: Rect) {
let block = Block::new()
.borders(border)
.title(format!("Borders::{border:#?}", border = border));
frame.render_widget(paragraph.clone().block(block), area);
}
fn render_border_type(
paragraph: &Paragraph,
border_type: BorderType,
frame: &mut Frame,
area: Rect,
) {
let block = Block::new()
.borders(Borders::ALL)
.border_type(border_type)
.title(format!("BorderType::{border_type:#?}"));
frame.render_widget(paragraph.clone().block(block), area);
}
fn render_styled_borders(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
let block = Block::new()
.borders(Borders::ALL)
.border_style(Style::new().blue().on_white().bold().italic())
.title("Styled borders");
frame.render_widget(paragraph.clone().block(block), area);
}
fn render_styled_block(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
let block = Block::new()
.borders(Borders::ALL)
.style(Style::new().blue().on_white().bold().italic())
.title("Styled block");
frame.render_widget(paragraph.clone().block(block), area);
}
// Note: this currently renders incorrectly, see https://github.com/ratatui-org/ratatui/issues/349
fn render_styled_title(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
let block = Block::new()
.borders(Borders::ALL)
.title("Styled title")
.title_style(Style::new().blue().on_white().bold().italic());
frame.render_widget(paragraph.clone().block(block), area);
}
fn render_styled_title_content(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
let title = Line::from(vec![
"Styled ".blue().on_white().bold().italic(),
"title content".red().on_white().bold().italic(),
]);
let block = Block::new().borders(Borders::ALL).title(title);
frame.render_widget(paragraph.clone().block(block), area);
}
fn render_multiple_titles(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
let block = Block::new()
.borders(Borders::ALL)
.title("Multiple".blue().on_white().bold().italic())
.title("Titles".red().on_white().bold().italic());
frame.render_widget(paragraph.clone().block(block), area);
}
fn render_multiple_title_positions(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
let block = Block::new()
.borders(Borders::ALL)
.title(
Title::from("top left")
.position(Position::Top)
.alignment(Alignment::Left),
)
.title(
Title::from("top center")
.position(Position::Top)
.alignment(Alignment::Center),
)
.title(
Title::from("top right")
.position(Position::Top)
.alignment(Alignment::Right),
)
.title(
Title::from("bottom left")
.position(Position::Bottom)
.alignment(Alignment::Left),
)
.title(
Title::from("bottom center")
.position(Position::Bottom)
.alignment(Alignment::Center),
)
.title(
Title::from("bottom right")
.position(Position::Bottom)
.alignment(Alignment::Right),
);
frame.render_widget(paragraph.clone().block(block), area);
}
fn render_padding(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
let block = Block::new()
.borders(Borders::ALL)
.title("Padding")
.padding(Padding::new(5, 10, 1, 2));
frame.render_widget(paragraph.clone().block(block), area);
}
fn render_nested_blocks(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
let outer_block = Block::new().borders(Borders::ALL).title("Outer block");
let inner_block = Block::new().borders(Borders::ALL).title("Inner block");
let inner = outer_block.inner(area);
frame.render_widget(outer_block, area);
frame.render_widget(paragraph.clone().block(inner_block), inner);
f.render_widget(outer_block, outer);
f.render_widget(Clear, top_left);
f.render_widget(top_left_block, top_left);
f.render_widget(top_right_block, top_right);
f.render_widget(bottom_left_block, bottom_left);
let bottom_right_inner = bottom_right_block.inner(bottom_right);
f.render_widget(bottom_right_block, bottom_right);
f.render_widget(bottom_inner_block, bottom_right_inner);
}

View File

@@ -1,12 +1,11 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/block.tape`
Output "target/block.gif"
Set Theme "OceanicMaterial"
Set Width 1200
Set Height 1200
Set Height 800
Hide
Type "cargo run --example=block"
Enter
Sleep 2s
Sleep 1s
Show
Sleep 2s
Sleep 5s

View File

@@ -1,7 +1,6 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/calendar.tape`
Output "target/calendar.gif"
Set Theme "OceanicMaterial"
Set Width 1200
Set Height 800
Hide

View File

@@ -1,7 +1,6 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/canvas.tape`
Output "target/canvas.gif"
Set Theme "OceanicMaterial"
Set Width 1200
Set Height 800
Hide

View File

@@ -1,7 +1,6 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/chart.tape`
Output "target/chart.gif"
Set Theme "OceanicMaterial"
Set Width 1200
Set Height 800
Hide

View File

@@ -1,295 +0,0 @@
/// This example shows all the colors supported by ratatui. It will render a grid of foreground
/// and background colors with their names and indexes.
use std::{
error::Error,
io::{self, Stdout},
result,
time::Duration,
};
use crossterm::{
event::{self, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use itertools::Itertools;
use ratatui::{prelude::*, widgets::*};
type Result<T> = result::Result<T, Box<dyn Error>>;
fn main() -> Result<()> {
let mut terminal = setup_terminal()?;
let res = run_app(&mut terminal);
restore_terminal(terminal)?;
if let Err(err) = res {
eprintln!("{err:?}");
}
Ok(())
}
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
loop {
terminal.draw(ui)?;
if event::poll(Duration::from_millis(250))? {
if let Event::Key(key) = event::read()? {
if let KeyCode::Char('q') = key.code {
return Ok(());
}
}
}
}
}
fn ui<B: Backend>(frame: &mut Frame<B>) {
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![
Constraint::Length(30),
Constraint::Length(17),
Constraint::Length(2),
])
.split(frame.size());
render_named_colors(frame, layout[0]);
render_indexed_colors(frame, layout[1]);
render_indexed_grayscale(frame, layout[2]);
}
const NAMED_COLORS: [Color; 16] = [
Color::Black,
Color::Red,
Color::Green,
Color::Yellow,
Color::Blue,
Color::Magenta,
Color::Cyan,
Color::Gray,
Color::DarkGray,
Color::LightRed,
Color::LightGreen,
Color::LightYellow,
Color::LightBlue,
Color::LightMagenta,
Color::LightCyan,
Color::White,
];
fn render_named_colors<B: Backend>(frame: &mut Frame<B>, area: Rect) {
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![Constraint::Length(3); 10])
.split(area);
render_fg_named_colors(frame, Color::Reset, layout[0]);
render_fg_named_colors(frame, Color::Black, layout[1]);
render_fg_named_colors(frame, Color::DarkGray, layout[2]);
render_fg_named_colors(frame, Color::Gray, layout[3]);
render_fg_named_colors(frame, Color::White, layout[4]);
render_bg_named_colors(frame, Color::Reset, layout[5]);
render_bg_named_colors(frame, Color::Black, layout[6]);
render_bg_named_colors(frame, Color::DarkGray, layout[7]);
render_bg_named_colors(frame, Color::Gray, layout[8]);
render_bg_named_colors(frame, Color::White, layout[9]);
}
fn render_fg_named_colors<B: Backend>(frame: &mut Frame<B>, bg: Color, area: Rect) {
let block = title_block(format!("Foreground colors on {bg} background"));
let inner = block.inner(area);
frame.render_widget(block, area);
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![Constraint::Length(1); 2])
.split(inner)
.iter()
.flat_map(|area| {
Layout::default()
.direction(Direction::Horizontal)
.constraints(vec![Constraint::Ratio(1, 8); 8])
.split(*area)
.to_vec()
})
.collect_vec();
for (i, &fg) in NAMED_COLORS.iter().enumerate() {
let color_name = fg.to_string();
let paragraph = Paragraph::new(color_name).fg(fg).bg(bg);
frame.render_widget(paragraph, layout[i]);
}
}
fn render_bg_named_colors<B: Backend>(frame: &mut Frame<B>, fg: Color, area: Rect) {
let block = title_block(format!("Background colors with {fg} foreground"));
let inner = block.inner(area);
frame.render_widget(block, area);
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![Constraint::Length(1); 2])
.split(inner)
.iter()
.flat_map(|area| {
Layout::default()
.direction(Direction::Horizontal)
.constraints(vec![Constraint::Ratio(1, 8); 8])
.split(*area)
.to_vec()
})
.collect_vec();
for (i, &bg) in NAMED_COLORS.iter().enumerate() {
let color_name = bg.to_string();
let paragraph = Paragraph::new(color_name).fg(fg).bg(bg);
frame.render_widget(paragraph, layout[i]);
}
}
fn render_indexed_colors<B: Backend>(frame: &mut Frame<B>, area: Rect) {
let block = title_block("Indexed colors".into());
let inner = block.inner(area);
frame.render_widget(block, area);
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![
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::default()
.direction(Direction::Horizontal)
.constraints(vec![Constraint::Length(5); 16])
.split(layout[0]);
for i in 0..16 {
let color = Color::Indexed(i);
let color_index = format!("{i:0>2}");
let bg = if i < 1 { Color::DarkGray } else { Color::Black };
let paragraph = Paragraph::new(Line::from(vec![
color_index.fg(color).bg(bg),
"██".bg(color).fg(color),
]));
frame.render_widget(paragraph, color_layout[i as usize]);
}
// 16 17 18 19 20 21 52 53 54 55 56 57 88 89 90 91 92 93
// 22 23 24 25 26 27 58 59 60 61 62 63 94 95 96 97 98 99
// 28 29 30 31 32 33 64 65 66 67 68 69 100 101 102 103 104 105
// 34 35 36 37 38 39 70 71 72 73 74 75 106 107 108 109 110 111
// 40 41 42 43 44 45 76 77 78 79 80 81 112 113 114 115 116 117
// 46 47 48 49 50 51 82 83 84 85 86 87 118 119 120 121 122 123
//
// 124 125 126 127 128 129 160 161 162 163 164 165 196 197 198 199 200 201
// 130 131 132 133 134 135 166 167 168 169 170 171 202 203 204 205 206 207
// 136 137 138 139 140 141 172 173 174 175 176 177 208 209 210 211 212 213
// 142 143 144 145 146 147 178 179 180 181 182 183 214 215 216 217 218 219
// 148 149 150 151 152 153 184 185 186 187 188 189 220 221 222 223 224 225
// 154 155 156 157 158 159 190 191 192 193 194 195 226 227 228 229 230 231
// the above looks complex but it's so the colors are grouped into blocks that display nicely
let index_layout = [layout[2], layout[4]]
.iter()
// two rows of 3 columns
.flat_map(|area| {
Layout::default()
.direction(Direction::Horizontal)
.constraints(vec![Constraint::Length(27); 3])
.split(*area)
.to_vec()
})
// each with 6 rows
.flat_map(|area| {
Layout::default()
.direction(Direction::Vertical)
.constraints(vec![Constraint::Length(1); 6])
.split(area)
.to_vec()
})
// each with 6 columns
.flat_map(|area| {
Layout::default()
.direction(Direction::Horizontal)
.constraints(vec![Constraint::Min(4); 6])
.split(area)
.to_vec()
})
.collect_vec();
for i in 16..=231 {
let color = Color::Indexed(i);
let color_index = format!("{i:0>3}");
let paragraph = Paragraph::new(Line::from(vec![
color_index.fg(color).bg(Color::Reset),
".".bg(color).fg(color),
// There's a bug in VHS that seems to bleed backgrounds into the next
// character. This is a workaround to make the bug less obvious.
"███".reversed(),
]));
frame.render_widget(paragraph, index_layout[i as usize - 16]);
}
}
fn title_block(title: String) -> Block<'static> {
Block::default()
.borders(Borders::TOP)
.border_style(Style::new().dark_gray())
.title(title)
.title_alignment(Alignment::Center)
.title_style(Style::new().reset())
}
fn render_indexed_grayscale<B: Backend>(frame: &mut Frame<B>, area: Rect) {
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![
Constraint::Length(1), // 232 - 243
Constraint::Length(1), // 244 - 255
])
.split(area)
.iter()
.flat_map(|area| {
Layout::default()
.direction(Direction::Horizontal)
.constraints(vec![Constraint::Length(6); 12])
.split(*area)
.to_vec()
})
.collect_vec();
for i in 232..=255 {
let color = Color::Indexed(i);
let color_index = format!("{i:0>3}");
// make the dark colors easier to read
let bg = if i < 244 { Color::Gray } else { Color::Black };
let paragraph = Paragraph::new(Line::from(vec![
color_index.fg(color).bg(bg),
"██".bg(color).fg(color),
// There's a bug in VHS that seems to bleed backgrounds into the next
// character. This is a workaround to make the bug less obvious.
"███████".reversed(),
]));
frame.render_widget(paragraph, layout[i as usize - 232]);
}
}
fn setup_terminal() -> Result<Terminal<CrosstermBackend<Stdout>>> {
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
Ok(terminal)
}
fn restore_terminal(mut terminal: Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
terminal.show_cursor()?;
Ok(())
}

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/colors.tape`
Output "target/colors.gif"
# The OceanicMaterial theme is a good choice for this example (Obsidian is almost as good) because:
# - Black is dark and distinct from the default background
# - White is light and distinct from the default foreground
# - Normal and bright colors are distinct
# - Black and DarkGray are distinct
# - White and Gray are distinct
Set Theme "OceanicMaterial"
Set Width 1200
Set Height 1410
Hide
Type "cargo run --example=colors --features=crossterm"
Enter
Sleep 2s
Show
Sleep 1s

View File

@@ -1,160 +0,0 @@
/// This example shows the full range of RGB colors that can be displayed in the terminal.
///
/// Requires a terminal that supports 24-bit color (true color) and unicode.
use std::{
error::Error,
io::{stdout, Stdout},
rc::Rc,
time::Duration,
};
use crossterm::{
event::{self, Event, KeyCode, KeyEventKind},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
};
use itertools::Itertools;
use ratatui::{prelude::*, widgets::*};
type Result<T> = std::result::Result<T, Box<dyn Error>>;
fn main() -> Result<()> {
install_panic_hook();
App::new()?.run()
}
struct App {
terminal: Terminal<CrosstermBackend<Stdout>>,
should_quit: bool,
}
impl App {
pub fn new() -> Result<Self> {
Ok(Self {
terminal: Terminal::new(CrosstermBackend::new(stdout()))?,
should_quit: false,
})
}
pub fn run(mut self) -> Result<()> {
init_terminal()?;
self.terminal.clear()?;
while !self.should_quit {
self.draw()?;
self.handle_events()?;
}
restore_terminal()?;
Ok(())
}
fn draw(&mut self) -> Result<()> {
self.terminal.draw(|frame| {
frame.render_widget(RgbColors, frame.size());
})?;
Ok(())
}
fn handle_events(&mut self) -> Result<()> {
if event::poll(Duration::from_millis(100))? {
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
self.should_quit = true;
};
}
}
Ok(())
}
}
impl Drop for App {
fn drop(&mut self) {
let _ = restore_terminal();
}
}
struct RgbColors;
impl Widget for RgbColors {
fn render(self, area: Rect, buf: &mut Buffer) {
let layout = Self::layout(area);
let rgb_colors = Self::create_rgb_color_grid(area.width, area.height * 2);
Self::render_title(layout[0], buf);
Self::render_colors(layout[1], buf, rgb_colors);
}
}
impl RgbColors {
fn layout(area: Rect) -> Rc<[Rect]> {
Layout::default()
.direction(Direction::Vertical)
.constraints(vec![Constraint::Length(1), Constraint::Min(0)])
.split(area)
}
fn render_title(area: Rect, buf: &mut Buffer) {
Paragraph::new("colors_rgb example. Press q to quit")
.dark_gray()
.alignment(Alignment::Center)
.render(area, buf);
}
/// Render a colored grid of half block characters (`"▀"`) each with a different RGB color.
fn render_colors(area: Rect, buf: &mut Buffer, rgb_colors: Vec<Vec<Color>>) {
for (x, column) in (area.left()..area.right()).zip(rgb_colors.iter()) {
for (y, (fg, bg)) in (area.top()..area.bottom()).zip(column.iter().tuples()) {
let cell = buf.get_mut(x, y);
cell.fg = *fg;
cell.bg = *bg;
cell.symbol = "".into();
}
}
}
/// Generate a smooth grid of colors
///
/// Red ranges from 0 to 255 across the x axis. Green ranges from 0 to 255 across the y axis.
/// Blue repeats every 32 pixels in both directions, but flipped every 16 pixels so that it
/// doesn't transition sharply from light to dark.
///
/// The result stored in a 2d vector of colors with the x axis as the first dimension, and the
/// y axis the second dimension.
fn create_rgb_color_grid(width: u16, height: u16) -> Vec<Vec<Color>> {
let mut result = vec![];
for x in 0..width {
let mut column = vec![];
for y in 0..height {
// flip both axes every 16 pixels. E.g. [0, 1, ... 15, 15, ... 1, 0]
let yy = if (y % 32) < 16 { y % 32 } else { 31 - y % 32 };
let xx = if (x % 32) < 16 { x % 32 } else { 31 - x % 32 };
let r = (256 * x / width) as u8;
let g = (256 * y / height) as u8;
let b = (yy * 16 + xx) as u8;
column.push(Color::Rgb(r, g, b))
}
result.push(column);
}
result
}
}
/// Install a panic hook that restores the terminal before panicking.
fn install_panic_hook() {
better_panic::install();
let prev_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
let _ = restore_terminal();
prev_hook(info);
}));
}
fn init_terminal() -> Result<()> {
enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
Ok(())
}
fn restore_terminal() -> Result<()> {
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
Ok(())
}

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/colors_rgb.tape`
Output "target/colors_rgb.gif"
# The OceanicMaterial theme is a good choice for this example (Obsidian is almost as good) because:
# - Black is dark and distinct from the default background
# - White is light and distinct from the default foreground
# - Normal and bright colors are distinct
# - Black and DarkGray are distinct
# - White and Gray are distinct
Set Theme "OceanicMaterial"
Set Width 1200
Set Height 1410
Hide
Type "cargo run --example=colors_rgb --features=crossterm"
Enter
Sleep 2s
Show
Sleep 1s

View File

@@ -1,7 +1,6 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/custom_widget.tape`
Output "target/custom_widget.gif"
Set Theme "OceanicMaterial"
Set Width 1200
Set Height 200
Hide

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`
Output "target/demo.gif"
Set Theme "OceanicMaterial"
Set Width 1200
Set Height 1200
Set PlaybackSpeed 0.5
Hide
Type "cargo run --example demo"
Enter
Sleep 2s
Show
Sleep 1s
Down@1s 12
Right
Sleep 4s
Right
Sleep 4s

View File

@@ -1,7 +1,6 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/gauge.tape`
Output "target/gauge.gif"
Set Theme "OceanicMaterial"
Set Width 1200
Set Height 600
Hide

View File

@@ -1,38 +0,0 @@
#!/bin/bash
# This script is used to generate the images for the examples README
# It requires the following tools:
# - cargo: https://doc.rust-lang.org/cargo/getting-started/installation.html
# - gh: https://github.com/cli/cli
# - git: https://git-scm.com/
# - vhs: https://github.com/charmbracelet/vhs
# Exit on error. Append "|| true" if you expect an error.
set -o errexit
# Exit on error inside any functions or subshells.
set -o errtrace
# Do not allow use of undefined vars. Use ${VAR:-} to use an undefined VAR
set -o nounset
# Catch the error in case mysqldump fails (but gzip succeeds) in `mysqldump |gzip`
set -o pipefail
# Turn on traces, useful while debugging but commented out by default
# set -o xtrace
# ensure that running each example doesn't have to wait for the build
cargo build --examples --features=crossterm,all-widgets
for tape in examples/*.tape
do
gif=${tape/examples\/}
gif=${gif/.tape/.gif}
vhs $tape --quiet
# this can be pasted into the examples README.md
echo "[${gif}]: https://github.com/ratatui-org/ratatui/blob/images/examples/${gif}?raw=true"
done
git switch images
git pull --rebase upstream images
cp target/*.gif examples/
git add examples/*.gif
git commit -m 'docs(examples): update images'
gh pr create
git sw main

View File

@@ -1,7 +1,6 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/hello_world.tape`
Output "target/hello_world.gif"
Set Theme "OceanicMaterial"
Set Width 1200
Set Height 200
Hide

View File

@@ -1,7 +1,6 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/inline.tape`
Output "target/inline.gif"
Set Theme "OceanicMaterial"
Set Width 1200
Set Height 600
Type "cargo run --example=inline --features=crossterm"

View File

@@ -5,8 +5,7 @@ use crossterm::{
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use itertools::Itertools;
use ratatui::{layout::Constraint::*, prelude::*, widgets::*};
use ratatui::{prelude::*, widgets::*};
fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
@@ -48,176 +47,50 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
}
fn ui<B: Backend>(frame: &mut Frame<B>) {
let main_layout = Layout::default()
let [top, mid, bottom] = *Layout::default()
.direction(Direction::Vertical)
.constraints(vec![
Length(4), // text
Length(50), // examples
Min(0), // fills remaining space
])
.split(frame.size());
// title
.constraints(
[
Constraint::Length(4),
Constraint::Percentage(50),
Constraint::Min(4),
]
.as_ref(),
)
.split(frame.size())
else {
return;
};
let [left, right] = *Layout::default()
.direction(Direction::Horizontal)
.horizontal_margin(5)
.vertical_margin(2)
.constraints([Constraint::Ratio(2, 5), Constraint::Ratio(3, 5)].as_ref())
.split(mid)
else {
return;
};
frame.render_widget(
Paragraph::new(vec![
Line::from("Horizontal Layout Example. Press q to quit".dark_gray())
.alignment(Alignment::Center),
Line::from("Each line has 2 constraints, plus Min(0) to fill the remaining space."),
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"),
]),
main_layout[0],
Paragraph::new("Constraint::Length(4)").block(Block::default().borders(Borders::ALL)),
top,
);
let example_rows = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![
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::default()
.direction(Direction::Horizontal)
.constraints(vec![
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();
frame.render_widget(
Paragraph::new("Constraint::Percentage(50)").block(Block::default().borders(Borders::ALL)),
mid,
);
// the examples are a cartesian product of the following constraints
// e.g. Len/Len, Len/Min, Len/Max, Len/Perc, Len/Ratio, Min/Len, Min/Min, ...
let examples = [
(
"Len",
vec![
Length(0),
Length(2),
Length(3),
Length(6),
Length(10),
Length(15),
],
),
(
"Min",
vec![Min(0), Min(2), Min(3), Min(6), Min(10), Min(15)],
),
(
"Max",
vec![Max(0), Max(2), Max(3), Max(6), Max(10), Max(15)],
),
(
"Perc",
vec![
Percentage(0),
Percentage(25),
Percentage(50),
Percentage(75),
Percentage(100),
Percentage(150),
],
),
(
"Ratio",
vec![
Ratio(0, 4),
Ratio(1, 4),
Ratio(2, 4),
Ratio(3, 4),
Ratio(4, 4),
Ratio(6, 4),
],
),
];
for (i, (a, b)) in examples
.iter()
.cartesian_product(examples.iter())
.enumerate()
{
let (name_a, examples_a) = a;
let (name_b, examples_b) = b;
let constraints = examples_a
.iter()
.copied()
.zip(examples_b.iter().copied())
.collect_vec();
render_example_combination(
frame,
example_areas[i],
&format!("{name_a}/{name_b}"),
constraints,
);
}
}
/// Renders a single example box
fn render_example_combination<B: Backend>(
frame: &mut Frame<B>,
area: Rect,
title: &str,
constraints: Vec<(Constraint, Constraint)>,
) {
let block = Block::default()
.title(title.gray())
.style(Style::reset())
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::DarkGray));
let inner = block.inner(area);
frame.render_widget(block, area);
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)]);
}
// This is to make it easy to visually see the alignment of the examples
// with the constraints.
frame.render_widget(Paragraph::new("123456789012"), layout[6]);
}
/// Renders a single example line
fn render_single_example<B: Backend>(
frame: &mut Frame<B>,
area: Rect,
constraints: Vec<Constraint>,
) {
let red = Paragraph::new(constraint_label(constraints[0])).on_red();
let blue = Paragraph::new(constraint_label(constraints[1])).on_blue();
let green = Paragraph::new("·".repeat(12)).on_green();
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 {
match constraint {
Length(n) => format!("{n}"),
Min(n) => format!("{n}"),
Max(n) => format!("{n}"),
Percentage(n) => format!("{n}"),
Ratio(a, b) => format!("{a}:{b}"),
}
frame.render_widget(
Paragraph::new("Constraint::Ratio(2, 5)\nhorizontal_margin(5)\nvertical_margin(2)")
.block(Block::default().borders(Borders::ALL)),
left,
);
frame.render_widget(
Paragraph::new("Constraint::Ratio(3, 5)").block(Block::default().borders(Borders::ALL)),
right,
);
frame.render_widget(
Paragraph::new("Constraint::Min(4)").block(Block::default().borders(Borders::ALL)),
bottom,
);
}

View File

@@ -1,12 +1,11 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/layout.tape`
Output "target/layout.gif"
Set Theme "OceanicMaterial"
Set Width 1200
Set Height 1410
Set Height 600
Hide
Type "cargo run --example=layout --features=crossterm"
Enter
Sleep 1s
Show
Sleep 2s
Sleep 5s

View File

@@ -1,7 +1,6 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/list.tape`
Output "target/list.gif"
Set Theme "OceanicMaterial"
Set Width 1200
Set Height 600
Hide

View File

@@ -1,116 +0,0 @@
/// This example is useful for testing how your terminal emulator handles different modifiers.
/// It will render a grid of combinations of foreground and background colors with all
/// modifiers applied to them.
use std::{
error::Error,
io::{self, Stdout},
iter::once,
result,
time::Duration,
};
use crossterm::{
event::{self, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use itertools::Itertools;
use ratatui::{prelude::*, widgets::*};
type Result<T> = result::Result<T, Box<dyn Error>>;
fn main() -> Result<()> {
let mut terminal = setup_terminal()?;
let res = run_app(&mut terminal);
restore_terminal(terminal)?;
if let Err(err) = res {
eprintln!("{err:?}");
}
Ok(())
}
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
loop {
terminal.draw(ui)?;
if event::poll(Duration::from_millis(250))? {
if let Event::Key(key) = event::read()? {
if let KeyCode::Char('q') = key.code {
return Ok(());
}
}
}
}
}
fn ui<B: Backend>(frame: &mut Frame<B>) {
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![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)),
layout[0],
);
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![Constraint::Length(1); 50])
.split(layout[1])
.iter()
.flat_map(|area| {
Layout::default()
.direction(Direction::Horizontal)
.constraints(vec![Constraint::Percentage(20); 5])
.split(*area)
.to_vec()
})
.collect_vec();
let colors = [
Color::Black,
Color::DarkGray,
Color::Gray,
Color::White,
Color::Red,
];
let all_modifiers = once(Modifier::empty())
.chain(Modifier::all().iter())
.collect_vec();
let mut index = 0;
for bg in colors.iter() {
for fg in colors.iter() {
for modifier in &all_modifiers {
let modifier_name = format!("{modifier:11?}");
let padding = (" ").repeat(12 - modifier_name.len());
let paragraph = Paragraph::new(Line::from(vec![
modifier_name.fg(*fg).bg(*bg).add_modifier(*modifier),
padding.fg(*fg).bg(*bg).add_modifier(*modifier),
// This is a hack to work around a bug in VHS which is used for rendering the
// examples to gifs. The bug is that the background color of a paragraph seems
// to bleed into the next character.
".".black().on_black(),
]));
frame.render_widget(paragraph, layout[index]);
index += 1;
}
}
}
}
fn setup_terminal() -> Result<Terminal<CrosstermBackend<Stdout>>> {
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
Ok(terminal)
}
fn restore_terminal(mut terminal: Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
terminal.show_cursor()?;
Ok(())
}

View File

@@ -1,12 +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/modifiers.tape`
Output "target/modifiers.gif"
Set Theme "OceanicMaterial"
Set Width 1200
Set Height 1460
Hide
Type "cargo run --example=modifiers --features=crossterm"
Enter
Sleep 2s
Show
Sleep 1s

View File

@@ -1,7 +1,6 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/panic.tape`
Output "target/panic.gif"
Set Theme "OceanicMaterial"
Set Width 1200
Set Height 600
Type "cargo run --example=panic --features=crossterm"

View File

@@ -1,7 +1,6 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/paragraph.tape`
Output "target/paragraph.gif"
Set Theme "OceanicMaterial"
Set Width 1200
Set Height 1800
Hide

View File

@@ -1,7 +1,6 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/popup.tape`
Output "target/popup.gif"
Set Theme "OceanicMaterial"
Set Width 1200
Set Height 600
Hide

View File

@@ -66,23 +66,27 @@ fn run_app<B: Backend>(
KeyCode::Char('q') => return Ok(()),
KeyCode::Char('j') => {
app.vertical_scroll = app.vertical_scroll.saturating_add(1);
app.vertical_scroll_state =
app.vertical_scroll_state.position(app.vertical_scroll);
app.vertical_scroll_state = app
.vertical_scroll_state
.position(app.vertical_scroll as u16);
}
KeyCode::Char('k') => {
app.vertical_scroll = app.vertical_scroll.saturating_sub(1);
app.vertical_scroll_state =
app.vertical_scroll_state.position(app.vertical_scroll);
app.vertical_scroll_state = app
.vertical_scroll_state
.position(app.vertical_scroll as u16);
}
KeyCode::Char('h') => {
app.horizontal_scroll = app.horizontal_scroll.saturating_sub(1);
app.horizontal_scroll_state =
app.horizontal_scroll_state.position(app.horizontal_scroll);
app.horizontal_scroll_state = app
.horizontal_scroll_state
.position(app.horizontal_scroll as u16);
}
KeyCode::Char('l') => {
app.horizontal_scroll = app.horizontal_scroll.saturating_add(1);
app.horizontal_scroll_state =
app.horizontal_scroll_state.position(app.horizontal_scroll);
app.horizontal_scroll_state = app
.horizontal_scroll_state
.position(app.horizontal_scroll as u16);
}
_ => {}
}
@@ -124,7 +128,7 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
Line::from("This is a line ".red()),
Line::from("This is a line".on_dark_gray()),
Line::from("This is a longer line".crossed_out()),
Line::from(long_line.clone()),
Line::from(long_line.reset()),
Line::from("This is a line".reset()),
Line::from(vec![
Span::raw("Masked text: "),
@@ -137,7 +141,7 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
Line::from("This is a line ".red()),
Line::from("This is a line".on_dark_gray()),
Line::from("This is a longer line".crossed_out()),
Line::from(long_line.clone()),
Line::from(long_line.reset()),
Line::from("This is a line".reset()),
Line::from(vec![
Span::raw("Masked text: "),
@@ -147,8 +151,10 @@ fn ui<B: Backend>(f: &mut Frame<B>, 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());
app.vertical_scroll_state = app.vertical_scroll_state.content_length(text.len() as u16);
app.horizontal_scroll_state = app
.horizontal_scroll_state
.content_length(long_line.len() as u16);
let create_block = |title| {
Block::default()
@@ -182,7 +188,7 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
let paragraph = Paragraph::new(text.clone())
.gray()
.block(create_block(
"Vertical scrollbar without arrows, without track symbol and mirrored",
"Vertical scrollbar without arrows and mirrored",
))
.scroll((app.vertical_scroll as u16, 0));
f.render_widget(paragraph, chunks[2]);
@@ -191,7 +197,6 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
.orientation(ScrollbarOrientation::VerticalLeft)
.symbols(scrollbar::VERTICAL)
.begin_symbol(None)
.track_symbol(None)
.end_symbol(None),
chunks[2].inner(&Margin {
vertical: 1,
@@ -230,7 +235,7 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
Scrollbar::default()
.orientation(ScrollbarOrientation::HorizontalBottom)
.thumb_symbol("")
.track_symbol(Some("")),
.track_symbol(""),
chunks[4].inner(&Margin {
vertical: 0,
horizontal: 1,

View File

@@ -1,7 +1,6 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/scrollbar.tape`
Output "target/scrollbar.gif"
Set Theme "OceanicMaterial"
Set Width 1200
Set Height 1200
Hide

View File

@@ -133,6 +133,7 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
[
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(7),
Constraint::Min(0),
]
.as_ref(),

View File

@@ -1,7 +1,6 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/sparkline.tape`
Output "target/sparkline.gif"
Set Theme "OceanicMaterial"
Set Width 1200
Set Height 600
Hide

View File

@@ -1,7 +1,6 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/table.tape`
Output "target/table.gif"
Set Theme "OceanicMaterial"
Set Width 1200
Set Height 600
Hide

View File

@@ -1,7 +1,6 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/tabs.tape`
Output "target/tabs.gif"
Set Theme "OceanicMaterial"
Set Width 1200
Set Height 300
Hide

View File

@@ -1,7 +1,6 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/user_input.tape`
Output "target/user_input.gif"
Set Theme "OceanicMaterial"
Set Width 1200
Set Height 600
Hide

View File

@@ -1,2 +0,0 @@
[toolchain]
channel = "stable"

View File

@@ -1,336 +0,0 @@
#![warn(missing_docs)]
//! This module provides the backend implementations for different terminal libraries.
//!
//! It defines the [`Backend`] trait which is used to abstract over the specific terminal library
//! being used.
//!
//! Supported terminal backends:
//! - [Crossterm]: enable the `crossterm` feature (enabled by default) and use [`CrosstermBackend`]
//! - [Termion]: enable the `termion` feature and use [`TermionBackend`]
//! - [Termwiz]: enable the `termwiz` feature and use [`TermwizBackend`]
//!
//! Additionally, a [`TestBackend`] is provided for testing purposes.
//!
//! See the [Backend Comparison] section of the [Ratatui Book] for more details on the different
//! backends.
//!
//! Each backend supports a number of features, such as [raw mode](#raw-mode), [alternate
//! screen](#alternate-screen), and [mouse capture](#mouse-capture). These features are generally
//! not enabled by default, and must be enabled by the application before they can be used. See the
//! documentation for each backend for more details.
//!
//! Note: most applications should use the [`Terminal`] struct instead of directly calling methods
//! on the backend.
//!
//! # Example
//!
//! ```rust,no_run
//! use std::io::stdout;
//! use ratatui::prelude::*;
//!
//! let backend = CrosstermBackend::new(stdout());
//! let mut terminal = Terminal::new(backend)?;
//! terminal.clear()?;
//! terminal.draw(|frame| {
//! // -- snip --
//! })?;
//! # std::io::Result::Ok(())
//! ```
//!
//! See the the [examples] directory for more examples.
//!
//! # Raw Mode
//!
//! Raw mode is a mode where the terminal does not perform any processing or handling of the input
//! and output. This means that features such as echoing input characters, line buffering, and
//! special character processing (e.g., CTRL-C for SIGINT) are disabled. This is useful for
//! applications that want to have complete control over the terminal input and output, processing
//! each keystroke themselves.
//!
//! For example, in raw mode, the terminal will not perform line buffering on the input, so the
//! application will receive each key press as it is typed, instead of waiting for the user to
//! press enter. This makes it suitable for real-time applications like text editors,
//! terminal-based games, and more.
//!
//! Each backend handles raw mode differently, so the behavior may vary depending on the backend
//! being used. Be sure to consult the backend's specific documentation for exact details on how it
//! implements raw mode.
//! # Alternate Screen
//!
//! The alternate screen is a separate buffer that some terminals provide, distinct from the main
//! screen. When activated, the terminal will display the alternate screen, hiding the current
//! content of the main screen. Applications can write to this screen as if it were the regular
//! terminal display, but when the application exits, the terminal will switch back to the main
//! screen, and the contents of the alternate screen will be cleared. This is useful for
//! applications like text editors or terminal games that want to use the full terminal window
//! without disrupting the command line or other terminal content.
//!
//! This creates a seamless transition between the application and the regular terminal session, as
//! the content displayed before launching the application will reappear after the application
//! exits.
//!
//! Note that not all terminal emulators support the alternate screen, and even those that do may
//! handle it differently. As a result, the behavior may vary depending on the backend being used.
//! Always consult the specific backend's documentation to understand how it implements the
//! alternate screen.
//!
//! # Mouse Capture
//!
//! Mouse capture is a mode where the terminal captures mouse events such as clicks, scrolls, and
//! movement, and sends them to the application as special sequences or events. This enables the
//! application to handle and respond to mouse actions, providing a more interactive and graphical
//! user experience within the terminal. It's particularly useful for applications like
//! terminal-based games, text editors, or other programs that require more direct interaction from
//! the user.
//!
//! Each backend handles mouse capture differently, with variations in the types of events that can
//! be captured and how they are represented. As such, the behavior may vary depending on the
//! backend being used, and developers should consult the specific backend's documentation to
//! understand how it implements mouse capture.
//!
//! [`TermionBackend`]: termion/struct.TermionBackend.html
//! [`Terminal`]: crate::terminal::Terminal
//! [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
//! [Backend Comparison]:
//! https://ratatui-org.github.io/ratatui-book/concepts/backends/comparison.html
//! [Ratatui Book]: https://ratatui-org.github.io/ratatui-book
use std::io;
use strum::{Display, EnumString};
use crate::{buffer::Cell, layout::Size, prelude::Rect};
#[cfg(feature = "termion")]
mod termion;
#[cfg(feature = "termion")]
pub use self::termion::TermionBackend;
#[cfg(feature = "crossterm")]
mod crossterm;
#[cfg(feature = "crossterm")]
pub use self::crossterm::CrosstermBackend;
#[cfg(feature = "termwiz")]
mod termwiz;
#[cfg(feature = "termwiz")]
pub use self::termwiz::TermwizBackend;
mod test;
pub use self::test::TestBackend;
/// Enum representing the different types of clearing operations that can be performed
/// on the terminal screen.
#[derive(Debug, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
pub enum ClearType {
/// Clear the entire screen.
All,
/// Clear everything after the cursor.
AfterCursor,
/// Clear everything before the cursor.
BeforeCursor,
/// Clear the current line.
CurrentLine,
/// Clear everything from the cursor until the next newline.
UntilNewLine,
}
/// The window size in characters (columns / rows) as well as pixels.
pub struct WindowSize {
/// Size of the window in characters (columns / rows).
pub columns_rows: Size,
/// Size of the window in pixels.
///
/// The `pixels` fields may not be implemented by all terminals and return `0,0`. See
/// <https://man7.org/linux/man-pages/man4/tty_ioctl.4.html> under section "Get and set window
/// size" / TIOCGWINSZ where the fields are commented as "unused".
pub pixels: Size,
}
/// The `Backend` trait provides an abstraction over different terminal libraries. It defines the
/// methods required to draw content, manipulate the cursor, and clear the terminal screen.
///
/// Most applications should not need to interact with the `Backend` trait directly as the
/// [`Terminal`] struct provides a higher level interface for interacting with the terminal.
///
/// [`Terminal`]: crate::terminal::Terminal
pub trait Backend {
/// Draw the given content to the terminal screen.
///
/// The content is provided as an iterator over `(u16, u16, &Cell)` tuples, where the first two
/// elements represent the x and y coordinates, and the third element is a reference to the
/// [`Cell`] to be drawn.
fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
where
I: Iterator<Item = (u16, u16, &'a Cell)>;
/// Insert `n` line breaks to the terminal screen.
///
/// This method is optional and may not be implemented by all backends.
fn append_lines(&mut self, _n: u16) -> io::Result<()> {
Ok(())
}
/// Hide the cursor on the terminal screen.
///
///
/// See also [`show_cursor`].
/// # Example
///
/// ```rust
/// # use ratatui::backend::{Backend, TestBackend};
/// # let mut backend = TestBackend::new(80, 25);
/// backend.hide_cursor()?;
/// // do something with hidden cursor
/// backend.show_cursor()?;
/// # std::io::Result::Ok(())
/// ```
///
/// [`show_cursor`]: Backend::show_cursor
fn hide_cursor(&mut self) -> io::Result<()>;
/// Show the cursor on the terminal screen.
///
/// See [`hide_cursor`] for an example.
///
/// [`hide_cursor`]: Backend::hide_cursor
fn show_cursor(&mut self) -> io::Result<()>;
/// Get the current cursor position on the terminal screen.
///
/// The returned tuple contains the x and y coordinates of the cursor. The origin
/// (0, 0) is at the top left corner of the screen.
///
/// See [`set_cursor`] for an example.
///
/// [`set_cursor`]: Backend::set_cursor
fn get_cursor(&mut self) -> io::Result<(u16, u16)>;
/// Set the cursor position on the terminal screen to the given x and y coordinates.
///
/// The origin (0, 0) is at the top left corner of the screen.
///
/// # Example
///
/// ```rust
/// # use ratatui::backend::{Backend, TestBackend};
/// # let mut backend = TestBackend::new(80, 25);
/// backend.set_cursor(10, 20)?;
/// assert_eq!(backend.get_cursor()?, (10, 20));
/// # std::io::Result::Ok(())
/// ```
///
/// [`get_cursor`]: Backend::get_cursor
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()>;
/// Clears the whole terminal scree
///
/// # Example
///
/// ```rust,no_run
/// # use ratatui::backend::{Backend, TestBackend};
/// # let mut backend = TestBackend::new(80, 25);
/// backend.clear()?;
/// # std::io::Result::Ok(())
/// ```
fn clear(&mut self) -> io::Result<()>;
/// Clears a specific region of the terminal specified by the [`ClearType`] parameter
///
/// This method is optional and may not be implemented by all backends. The default
/// implementation calls [`clear`] if the `clear_type` is [`ClearType::All`] and returns an
/// error otherwise.
///
/// # Example
///
/// ```rust,no_run
/// # use ratatui::{prelude::*, backend::{TestBackend, ClearType}};
/// # let mut backend = TestBackend::new(80, 25);
/// backend.clear_region(ClearType::All)?;
/// # std::io::Result::Ok(())
/// ```
///
/// # Errors
///
/// This method will return an error if the terminal screen could not be cleared. It will also
/// return an error if the `clear_type` is not supported by the backend.
///
/// [`clear`]: Backend::clear
fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
match clear_type {
ClearType::All => self.clear(),
ClearType::AfterCursor
| ClearType::BeforeCursor
| ClearType::CurrentLine
| ClearType::UntilNewLine => Err(io::Error::new(
io::ErrorKind::Other,
format!("clear_type [{clear_type:?}] not supported with this backend"),
)),
}
}
/// Get the size of the terminal screen in columns/rows as a [`Rect`].
///
/// The returned [`Rect`] contains the width and height of the terminal screen.
///
/// # Example
///
/// ```rust,no_run
/// # use ratatui::{prelude::*, backend::TestBackend};
/// let backend = TestBackend::new(80, 25);
/// assert_eq!(backend.size()?, Rect::new(0, 0, 80, 25));
/// # std::io::Result::Ok(())
/// ```
fn size(&self) -> io::Result<Rect>;
/// Get the size of the terminal screen in columns/rows and pixels as a [`WindowSize`].
///
/// The reason for this not returning only the pixel size, given the redundancy with the
/// `size()` method, is that the underlying backends most likely get both values with one
/// syscall, and the user is also most likely to need columns and rows along with pixel size.
fn window_size(&mut self) -> io::Result<WindowSize>;
/// Flush any buffered content to the terminal screen.
fn flush(&mut self) -> io::Result<()>;
}
#[cfg(test)]
mod tests {
use strum::ParseError;
use super::*;
#[test]
fn clear_type_tostring() {
assert_eq!(ClearType::All.to_string(), "All");
assert_eq!(ClearType::AfterCursor.to_string(), "AfterCursor");
assert_eq!(ClearType::BeforeCursor.to_string(), "BeforeCursor");
assert_eq!(ClearType::CurrentLine.to_string(), "CurrentLine");
assert_eq!(ClearType::UntilNewLine.to_string(), "UntilNewLine");
}
#[test]
fn clear_type_from_str() {
assert_eq!("All".parse::<ClearType>(), Ok(ClearType::All));
assert_eq!(
"AfterCursor".parse::<ClearType>(),
Ok(ClearType::AfterCursor)
);
assert_eq!(
"BeforeCursor".parse::<ClearType>(),
Ok(ClearType::BeforeCursor)
);
assert_eq!(
"CurrentLine".parse::<ClearType>(),
Ok(ClearType::CurrentLine)
);
assert_eq!(
"UntilNewLine".parse::<ClearType>(),
Ok(ClearType::UntilNewLine)
);
assert_eq!("".parse::<ClearType>(), Err(ParseError::VariantNotFound));
}
}

View File

@@ -1,7 +1,10 @@
//! This module provides the [`CrosstermBackend`] implementation for the [`Backend`] trait. It uses
//! the [Crossterm] crate to interact with the terminal.
//! This module provides the `CrosstermBackend` implementation for the `Backend` trait.
//! It uses the `crossterm` crate to interact with the terminal.
//!
//! [Crossterm]: https://crates.io/crates/crossterm
//!
//! [`Backend`]: trait.Backend.html
//! [`CrosstermBackend`]: struct.CrosstermBackend.html
use std::io::{self, Write};
use crossterm::{
@@ -15,87 +18,42 @@ use crossterm::{
};
use crate::{
backend::{Backend, ClearType, WindowSize},
backend::{Backend, ClearType},
buffer::Cell,
layout::Size,
prelude::Rect,
layout::Rect,
style::{Color, Modifier},
};
/// A [`Backend`] implementation that uses [Crossterm] to render to the terminal.
/// A backend implementation using the `crossterm` crate.
///
/// The `CrosstermBackend` struct is a wrapper around a writer implementing [`Write`], which is
/// used to send commands to the terminal. It provides methods for drawing content, manipulating
/// the cursor, and clearing the terminal screen.
///
/// Most applications should not call the methods on `CrosstermBackend` directly, but will instead
/// use the [`Terminal`] struct, which provides a more ergonomic interface.
///
/// Usually applications will enable raw mode and switch to alternate screen mode after creating
/// a `CrosstermBackend`. This is done by calling [`crossterm::terminal::enable_raw_mode`] and
/// [`crossterm::terminal::EnterAlternateScreen`] (and the corresponding disable/leave functions
/// when the application exits). This is not done automatically by the backend because it is
/// possible that the application may want to use the terminal for other purposes (like showing
/// help text) before entering alternate screen mode.
/// The `CrosstermBackend` struct is a wrapper around a type implementing `Write`, which
/// is used to send commands to the terminal. It provides methods for drawing content,
/// manipulating the cursor, and clearing the terminal screen.
///
/// # Example
///
/// ```rust,no_run
/// use std::io::{stdout, stderr};
/// use crossterm::{
/// terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
/// ExecutableCommand,
/// };
/// use ratatui::prelude::*;
/// ```rust
/// use ratatui::backend::{Backend, CrosstermBackend};
///
/// let mut backend = CrosstermBackend::new(stdout());
/// // or
/// let backend = CrosstermBackend::new(stderr());
/// let mut terminal = Terminal::new(backend)?;
///
/// enable_raw_mode()?;
/// stdout().execute(EnterAlternateScreen)?;
///
/// terminal.clear()?;
/// terminal.draw(|frame| {
/// // -- snip --
/// })?;
///
/// stdout().execute(LeaveAlternateScreen)?;
/// disable_raw_mode()?;
///
/// # std::io::Result::Ok(())
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let buffer = std::io::stdout();
/// let mut backend = CrosstermBackend::new(buffer);
/// backend.clear()?;
/// # Ok(())
/// # }
/// ```
///
/// 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#examples
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
#[derive(Debug, Default, Clone)]
pub struct CrosstermBackend<W: Write> {
/// The writer used to send commands to the terminal.
writer: W,
buffer: W,
}
impl<W> CrosstermBackend<W>
where
W: Write,
{
/// Creates a new `CrosstermBackend` with the given writer.
///
/// # Example
///
/// ```rust,no_run
/// # use std::io::stdout;
/// # use ratatui::prelude::*;
/// let backend = CrosstermBackend::new(stdout());
/// ```
pub fn new(writer: W) -> CrosstermBackend<W> {
CrosstermBackend { writer }
/// Creates a new `CrosstermBackend` with the given buffer.
pub fn new(buffer: W) -> CrosstermBackend<W> {
CrosstermBackend { buffer }
}
}
@@ -105,12 +63,12 @@ where
{
/// Writes a buffer of bytes to the underlying buffer.
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.writer.write(buf)
self.buffer.write(buf)
}
/// Flushes the underlying buffer.
fn flush(&mut self) -> io::Result<()> {
self.writer.flush()
self.buffer.flush()
}
}
@@ -130,7 +88,7 @@ where
for (x, y, cell) in content {
// Move the cursor if the previous location was not (x - 1, y)
if !matches!(last_pos, Some(p) if x == p.0 + 1 && y == p.1) {
queue!(self.writer, MoveTo(x, y))?;
map_error(queue!(self.buffer, MoveTo(x, y)))?;
}
last_pos = Some((x, y));
if cell.modifier != modifier {
@@ -138,43 +96,43 @@ where
from: modifier,
to: cell.modifier,
};
diff.queue(&mut self.writer)?;
diff.queue(&mut self.buffer)?;
modifier = cell.modifier;
}
if cell.fg != fg {
let color = CColor::from(cell.fg);
queue!(self.writer, SetForegroundColor(color))?;
map_error(queue!(self.buffer, SetForegroundColor(color)))?;
fg = cell.fg;
}
if cell.bg != bg {
let color = CColor::from(cell.bg);
queue!(self.writer, SetBackgroundColor(color))?;
map_error(queue!(self.buffer, SetBackgroundColor(color)))?;
bg = cell.bg;
}
if cell.underline_color != underline_color {
let color = CColor::from(cell.underline_color);
queue!(self.writer, SetUnderlineColor(color))?;
map_error(queue!(self.buffer, SetUnderlineColor(color)))?;
underline_color = cell.underline_color;
}
queue!(self.writer, Print(&cell.symbol))?;
map_error(queue!(self.buffer, Print(&cell.symbol)))?;
}
queue!(
self.writer,
map_error(queue!(
self.buffer,
SetForegroundColor(CColor::Reset),
SetBackgroundColor(CColor::Reset),
SetUnderlineColor(CColor::Reset),
SetAttribute(CAttribute::Reset)
)
))
}
fn hide_cursor(&mut self) -> io::Result<()> {
execute!(self.writer, Hide)
map_error(execute!(self.buffer, Hide))
}
fn show_cursor(&mut self) -> io::Result<()> {
execute!(self.writer, Show)
map_error(execute!(self.buffer, Show))
}
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
@@ -183,7 +141,7 @@ where
}
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
execute!(self.writer, MoveTo(x, y))
map_error(execute!(self.buffer, MoveTo(x, y)))
}
fn clear(&mut self) -> io::Result<()> {
@@ -191,8 +149,8 @@ where
}
fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
execute!(
self.writer,
map_error(execute!(
self.buffer,
Clear(match clear_type {
ClearType::All => crossterm::terminal::ClearType::All,
ClearType::AfterCursor => crossterm::terminal::ClearType::FromCursorDown,
@@ -200,42 +158,32 @@ where
ClearType::CurrentLine => crossterm::terminal::ClearType::CurrentLine,
ClearType::UntilNewLine => crossterm::terminal::ClearType::UntilNewLine,
})
)
))
}
fn append_lines(&mut self, n: u16) -> io::Result<()> {
for _ in 0..n {
queue!(self.writer, Print("\n"))?;
map_error(queue!(self.buffer, Print("\n")))?;
}
self.writer.flush()
self.buffer.flush()
}
fn size(&self) -> io::Result<Rect> {
let (width, height) = terminal::size()?;
let (width, height) =
terminal::size().map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
Ok(Rect::new(0, 0, width, height))
}
fn window_size(&mut self) -> Result<WindowSize, io::Error> {
let crossterm::terminal::WindowSize {
columns,
rows,
width,
height,
} = terminal::window_size()?;
Ok(WindowSize {
columns_rows: Size {
width: columns,
height: rows,
},
pixels: Size { width, height },
})
}
fn flush(&mut self) -> io::Result<()> {
self.writer.flush()
self.buffer.flush()
}
}
fn map_error(error: crossterm::Result<()>) -> io::Result<()> {
error.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))
}
impl From<Color> for CColor {
fn from(color: Color) -> Self {
match color {
@@ -265,7 +213,7 @@ impl From<Color> for CColor {
/// The `ModifierDiff` struct is used to calculate the difference between two `Modifier`
/// values. This is useful when updating the terminal display, as it allows for more
/// efficient updates by only sending the necessary changes.
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
#[derive(Debug, Default, Clone, Copy)]
struct ModifierDiff {
pub from: Modifier,
pub to: Modifier,
@@ -279,54 +227,54 @@ impl ModifierDiff {
//use crossterm::Attribute;
let removed = self.from - self.to;
if removed.contains(Modifier::REVERSED) {
queue!(w, SetAttribute(CAttribute::NoReverse))?;
map_error(queue!(w, SetAttribute(CAttribute::NoReverse)))?;
}
if removed.contains(Modifier::BOLD) {
queue!(w, SetAttribute(CAttribute::NormalIntensity))?;
map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?;
if self.to.contains(Modifier::DIM) {
queue!(w, SetAttribute(CAttribute::Dim))?;
map_error(queue!(w, SetAttribute(CAttribute::Dim)))?;
}
}
if removed.contains(Modifier::ITALIC) {
queue!(w, SetAttribute(CAttribute::NoItalic))?;
map_error(queue!(w, SetAttribute(CAttribute::NoItalic)))?;
}
if removed.contains(Modifier::UNDERLINED) {
queue!(w, SetAttribute(CAttribute::NoUnderline))?;
map_error(queue!(w, SetAttribute(CAttribute::NoUnderline)))?;
}
if removed.contains(Modifier::DIM) {
queue!(w, SetAttribute(CAttribute::NormalIntensity))?;
map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?;
}
if removed.contains(Modifier::CROSSED_OUT) {
queue!(w, SetAttribute(CAttribute::NotCrossedOut))?;
map_error(queue!(w, SetAttribute(CAttribute::NotCrossedOut)))?;
}
if removed.contains(Modifier::SLOW_BLINK) || removed.contains(Modifier::RAPID_BLINK) {
queue!(w, SetAttribute(CAttribute::NoBlink))?;
map_error(queue!(w, SetAttribute(CAttribute::NoBlink)))?;
}
let added = self.to - self.from;
if added.contains(Modifier::REVERSED) {
queue!(w, SetAttribute(CAttribute::Reverse))?;
map_error(queue!(w, SetAttribute(CAttribute::Reverse)))?;
}
if added.contains(Modifier::BOLD) {
queue!(w, SetAttribute(CAttribute::Bold))?;
map_error(queue!(w, SetAttribute(CAttribute::Bold)))?;
}
if added.contains(Modifier::ITALIC) {
queue!(w, SetAttribute(CAttribute::Italic))?;
map_error(queue!(w, SetAttribute(CAttribute::Italic)))?;
}
if added.contains(Modifier::UNDERLINED) {
queue!(w, SetAttribute(CAttribute::Underlined))?;
map_error(queue!(w, SetAttribute(CAttribute::Underlined)))?;
}
if added.contains(Modifier::DIM) {
queue!(w, SetAttribute(CAttribute::Dim))?;
map_error(queue!(w, SetAttribute(CAttribute::Dim)))?;
}
if added.contains(Modifier::CROSSED_OUT) {
queue!(w, SetAttribute(CAttribute::CrossedOut))?;
map_error(queue!(w, SetAttribute(CAttribute::CrossedOut)))?;
}
if added.contains(Modifier::SLOW_BLINK) {
queue!(w, SetAttribute(CAttribute::SlowBlink))?;
map_error(queue!(w, SetAttribute(CAttribute::SlowBlink)))?;
}
if added.contains(Modifier::RAPID_BLINK) {
queue!(w, SetAttribute(CAttribute::RapidBlink))?;
map_error(queue!(w, SetAttribute(CAttribute::RapidBlink)))?;
}
Ok(())

117
src/backend/mod.rs Normal file
View File

@@ -0,0 +1,117 @@
//! This module provides the backend implementations for different terminal libraries.
//! It defines the [`Backend`] trait which is used to abstract over the specific
//! terminal library being used.
//!
//! The following terminal libraries are supported:
//! - Crossterm (with the `crossterm` feature)
//! - Termion (with the `termion` feature)
//! - Termwiz (with the `termwiz` feature)
//!
//! Additionally, a [`TestBackend`] is provided for testing purposes.
//!
//! # Example
//!
//! ```rust
//! use ratatui::backend::{Backend, CrosstermBackend};
//!
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
//! let buffer = std::io::stdout();
//! let mut backend = CrosstermBackend::new(buffer);
//! backend.clear()?;
//! # Ok(())
//! # }
//! ```
//!
//! [`Backend`]: trait.Backend.html
//! [`TestBackend`]: struct.TestBackend.html
use std::io;
use crate::{buffer::Cell, layout::Rect};
#[cfg(feature = "termion")]
mod termion;
#[cfg(feature = "termion")]
pub use self::termion::TermionBackend;
#[cfg(feature = "crossterm")]
mod crossterm;
#[cfg(feature = "crossterm")]
pub use self::crossterm::CrosstermBackend;
#[cfg(feature = "termwiz")]
mod termwiz;
#[cfg(feature = "termwiz")]
pub use self::termwiz::TermwizBackend;
mod test;
pub use self::test::TestBackend;
/// Enum representing the different types of clearing operations that can be performed
/// on the terminal screen.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClearType {
All,
AfterCursor,
BeforeCursor,
CurrentLine,
UntilNewLine,
}
/// The `Backend` trait provides an abstraction over different terminal libraries.
/// It defines the methods required to draw content, manipulate the cursor, and
/// clear the terminal screen.
pub trait Backend {
/// Draw the given content to the terminal screen.
///
/// The content is provided as an iterator over `(u16, u16, &Cell)` tuples,
/// where the first two elements represent the x and y coordinates, and the
/// third element is a reference to the [`Cell`] to be drawn.
fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error>
where
I: Iterator<Item = (u16, u16, &'a Cell)>;
/// Insert `n` line breaks to the terminal screen.
///
/// This method is optional and may not be implemented by all backends.
fn append_lines(&mut self, _n: u16) -> io::Result<()> {
Ok(())
}
/// Hide the cursor on the terminal screen.
fn hide_cursor(&mut self) -> Result<(), io::Error>;
/// Show the cursor on the terminal screen.
fn show_cursor(&mut self) -> Result<(), io::Error>;
/// Get the current cursor position on the terminal screen.
fn get_cursor(&mut self) -> Result<(u16, u16), io::Error>;
/// Set the cursor position on the terminal screen to the given x and y coordinates.
fn set_cursor(&mut self, x: u16, y: u16) -> Result<(), io::Error>;
/// Clears the whole terminal screen
fn clear(&mut self) -> Result<(), io::Error>;
/// Clears a specific region of the terminal specified by the [`ClearType`] parameter
///
/// This method is optional and may not be implemented by all backends.
fn clear_region(&mut self, clear_type: ClearType) -> Result<(), io::Error> {
match clear_type {
ClearType::All => self.clear(),
ClearType::AfterCursor
| ClearType::BeforeCursor
| ClearType::CurrentLine
| ClearType::UntilNewLine => Err(io::Error::new(
io::ErrorKind::Other,
format!("clear_type [{clear_type:?}] not supported with this backend"),
)),
}
}
/// Get the size of the terminal screen as a [`Rect`].
fn size(&self) -> Result<Rect, io::Error>;
/// Flush any buffered content to the terminal screen.
fn flush(&mut self) -> Result<(), io::Error>;
}

View File

@@ -1,86 +1,51 @@
//! This module provides the [`TermionBackend`] implementation for the [`Backend`] trait. It uses
//! the [Termion] crate to interact with the terminal.
//! This module provides the `TermionBackend` implementation for the [`Backend`] trait.
//! It uses the Termion crate to interact with the terminal.
//!
//! [`Backend`]: crate::backend::Backend
//! [`TermionBackend`]: crate::backend::TermionBackend
//! [Termion]: https://docs.rs/termion
use std::{
fmt,
io::{self, Write},
};
use crate::{
backend::{Backend, ClearType, WindowSize},
backend::{Backend, ClearType},
buffer::Cell,
prelude::Rect,
layout::Rect,
style::{Color, Modifier},
};
/// A [`Backend`] implementation that uses [Termion] to render to the terminal.
///
/// The `TermionBackend` struct is a wrapper around a writer implementing [`Write`], which is used
/// to send commands to the terminal. It provides methods for drawing content, manipulating the
/// cursor, and clearing the terminal screen.
///
/// Most applications should not call the methods on `TermionBackend` directly, but will instead
/// use the [`Terminal`] struct, which provides a more ergonomic interface.
///
/// Usually applications will enable raw mode and switch to alternate screen mode when starting.
/// This is done by calling [`IntoRawMode::into_raw_mode()`] and
/// [`IntoAlternateScreen::into_alternate_screen()`] on the writer before creating the backend.
/// This is not done automatically by the backend because it is possible that the application may
/// want to use the terminal for other purposes (like showing help text) before entering alternate
/// screen mode. This backend automatically disable raw mode and switches back to the primary
/// screen when the writer is dropped.
/// A backend that uses the Termion library to draw content, manipulate the cursor,
/// and clear the terminal screen.
///
/// # Example
///
/// ```rust,no_run
/// use std::io::{stdout, stderr};
/// use ratatui::prelude::*;
/// use termion::{raw::IntoRawMode, screen::IntoAlternateScreen};
/// ```rust
/// use ratatui::backend::{Backend, TermionBackend};
///
/// let writer = stdout().into_raw_mode()?.into_alternate_screen()?;
/// let mut backend = TermionBackend::new(writer);
/// // or
/// let writer = stderr().into_raw_mode()?.into_alternate_screen()?;
/// let backend = TermionBackend::new(stderr());
/// let mut terminal = Terminal::new(backend)?;
///
/// terminal.clear()?;
/// terminal.draw(|frame| {
/// // -- snip --
/// })?;
/// # std::io::Result::Ok(())
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let stdout = std::io::stdout();
/// let mut backend = TermionBackend::new(stdout);
/// backend.clear()?;
/// # Ok(())
/// # }
/// ```
///
/// [`IntoRawMode::into_raw_mode()`]: termion::raw::IntoRawMode
/// [`IntoAlternateScreen::into_alternate_screen()`]: termion::screen::IntoAlternateScreen
/// [`Terminal`]: crate::terminal::Terminal
/// [Termion]: https://docs.rs/termion
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
#[derive(Debug, Default, Clone)]
pub struct TermionBackend<W>
where
W: Write,
{
writer: W,
stdout: W,
}
impl<W> TermionBackend<W>
where
W: Write,
{
/// Creates a new Termion backend with the given writer.
///
/// # Example
///
/// ```rust,no_run
/// # use std::io::stdout;
/// # use ratatui::prelude::*;
/// let backend = TermionBackend::new(stdout());
/// ```
pub fn new(writer: W) -> TermionBackend<W> {
TermionBackend { writer }
/// Creates a new Termion backend with the given output.
pub fn new(stdout: W) -> TermionBackend<W> {
TermionBackend { stdout }
}
}
@@ -89,11 +54,11 @@ where
W: Write,
{
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.writer.write(buf)
self.stdout.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.writer.flush()
self.stdout.flush()
}
}
@@ -107,39 +72,39 @@ where
fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
match clear_type {
ClearType::All => write!(self.writer, "{}", termion::clear::All)?,
ClearType::AfterCursor => write!(self.writer, "{}", termion::clear::AfterCursor)?,
ClearType::BeforeCursor => write!(self.writer, "{}", termion::clear::BeforeCursor)?,
ClearType::CurrentLine => write!(self.writer, "{}", termion::clear::CurrentLine)?,
ClearType::UntilNewLine => write!(self.writer, "{}", termion::clear::UntilNewline)?,
ClearType::All => write!(self.stdout, "{}", termion::clear::All)?,
ClearType::AfterCursor => write!(self.stdout, "{}", termion::clear::AfterCursor)?,
ClearType::BeforeCursor => write!(self.stdout, "{}", termion::clear::BeforeCursor)?,
ClearType::CurrentLine => write!(self.stdout, "{}", termion::clear::CurrentLine)?,
ClearType::UntilNewLine => write!(self.stdout, "{}", termion::clear::UntilNewline)?,
};
self.writer.flush()
self.stdout.flush()
}
fn append_lines(&mut self, n: u16) -> io::Result<()> {
for _ in 0..n {
writeln!(self.writer)?;
writeln!(self.stdout)?;
}
self.writer.flush()
self.stdout.flush()
}
fn hide_cursor(&mut self) -> io::Result<()> {
write!(self.writer, "{}", termion::cursor::Hide)?;
self.writer.flush()
write!(self.stdout, "{}", termion::cursor::Hide)?;
self.stdout.flush()
}
fn show_cursor(&mut self) -> io::Result<()> {
write!(self.writer, "{}", termion::cursor::Show)?;
self.writer.flush()
write!(self.stdout, "{}", termion::cursor::Show)?;
self.stdout.flush()
}
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
termion::cursor::DetectCursorPos::cursor_pos(&mut self.writer).map(|(x, y)| (x - 1, y - 1))
termion::cursor::DetectCursorPos::cursor_pos(&mut self.stdout).map(|(x, y)| (x - 1, y - 1))
}
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
write!(self.writer, "{}", termion::cursor::Goto(x + 1, y + 1))?;
self.writer.flush()
write!(self.stdout, "{}", termion::cursor::Goto(x + 1, y + 1))?;
self.stdout.flush()
}
fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
@@ -182,7 +147,7 @@ where
string.push_str(&cell.symbol);
}
write!(
self.writer,
self.stdout,
"{string}{}{}{}",
Fg(Color::Reset),
Bg(Color::Reset),
@@ -195,27 +160,20 @@ where
Ok(Rect::new(0, 0, terminal.0, terminal.1))
}
fn window_size(&mut self) -> Result<WindowSize, io::Error> {
Ok(WindowSize {
columns_rows: termion::terminal_size()?.into(),
pixels: termion::terminal_size_pixels()?.into(),
})
}
fn flush(&mut self) -> io::Result<()> {
self.writer.flush()
self.stdout.flush()
}
}
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
#[derive(Debug, Default, Clone, Copy)]
struct Fg(Color);
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
#[derive(Debug, Default, Clone, Copy)]
struct Bg(Color);
/// The `ModifierDiff` struct is used to calculate the difference between two `Modifier`
/// values. This is useful when updating the terminal display, as it allows for more
/// efficient updates by only sending the necessary changes.
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
#[derive(Debug, Default, Clone, Copy)]
struct ModifierDiff {
from: Modifier,
to: Modifier,

View File

@@ -1,9 +1,8 @@
//! This module provides the `TermwizBackend` implementation for the [`Backend`] trait. It uses the
//! [Termwiz] crate to interact with the terminal.
//! This module provides the `TermwizBackend` implementation for the [`Backend`] trait.
//! It uses the `termwiz` crate to interact with the terminal.
//!
//! [`Backend`]: trait.Backend.html
//! [`TermwizBackend`]: crate::backend::TermionBackend
//! [Termwiz]: https://crates.io/crates/termwiz
use std::{error::Error, io};
@@ -12,78 +11,34 @@ use termwiz::{
cell::{AttributeChange, Blink, Intensity, Underline},
color::{AnsiColor, ColorAttribute, SrgbaTuple},
surface::{Change, CursorVisibility, Position},
terminal::{buffered::BufferedTerminal, ScreenSize, SystemTerminal, Terminal},
terminal::{buffered::BufferedTerminal, SystemTerminal, Terminal},
};
use crate::{
backend::{Backend, WindowSize},
backend::Backend,
buffer::Cell,
layout::Size,
prelude::Rect,
layout::Rect,
style::{Color, Modifier},
};
/// A [`Backend`] implementation that uses [Termwiz] to render to the terminal.
///
/// The `TermwizBackend` struct is a wrapper around a [`BufferedTerminal`], which is used to send
/// commands to the terminal. It provides methods for drawing content, manipulating the cursor, and
/// clearing the terminal screen.
///
/// Most applications should not call the methods on `TermwizBackend` directly, but will instead
/// use the [`Terminal`] struct, which provides a more ergonomic interface.
///
/// This backend automatically enables raw mode and switches to the alternate screen when it is
/// created using the [`TermwizBackend::new`] method (and disables raw mode and returns to the main
/// screen when dropped). Use the [`TermwizBackend::with_buffered_terminal`] to create a new
/// instance with a custom [`BufferedTerminal`] if this is not desired.
///
/// Termwiz backend implementation for the [`Backend`] trait.
/// # Example
///
/// ```rust,no_run
/// use ratatui::prelude::*;
/// use ratatui::backend::{Backend, TermwizBackend};
///
/// let backend = TermwizBackend::new()?;
/// let mut terminal = Terminal::new(backend)?;
///
/// terminal.clear()?;
/// terminal.draw(|frame| {
/// // -- snip --
/// })?;
/// # std::result::Result::Ok::<(), Box<dyn std::error::Error>>(())
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let mut backend = TermwizBackend::new()?;
/// backend.clear()?;
/// # Ok(())
/// # }
/// ```
///
/// 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
pub struct TermwizBackend {
buffered_terminal: BufferedTerminal<SystemTerminal>,
}
impl TermwizBackend {
/// Creates a new Termwiz backend instance.
///
/// The backend will automatically enable raw mode and enter the alternate screen.
///
/// # Errors
///
/// Returns an error if unable to do any of the following:
/// - query the terminal capabilities.
/// - enter raw mode.
/// - enter the alternate screen.
/// - create the system or buffered terminal.
///
/// # Example
///
/// ```rust,no_run
/// # use ratatui::prelude::*;
/// let backend = TermwizBackend::new()?;
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
pub fn new() -> Result<TermwizBackend, Box<dyn Error>> {
let mut buffered_terminal =
BufferedTerminal::new(SystemTerminal::new(Capabilities::new_from_env()?)?)?;
@@ -214,31 +169,22 @@ impl Backend for TermwizBackend {
}
fn size(&self) -> Result<Rect, io::Error> {
let (cols, rows) = self.buffered_terminal.dimensions();
Ok(Rect::new(0, 0, u16_max(cols), u16_max(rows)))
}
fn window_size(&mut self) -> Result<WindowSize, io::Error> {
let ScreenSize {
cols,
rows,
xpixel,
ypixel,
} = self
.buffered_terminal
.terminal()
.get_screen_size()
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
Ok(WindowSize {
columns_rows: Size {
width: u16_max(cols),
height: u16_max(rows),
let (term_width, term_height) = self.buffered_terminal.dimensions();
let max = u16::max_value();
Ok(Rect::new(
0,
0,
if term_width > usize::from(max) {
max
} else {
term_width as u16
},
pixels: Size {
width: u16_max(xpixel),
height: u16_max(ypixel),
if term_height > usize::from(max) {
max
} else {
term_height as u16
},
})
))
}
fn flush(&mut self) -> Result<(), io::Error> {
@@ -275,8 +221,3 @@ impl From<Color> for ColorAttribute {
}
}
}
#[inline]
fn u16_max(i: usize) -> u16 {
u16::try_from(i).unwrap_or(u16::MAX)
}

View File

@@ -9,31 +9,26 @@ use std::{
use unicode_width::UnicodeWidthStr;
use crate::{
backend::{Backend, WindowSize},
backend::Backend,
buffer::{Buffer, Cell},
layout::{Rect, Size},
layout::Rect,
};
/// A [`Backend`] implementation used for integration testing that that renders to an in memory
/// buffer.
///
/// Note: that although many of the integration and unit tests in ratatui are written using this
/// backend, it is preferable to write unit tests for widgets directly against the buffer rather
/// than using this backend. This backend is intended for integration tests that test the entire
/// terminal UI.
/// A backend used for the integration tests.
///
/// # Example
///
/// ```rust
/// use ratatui::{backend::TestBackend, prelude::*};
/// use ratatui::{backend::{Backend, TestBackend}, buffer::Buffer};
///
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let mut backend = TestBackend::new(10, 2);
/// backend.clear()?;
/// backend.assert_buffer(&Buffer::with_lines(vec![" "; 2]));
/// # std::io::Result::Ok(())
/// # Ok(())
/// # }
/// ```
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone)]
pub struct TestBackend {
width: u16,
buffer: Buffer,
@@ -98,7 +93,6 @@ impl TestBackend {
/// Asserts that the TestBackend's buffer is equal to the expected buffer.
/// If the buffers are not equal, a panic occurs with a detailed error message
/// showing the differences between the expected and actual buffers.
#[track_caller]
pub fn assert_buffer(&self, expected: &Buffer) {
assert_eq!(expected.area, self.buffer.area);
let diff = expected.diff(&self.buffer);
@@ -183,151 +177,7 @@ impl Backend for TestBackend {
Ok(Rect::new(0, 0, self.width, self.height))
}
fn window_size(&mut self) -> Result<WindowSize, io::Error> {
// Some arbitrary window pixel size, probably doesn't need much testing.
static WINDOW_PIXEL_SIZE: Size = Size {
width: 640,
height: 480,
};
Ok(WindowSize {
columns_rows: (self.width, self.height).into(),
pixels: WINDOW_PIXEL_SIZE,
})
}
fn flush(&mut self) -> Result<(), io::Error> {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new() {
assert_eq!(
TestBackend::new(10, 2),
TestBackend {
width: 10,
height: 2,
buffer: Buffer::with_lines(vec![" "; 2]),
cursor: false,
pos: (0, 0),
}
);
}
#[test]
fn test_buffer_view() {
let buffer = Buffer::with_lines(vec!["aaaa"; 2]);
assert_eq!(buffer_view(&buffer), "\"aaaa\"\n\"aaaa\"\n");
}
#[test]
fn buffer_view_with_overwrites() {
let multi_byte_char = "👨‍👩‍👧‍👦"; // renders 8 wide
let buffer = Buffer::with_lines(vec![multi_byte_char]);
assert_eq!(
buffer_view(&buffer),
format!(
r#""{multi_byte_char}" Hidden by multi-width symbols: [(1, " "), (2, " "), (3, " "), (4, " "), (5, " "), (6, " "), (7, " ")]
"#,
multi_byte_char = multi_byte_char
)
);
}
#[test]
fn buffer() {
let backend = TestBackend::new(10, 2);
assert_eq!(backend.buffer(), &Buffer::with_lines(vec![" "; 2]));
}
#[test]
fn resize() {
let mut backend = TestBackend::new(10, 2);
backend.resize(5, 5);
assert_eq!(backend.buffer(), &Buffer::with_lines(vec![" "; 5]));
}
#[test]
fn assert_buffer() {
let backend = TestBackend::new(10, 2);
let buffer = Buffer::with_lines(vec![" "; 2]);
backend.assert_buffer(&buffer);
}
#[test]
#[should_panic]
fn assert_buffer_panics() {
let backend = TestBackend::new(10, 2);
let buffer = Buffer::with_lines(vec!["aaaaaaaaaa"; 2]);
backend.assert_buffer(&buffer);
}
#[test]
fn display() {
let backend = TestBackend::new(10, 2);
assert_eq!(format!("{}", backend), "\" \"\n\" \"\n");
}
#[test]
fn draw() {
let mut backend = TestBackend::new(10, 2);
let mut cell = Cell::default();
cell.set_symbol("a");
backend.draw([(0, 0, &cell)].into_iter()).unwrap();
backend.draw([(0, 1, &cell)].into_iter()).unwrap();
backend.assert_buffer(&Buffer::with_lines(vec!["a "; 2]));
}
#[test]
fn hide_cursor() {
let mut backend = TestBackend::new(10, 2);
backend.hide_cursor().unwrap();
assert!(!backend.cursor);
}
#[test]
fn show_cursor() {
let mut backend = TestBackend::new(10, 2);
backend.show_cursor().unwrap();
assert!(backend.cursor);
}
#[test]
fn get_cursor() {
let mut backend = TestBackend::new(10, 2);
assert_eq!(backend.get_cursor().unwrap(), (0, 0));
}
#[test]
fn set_cursor() {
let mut backend = TestBackend::new(10, 10);
backend.set_cursor(5, 5).unwrap();
assert_eq!(backend.pos, (5, 5));
}
#[test]
fn clear() {
let mut backend = TestBackend::new(10, 2);
let mut cell = Cell::default();
cell.set_symbol("a");
backend.draw([(0, 0, &cell)].into_iter()).unwrap();
backend.draw([(0, 1, &cell)].into_iter()).unwrap();
backend.clear().unwrap();
backend.assert_buffer(&Buffer::with_lines(vec![" "; 2]));
}
#[test]
fn size() {
let backend = TestBackend::new(10, 2);
assert_eq!(backend.size().unwrap(), Rect::new(0, 0, 10, 2));
}
#[test]
fn flush() {
let mut backend = TestBackend::new(10, 2);
backend.flush().unwrap();
}
}

View File

@@ -14,8 +14,7 @@ use crate::{
};
/// A buffer cell
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Cell {
pub symbol: String,
pub fg: Color,
@@ -23,7 +22,6 @@ pub struct Cell {
#[cfg(feature = "crossterm")]
pub underline_color: Color,
pub modifier: Modifier,
pub skip: bool,
}
impl Cell {
@@ -82,15 +80,6 @@ impl Cell {
.add_modifier(self.modifier)
}
/// Sets the cell to be skipped when copying (diffing) the buffer to the screen.
///
/// This is helpful when it is necessary to prevent the buffer from overwriting a cell that is
/// covered by an image from some terminal graphics protocol (Sixel / iTerm / Kitty ...).
pub fn set_skip(&mut self, skip: bool) -> &mut Cell {
self.skip = skip;
self
}
pub fn reset(&mut self) {
self.symbol.clear();
self.symbol.push(' ');
@@ -101,7 +90,6 @@ impl Cell {
self.underline_color = Color::Reset;
}
self.modifier = Modifier::empty();
self.skip = false;
}
}
@@ -114,7 +102,6 @@ impl Default for Cell {
#[cfg(feature = "crossterm")]
underline_color: Color::Reset,
modifier: Modifier::empty(),
skip: false,
}
}
}
@@ -129,7 +116,9 @@ impl Default for Cell {
/// # Examples:
///
/// ```
/// use ratatui::{prelude::*, buffer::Cell};
/// use ratatui::buffer::{Buffer, Cell};
/// use ratatui::layout::Rect;
/// use ratatui::style::{Color, Style, Modifier};
///
/// let mut buf = Buffer::empty(Rect{x: 0, y: 0, width: 10, height: 5});
/// buf.get_mut(0, 2).set_symbol("x");
@@ -141,14 +130,12 @@ impl Default for Cell {
/// bg: Color::White,
/// #[cfg(feature = "crossterm")]
/// underline_color: Color::Reset,
/// modifier: Modifier::empty(),
/// skip: false
/// modifier: Modifier::empty()
/// });
/// buf.get_mut(5, 0).set_char('x');
/// assert_eq!(buf.get(5, 0).symbol, "x");
/// ```
#[derive(Default, Clone, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Default, Clone, Eq, PartialEq)]
pub struct Buffer {
/// The area represented by this buffer
pub area: Rect,
@@ -175,16 +162,24 @@ impl Buffer {
}
/// Returns a Buffer containing the given lines
pub fn with_lines<'a, S>(lines: Vec<S>) -> Buffer
pub fn with_lines<S>(lines: Vec<S>) -> Buffer
where
S: Into<Line<'a>>,
S: AsRef<str>,
{
let lines = lines.into_iter().map(Into::into).collect::<Vec<_>>();
let height = lines.len() as u16;
let width = lines.iter().map(Line::width).max().unwrap_or_default() as u16;
let mut buffer = Buffer::empty(Rect::new(0, 0, width, height));
let width = lines
.iter()
.map(|i| i.as_ref().width() as u16)
.max()
.unwrap_or_default();
let mut buffer = Buffer::empty(Rect {
x: 0,
y: 0,
width,
height,
});
for (y, line) in lines.iter().enumerate() {
buffer.set_line(0, y as u16, line, width);
buffer.set_string(0, y as u16, line, Style::default());
}
buffer
}
@@ -218,7 +213,8 @@ impl Buffer {
/// # Examples
///
/// ```
/// # use ratatui::prelude::*;
/// # use ratatui::buffer::Buffer;
/// # use ratatui::layout::Rect;
/// let rect = Rect::new(200, 100, 10, 10);
/// let buffer = Buffer::empty(rect);
/// // Global coordinates to the top corner of this buffer's area
@@ -230,7 +226,8 @@ impl Buffer {
/// Panics when given an coordinate that is outside of this Buffer's area.
///
/// ```should_panic
/// # use ratatui::prelude::*;
/// # use ratatui::buffer::Buffer;
/// # use ratatui::layout::Rect;
/// let rect = Rect::new(200, 100, 10, 10);
/// let buffer = Buffer::empty(rect);
/// // Top coordinate is outside of the buffer in global coordinate space, as the Buffer's area
@@ -256,7 +253,8 @@ impl Buffer {
/// # Examples
///
/// ```
/// # use ratatui::prelude::*;
/// # use ratatui::buffer::Buffer;
/// # use ratatui::layout::Rect;
/// let rect = Rect::new(200, 100, 10, 10);
/// let buffer = Buffer::empty(rect);
/// assert_eq!(buffer.pos_of(0), (200, 100));
@@ -268,7 +266,8 @@ impl Buffer {
/// Panics when given an index that is outside the Buffer's content.
///
/// ```should_panic
/// # use ratatui::prelude::*;
/// # use ratatui::buffer::Buffer;
/// # use ratatui::layout::Rect;
/// let rect = Rect::new(0, 0, 10, 10); // 100 cells in total
/// let buffer = Buffer::empty(rect);
/// // Index 100 is the 101th cell, which lies outside of the area of this Buffer.
@@ -487,10 +486,10 @@ impl Buffer {
// Cells invalidated by drawing/replacing preceding multi-width characters:
let mut invalidated: usize = 0;
// Cells from the current buffer to skip due to preceding multi-width characters taking
// their place (the skipped cells should be blank anyway), or due to per-cell-skipping:
// their place (the skipped cells should be blank anyway):
let mut to_skip: usize = 0;
for (i, (current, previous)) in next_buffer.iter().zip(previous_buffer.iter()).enumerate() {
if !current.skip && (current != previous || invalidated > 0) && to_skip == 0 {
if (current != previous || invalidated > 0) && to_skip == 0 {
let (x, y) = self.pos_of(i);
updates.push((x, y, &next_buffer[i]));
}
@@ -915,18 +914,6 @@ mod tests {
);
}
#[test]
fn buffer_diffing_skip() {
let prev = Buffer::with_lines(vec!["123"]);
let mut next = Buffer::with_lines(vec!["456"]);
for i in 1..3 {
next.content[i].set_skip(true);
}
let diff = prev.diff(&next);
assert_eq!(diff, vec![(0, 0, &cell("4"))],);
}
#[test]
fn buffer_merge() {
let mut one = Buffer::filled(
@@ -1008,63 +995,4 @@ mod tests {
};
assert_buffer_eq!(one, merged);
}
#[test]
fn buffer_merge_skip() {
let mut one = Buffer::filled(
Rect {
x: 0,
y: 0,
width: 2,
height: 2,
},
Cell::default().set_symbol("1"),
);
let two = Buffer::filled(
Rect {
x: 0,
y: 1,
width: 2,
height: 2,
},
Cell::default().set_symbol("2").set_skip(true),
);
one.merge(&two);
let skipped: Vec<bool> = one.content().iter().map(|c| c.skip).collect();
assert_eq!(skipped, vec![false, false, true, true, true, true]);
}
#[test]
fn buffer_merge_skip2() {
let mut one = Buffer::filled(
Rect {
x: 0,
y: 0,
width: 2,
height: 2,
},
Cell::default().set_symbol("1").set_skip(true),
);
let two = Buffer::filled(
Rect {
x: 0,
y: 1,
width: 2,
height: 2,
},
Cell::default().set_symbol("2"),
);
one.merge(&two);
let skipped: Vec<bool> = one.content().iter().map(|c| c.skip).collect();
assert_eq!(skipped, vec![true, true, false, false, false, false]);
}
#[test]
fn with_lines_accepts_into_lines() {
use crate::style::Stylize;
let mut buf = Buffer::empty(Rect::new(0, 0, 3, 2));
buf.set_string(0, 0, "foo", Style::new().red());
buf.set_string(0, 1, "bar", Style::new().blue());
assert_eq!(buf, Buffer::with_lines(vec!["foo".red(), "bar".blue()]));
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,9 @@
#![forbid(unsafe_code)]
//! [ratatui](https://github.com/ratatui-org/ratatui) is a library that is all about cooking up terminal user
//! interfaces (TUIs).
//! [ratatui](https://github.com/ratatui-org/ratatui) is a library used to build rich
//! terminal users interfaces and dashboards.
//!
//! ![Demo](https://vhs.charm.sh/vhs-tF0QbuPbtHgUeG0sTVgFr.gif)
//! ![](https://raw.githubusercontent.com/ratatui-org/ratatui/master/assets/demo.gif)
//!
//! # Get started
//!
@@ -12,8 +12,8 @@
//! Add the following to your `Cargo.toml`:
//! ```toml
//! [dependencies]
//! crossterm = "0.27"
//! ratatui = "0.23"
//! crossterm = "0.26"
//! ratatui = "0.20"
//! ```
//!
//! The crate is using the `crossterm` backend by default that works on most platforms. But if for
@@ -22,12 +22,20 @@
//!
//! ```toml
//! [dependencies]
//! termion = "2.0.1"
//! ratatui = { version = "0.23", default-features = false, features = ['termion'] }
//! termion = "1.5"
//! ratatui = { version = "0.20", default-features = false, features = ['termion'] }
//! ```
//!
//! The same logic applies for all other available backends.
//!
//! ### Features
//!
//! Widgets which add dependencies are gated behind feature flags to prevent unused transitive
//! dependencies. The available features are:
//!
//! * `widget-calendar` - enables [`widgets::calendar`] and adds a dependency on the [time
//! crate](https://crates.io/crates/time).
//!
//! ## Creating a `Terminal`
//!
//! Every application using `ratatui` should start by instantiating a `Terminal`. It is a light
@@ -36,7 +44,7 @@
//!
//! ```rust,no_run
//! use std::io;
//! use ratatui::prelude::*;
//! use ratatui::{backend::CrosstermBackend, Terminal};
//!
//! fn main() -> Result<(), io::Error> {
//! let stdout = io::stdout();
@@ -51,7 +59,7 @@
//!
//! ```rust,ignore
//! use std::io;
//! use ratatui::prelude::*;
//! use ratatui::{backend::TermionBackend, Terminal};
//! use termion::raw::IntoRawMode;
//!
//! fn main() -> Result<(), io::Error> {
@@ -72,14 +80,18 @@
//! implement your own.
//!
//! Each widget follows a builder pattern API providing a default configuration along with methods
//! to customize them. The widget is then rendered using [`Frame::render_widget`] which takes your
//! widget instance and an area to draw to.
//! to customize them. The widget is then rendered using [`Frame::render_widget`] which takes
//! your widget instance and an area to draw to.
//!
//! The following example renders a block of the size of the terminal:
//!
//! ```rust,no_run
//! use std::{io, thread, time::Duration};
//! use ratatui::{prelude::*, widgets::*};
//! use ratatui::{
//! backend::CrosstermBackend,
//! widgets::{Block, Borders},
//! Terminal
//! };
//! use crossterm::{
//! event::{self, DisableMouseCapture, EnableMouseCapture},
//! execute,
@@ -130,8 +142,12 @@
//! full customization. And `Layout` is no exception:
//!
//! ```rust,no_run
//! use ratatui::{prelude::*, widgets::*};
//!
//! use ratatui::{
//! backend::Backend,
//! layout::{Constraint, Direction, Layout},
//! widgets::{Block, Borders},
//! Frame,
//! };
//! fn ui<B: Backend>(f: &mut Frame<B>) {
//! let chunks = Layout::default()
//! .direction(Direction::Vertical)
@@ -155,32 +171,13 @@
//! }
//! ```
//!
//! This let you describe responsive terminal UI by nesting layouts. You should note that by default
//! the computed layout tries to fill the available space completely. So if for any reason you might
//! need a blank space somewhere, try to pass an additional constraint and don't use the
//! This let you describe responsive terminal UI by nesting layouts. You should note that by
//! default the computed layout tries to fill the available space completely. So if for any reason
//! you might need a blank space somewhere, try to pass an additional constraint and don't use the
//! corresponding area.
//!
//! # Features
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
//!
//! [`Layout`]: layout::Layout
//! [`backend`]: backend
//! [`calendar`]: widgets::calendar
//! [`CrosstermBackend`]: backend::CrosstermBackend
//! [`TermionBackend`]: backend::TermionBackend
//! [`TermwizBackend`]: backend::TermwizBackend
//! [Crossterm crate]: https://crates.io/crates/crossterm
//! [Serde crate]: https://crates.io/crates/serde
//! [Termion crate]: https://crates.io/crates/termion
//! [Termwiz crate]: https://crates.io/crates/termwiz
//! [Time crate]: https://crates.io/crates/time
// show the feature flags in the generated documentation
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![doc(
html_logo_url = "https://raw.githubusercontent.com/ratatui-org/ratatui/main/assets/logo.png",
html_favicon_url = "https://raw.githubusercontent.com/ratatui-org/ratatui/main/assets/favicon.ico"
)]
pub mod backend;
pub mod buffer;
@@ -191,7 +188,6 @@ pub mod terminal;
pub mod text;
pub mod widgets;
#[doc(inline)]
pub use self::terminal::{CompletedFrame, Frame, Terminal, TerminalOptions, Viewport};
pub use self::terminal::{Frame, Terminal, TerminalOptions, Viewport};
pub mod prelude;

View File

@@ -9,6 +9,7 @@
//!
//! ```rust
//! use ratatui::{prelude::*, widgets::*};
//! use ratatui::widgets::{Block, Borders};
//!
//! #[derive(Debug, Default, PartialEq, Eq)]
//! struct Line;
@@ -29,6 +30,6 @@ pub use crate::{
layout::{self, Alignment, Constraint, Corner, Direction, Layout, Margin, Rect},
style::{self, Color, Modifier, Style, Styled, Stylize},
symbols::{self, Marker},
terminal::{CompletedFrame, Frame, Terminal, TerminalOptions, Viewport},
terminal::{self, Frame, Terminal, TerminalOptions, Viewport},
text::{self, Line, Masked, Span, Text},
};

View File

@@ -1,73 +1,142 @@
//! `style` contains the primitives used to control how your user interface will look.
//!
//! There are two ways to set styles:
//! - Creating and using the [`Style`] struct. (e.g. `Style::new().fg(Color::Red)`).
//! - Using style shorthands. (e.g. `"hello".red()`).
//!
//! # Using the `Style` struct
//!
//! This is the original approach to styling and likely the most common. This is useful when
//! creating style variables to reuse, however the shorthands are often more convenient and
//! readable for most use cases.
//!
//! This is useful when creating style variables.
//! ## Example
//!
//! ```
//! use ratatui::prelude::*;
//! use ratatui::style::{Color, Modifier, Style};
//!
//! let heading_style = Style::new()
//! Style::default()
//! .fg(Color::Black)
//! .bg(Color::Green)
//! .add_modifier(Modifier::ITALIC | Modifier::BOLD);
//! let span = Span::styled("hello", heading_style);
//! ```
//!
//! # Using style shorthands
//!
//! Originally Ratatui only had the ability to set styles using the `Style` struct. This is still
//! supported, but there are now shorthands for all the styles that can be set. These save you from
//! having to create a `Style` struct every time you want to set a style.
//!
//! The shorthands are implemented in the [`Stylize`] trait which is automatically implemented for
//! many types via the [`Styled`] trait. This means that you can use the shorthands on any type
//! that implements [`Styled`]. E.g.:
//! - Strings and string slices when styled return a [`Span`]
//! - [`Span`]s can be styled again, which will merge the styles.
//! - Many widget types can be styled directly rather than calling their style() method.
//!
//! See the [`Stylize`] and [`Styled`] traits for more information. These traits are re-exported in
//! the [`prelude`] module for convenience.
//!
//! This is best for concise styling.
//! ## Example
//!
//! ```
//! use ratatui::{prelude::*, widgets::*};
//! use ratatui::prelude::*;
//!
//! assert_eq!(
//! "hello".red().on_blue().bold(),
//! Span::styled(
//! "hello",
//! Style::default().fg(Color::Red).bg(Color::Blue).add_modifier(Modifier::BOLD))
//! );
//!
//! assert_eq!(
//! Paragraph::new("hello").red().on_blue().bold(),
//! Paragraph::new("hello")
//! .style(Style::default().fg(Color::Red).bg(Color::Blue).add_modifier(Modifier::BOLD))
//! );
//! Span::styled("hello", Style::default().fg(Color::Red).bg(Color::Blue).add_modifier(Modifier::BOLD))
//! )
//! ```
//!
//! [`prelude`]: crate::prelude
//! [`Span`]: crate::text::Span
use std::fmt::{self, Debug};
use std::{
fmt::{self, Debug},
str::FromStr,
};
use bitflags::bitflags;
mod stylize;
pub use stylize::{Styled, Stylize};
mod color;
pub use color::Color;
/// ANSI Color
///
/// All colors from the [ANSI color table](https://en.wikipedia.org/wiki/ANSI_escape_code#Colors)
/// are supported (though some names are not exactly the same).
///
/// | Color Name | Color | Foreground | Background |
/// |----------------|-------------------------|------------|------------|
/// | `black` | [`Color::Black`] | 30 | 40 |
/// | `red` | [`Color::Red`] | 31 | 41 |
/// | `green` | [`Color::Green`] | 32 | 42 |
/// | `yellow` | [`Color::Yellow`] | 33 | 43 |
/// | `blue` | [`Color::Blue`] | 34 | 44 |
/// | `magenta` | [`Color::Magenta`] | 35 | 45 |
/// | `cyan` | [`Color::Cyan`] | 36 | 46 |
/// | `gray`* | [`Color::Gray`] | 37 | 47 |
/// | `darkgray`* | [`Color::DarkGray`] | 90 | 100 |
/// | `lightred` | [`Color::LightRed`] | 91 | 101 |
/// | `lightgreen` | [`Color::LightGreen`] | 92 | 102 |
/// | `lightyellow` | [`Color::LightYellow`] | 93 | 103 |
/// | `lightblue` | [`Color::LightBlue`] | 94 | 104 |
/// | `lightmagenta` | [`Color::LightMagenta`] | 95 | 105 |
/// | `lightcyan` | [`Color::LightCyan`] | 96 | 106 |
/// | `white`* | [`Color::White`] | 97 | 107 |
///
/// - `gray` is sometimes called `white` - this is not supported as we use `white` for bright white
/// - `gray` is sometimes called `silver` - this is supported
/// - `darkgray` is sometimes called `light black` or `bright black` (both are supported)
/// - `white` is sometimes called `light white` or `bright white` (both are supported)
/// - we support `bright` and `light` prefixes for all colors
/// - we support `-` and `_` and ` ` as separators for all colors
/// - we support both `gray` and `grey` spellings
///
/// # Example
///
/// ```
/// use ratatui::style::Color;
/// use std::str::FromStr;
/// assert_eq!(Color::from_str("red"), Ok(Color::Red));
/// assert_eq!("red".parse(), Ok(Color::Red));
/// assert_eq!("lightred".parse(), Ok(Color::LightRed));
/// assert_eq!("light red".parse(), Ok(Color::LightRed));
/// assert_eq!("light-red".parse(), Ok(Color::LightRed));
/// assert_eq!("light_red".parse(), Ok(Color::LightRed));
/// assert_eq!("lightRed".parse(), Ok(Color::LightRed));
/// assert_eq!("bright red".parse(), Ok(Color::LightRed));
/// assert_eq!("bright-red".parse(), Ok(Color::LightRed));
/// assert_eq!("silver".parse(), Ok(Color::Gray));
/// assert_eq!("dark-grey".parse(), Ok(Color::DarkGray));
/// assert_eq!("dark gray".parse(), Ok(Color::DarkGray));
/// assert_eq!("light-black".parse(), Ok(Color::DarkGray));
/// assert_eq!("white".parse(), Ok(Color::White));
/// assert_eq!("bright white".parse(), Ok(Color::White));
/// ```
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Color {
/// Resets the foreground or background color
#[default]
Reset,
/// ANSI Color: Black. Foreground: 30, Background: 40
Black,
/// ANSI Color: Red. Foreground: 31, Background: 41
Red,
/// ANSI Color: Green. Foreground: 32, Background: 42
Green,
/// ANSI Color: Yellow. Foreground: 33, Background: 43
Yellow,
/// ANSI Color: Blue. Foreground: 34, Background: 44
Blue,
/// ANSI Color: Magenta. Foreground: 35, Background: 45
Magenta,
/// ANSI Color: Cyan. Foreground: 36, Background: 46
Cyan,
/// ANSI Color: White. Foreground: 37, Background: 47
///
/// Note that this is sometimes called `silver` or `white` but we use `white` for bright white
Gray,
/// ANSI Color: Bright Black. Foreground: 90, Background: 100
///
/// Note that this is sometimes called `light black` or `bright black` but we use `dark gray`
DarkGray,
/// ANSI Color: Bright Red. Foreground: 91, Background: 101
LightRed,
/// ANSI Color: Bright Green. Foreground: 92, Background: 102
LightGreen,
/// ANSI Color: Bright Yellow. Foreground: 93, Background: 103
LightYellow,
/// ANSI Color: Bright Blue. Foreground: 94, Background: 104
LightBlue,
/// ANSI Color: Bright Magenta. Foreground: 95, Background: 105
LightMagenta,
/// ANSI Color: Bright Cyan. Foreground: 96, Background: 106
LightCyan,
/// ANSI Color: Bright White. Foreground: 97, Background: 107
/// Sometimes called `bright white` or `light white` in some terminals
White,
/// An RGB color
Rgb(u8, u8, u8),
/// An 8-bit 256 color. See <https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit>
Indexed(u8),
}
bitflags! {
/// Modifier changes the way a piece of text is displayed.
@@ -77,12 +146,12 @@ bitflags! {
/// ## Examples
///
/// ```rust
/// use ratatui::{prelude::*};
/// # use ratatui::style::Modifier;
///
/// let m = Modifier::BOLD | Modifier::ITALIC;
/// ```
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Default, Clone, Copy, Eq, PartialEq, Hash)]
#[derive(Default, Clone, Copy, PartialEq, Eq)]
pub struct Modifier: u16 {
const BOLD = 0b0000_0000_0001;
const DIM = 0b0000_0000_0010;
@@ -110,33 +179,24 @@ impl fmt::Debug for Modifier {
}
}
/// Style lets you control the main characteristics of the displayed elements.
/// Style let you control the main characteristics of the displayed elements.
///
/// ```rust
/// use ratatui::{prelude::*};
///
/// # use ratatui::style::{Color, Modifier, Style};
/// Style::default()
/// .fg(Color::Black)
/// .bg(Color::Green)
/// .add_modifier(Modifier::ITALIC | Modifier::BOLD);
/// ```
///
/// Styles can also be created with a [shorthand notation](crate::style#using-style-shorthands).
///
/// ```rust
/// # use ratatui::prelude::*;
/// Style::new().black().on_green().italic().bold();
/// ```
///
/// For more information about the style shorthands, see the [`Stylize`] trait.
///
/// Styles represents an incremental change. If you apply the styles S1, S2, S3 to a cell of the
/// It represents an incremental change. If you apply the styles S1, S2, S3 to a cell of the
/// terminal buffer, the style of this cell will be the result of the merge of S1, S2 and S3, not
/// just S3.
///
/// ```rust
/// use ratatui::{prelude::*};
///
/// # use ratatui::style::{Color, Modifier, Style};
/// # use ratatui::buffer::Buffer;
/// # use ratatui::layout::Rect;
/// let styles = [
/// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC),
/// Style::default().bg(Color::Red).add_modifier(Modifier::UNDERLINED),
@@ -165,8 +225,9 @@ impl fmt::Debug for Modifier {
/// reset all properties until that point use [`Style::reset`].
///
/// ```
/// use ratatui::{prelude::*};
///
/// # use ratatui::style::{Color, Modifier, Style};
/// # use ratatui::buffer::Buffer;
/// # use ratatui::layout::Rect;
/// let styles = [
/// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC),
/// Style::reset().fg(Color::Yellow),
@@ -187,7 +248,7 @@ impl fmt::Debug for Modifier {
/// buffer.get(0, 0).style(),
/// );
/// ```
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Style {
pub fg: Option<Color>,
@@ -244,7 +305,7 @@ impl Style {
/// ## Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// # use ratatui::style::{Color, Style};
/// let style = Style::default().fg(Color::Blue);
/// let diff = Style::default().fg(Color::Red);
/// assert_eq!(style.patch(diff), Style::default().fg(Color::Red));
@@ -259,7 +320,7 @@ impl Style {
/// ## Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// # use ratatui::style::{Color, Style};
/// let style = Style::default().bg(Color::Blue);
/// let diff = Style::default().bg(Color::Red);
/// assert_eq!(style.patch(diff), Style::default().bg(Color::Red));
@@ -279,7 +340,7 @@ impl Style {
/// ## Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// # use ratatui::style::{Color, Modifier, Style};
/// let style = Style::default().underline_color(Color::Blue).add_modifier(Modifier::UNDERLINED);
/// let diff = Style::default().underline_color(Color::Red).add_modifier(Modifier::UNDERLINED);
/// assert_eq!(style.patch(diff), Style::default().underline_color(Color::Red).add_modifier(Modifier::UNDERLINED));
@@ -297,7 +358,7 @@ impl Style {
/// ## Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// # use ratatui::style::{Color, Modifier, Style};
/// let style = Style::default().add_modifier(Modifier::BOLD);
/// let diff = Style::default().add_modifier(Modifier::ITALIC);
/// let patched = style.patch(diff);
@@ -317,7 +378,7 @@ impl Style {
/// ## Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// # use ratatui::style::{Color, Modifier, Style};
/// let style = Style::default().add_modifier(Modifier::BOLD | Modifier::ITALIC);
/// let diff = Style::default().remove_modifier(Modifier::ITALIC);
/// let patched = style.patch(diff);
@@ -335,7 +396,7 @@ impl Style {
///
/// ## Examples
/// ```
/// # use ratatui::prelude::*;
/// # use ratatui::style::{Color, Modifier, Style};
/// let style_1 = Style::default().fg(Color::Yellow);
/// let style_2 = Style::default().bg(Color::Red);
/// let combined = style_1.patch(style_2);
@@ -361,8 +422,105 @@ impl Style {
}
}
/// Error type indicating a failure to parse a color string.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ParseColorError;
impl std::fmt::Display for ParseColorError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Failed to parse Colors")
}
}
impl std::error::Error for ParseColorError {}
/// Converts a string representation to a `Color` instance.
///
/// The `from_str` function attempts to parse the given string and convert it to the corresponding
/// `Color` variant. It supports named colors, RGB values, and indexed colors. If the string cannot
/// be parsed, a `ParseColorError` is returned.
///
/// See the [`Color`](Color) documentation for more information on the supported color names.
///
/// # Examples
///
/// ```
/// # use std::str::FromStr;
/// # use ratatui::style::Color;
/// let color: Color = Color::from_str("blue").unwrap();
/// assert_eq!(color, Color::Blue);
///
/// let color: Color = Color::from_str("#FF0000").unwrap();
/// assert_eq!(color, Color::Rgb(255, 0, 0));
///
/// let color: Color = Color::from_str("10").unwrap();
/// assert_eq!(color, Color::Indexed(10));
///
/// let color: Result<Color, _> = Color::from_str("invalid_color");
/// assert!(color.is_err());
/// ```
impl FromStr for Color {
type Err = ParseColorError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(
// There is a mix of different color names and formats in the wild.
// This is an attempt to support as many as possible.
match s
.to_lowercase()
.replace([' ', '-', '_'], "")
.replace("bright", "light")
.replace("grey", "gray")
.replace("silver", "gray")
.replace("lightblack", "darkgray")
.replace("lightwhite", "white")
.replace("lightgray", "white")
.as_ref()
{
"reset" => Self::Reset,
"black" => Self::Black,
"red" => Self::Red,
"green" => Self::Green,
"yellow" => Self::Yellow,
"blue" => Self::Blue,
"magenta" => Self::Magenta,
"cyan" => Self::Cyan,
"gray" => Self::Gray,
"darkgray" => Self::DarkGray,
"lightred" => Self::LightRed,
"lightgreen" => Self::LightGreen,
"lightyellow" => Self::LightYellow,
"lightblue" => Self::LightBlue,
"lightmagenta" => Self::LightMagenta,
"lightcyan" => Self::LightCyan,
"white" => Self::White,
_ => {
if let Ok(index) = s.parse::<u8>() {
Self::Indexed(index)
} else if let (Ok(r), Ok(g), Ok(b)) = {
if !s.starts_with('#') || s.len() != 7 {
return Err(ParseColorError);
}
(
u8::from_str_radix(&s[1..3], 16),
u8::from_str_radix(&s[3..5], 16),
u8::from_str_radix(&s[5..7], 16),
)
} {
Self::Rgb(r, g, b)
} else {
return Err(ParseColorError);
}
}
},
)
}
}
#[cfg(test)]
mod tests {
use std::error::Error;
use super::*;
fn styles() -> Vec<Style> {
@@ -449,6 +607,92 @@ mod tests {
);
}
#[test]
fn from_rgb_color() {
let color: Color = Color::from_str("#FF0000").unwrap();
assert_eq!(color, Color::Rgb(255, 0, 0));
}
#[test]
fn from_indexed_color() {
let color: Color = Color::from_str("10").unwrap();
assert_eq!(color, Color::Indexed(10));
}
#[test]
fn from_ansi_color() -> Result<(), Box<dyn Error>> {
assert_eq!(Color::from_str("reset")?, Color::Reset);
assert_eq!(Color::from_str("black")?, Color::Black);
assert_eq!(Color::from_str("red")?, Color::Red);
assert_eq!(Color::from_str("green")?, Color::Green);
assert_eq!(Color::from_str("yellow")?, Color::Yellow);
assert_eq!(Color::from_str("blue")?, Color::Blue);
assert_eq!(Color::from_str("magenta")?, Color::Magenta);
assert_eq!(Color::from_str("cyan")?, Color::Cyan);
assert_eq!(Color::from_str("gray")?, Color::Gray);
assert_eq!(Color::from_str("darkgray")?, Color::DarkGray);
assert_eq!(Color::from_str("lightred")?, Color::LightRed);
assert_eq!(Color::from_str("lightgreen")?, Color::LightGreen);
assert_eq!(Color::from_str("lightyellow")?, Color::LightYellow);
assert_eq!(Color::from_str("lightblue")?, Color::LightBlue);
assert_eq!(Color::from_str("lightmagenta")?, Color::LightMagenta);
assert_eq!(Color::from_str("lightcyan")?, Color::LightCyan);
assert_eq!(Color::from_str("white")?, Color::White);
// aliases
assert_eq!(Color::from_str("lightblack")?, Color::DarkGray);
assert_eq!(Color::from_str("lightwhite")?, Color::White);
assert_eq!(Color::from_str("lightgray")?, Color::White);
// silver = grey = gray
assert_eq!(Color::from_str("grey")?, Color::Gray);
assert_eq!(Color::from_str("silver")?, Color::Gray);
// spaces are ignored
assert_eq!(Color::from_str("light black")?, Color::DarkGray);
assert_eq!(Color::from_str("light white")?, Color::White);
assert_eq!(Color::from_str("light gray")?, Color::White);
// dashes are ignored
assert_eq!(Color::from_str("light-black")?, Color::DarkGray);
assert_eq!(Color::from_str("light-white")?, Color::White);
assert_eq!(Color::from_str("light-gray")?, Color::White);
// underscores are ignored
assert_eq!(Color::from_str("light_black")?, Color::DarkGray);
assert_eq!(Color::from_str("light_white")?, Color::White);
assert_eq!(Color::from_str("light_gray")?, Color::White);
// bright = light
assert_eq!(Color::from_str("bright-black")?, Color::DarkGray);
assert_eq!(Color::from_str("bright-white")?, Color::White);
// bright = light
assert_eq!(Color::from_str("brightblack")?, Color::DarkGray);
assert_eq!(Color::from_str("brightwhite")?, Color::White);
Ok(())
}
#[test]
fn from_invalid_colors() {
let bad_colors = [
"invalid_color", // not a color string
"abcdef0", // 7 chars is not a color
" bcdefa", // doesn't start with a '#'
"#abcdef00", // too many chars
"resett", // typo
"lightblackk", // typo
];
for bad_color in bad_colors {
assert!(
Color::from_str(bad_color).is_err(),
"bad color: '{bad_color}'"
);
}
}
#[test]
fn style_can_be_const() {
const RED: Color = Color::Red;

View File

@@ -1,362 +0,0 @@
use std::{
fmt::{self, Debug, Display},
str::FromStr,
};
/// ANSI Color
///
/// All colors from the [ANSI color table] are supported (though some names are not exactly the
/// same).
///
/// | Color Name | Color | Foreground | Background |
/// |----------------|-------------------------|------------|------------|
/// | `black` | [`Color::Black`] | 30 | 40 |
/// | `red` | [`Color::Red`] | 31 | 41 |
/// | `green` | [`Color::Green`] | 32 | 42 |
/// | `yellow` | [`Color::Yellow`] | 33 | 43 |
/// | `blue` | [`Color::Blue`] | 34 | 44 |
/// | `magenta` | [`Color::Magenta`] | 35 | 45 |
/// | `cyan` | [`Color::Cyan`] | 36 | 46 |
/// | `gray`* | [`Color::Gray`] | 37 | 47 |
/// | `darkgray`* | [`Color::DarkGray`] | 90 | 100 |
/// | `lightred` | [`Color::LightRed`] | 91 | 101 |
/// | `lightgreen` | [`Color::LightGreen`] | 92 | 102 |
/// | `lightyellow` | [`Color::LightYellow`] | 93 | 103 |
/// | `lightblue` | [`Color::LightBlue`] | 94 | 104 |
/// | `lightmagenta` | [`Color::LightMagenta`] | 95 | 105 |
/// | `lightcyan` | [`Color::LightCyan`] | 96 | 106 |
/// | `white`* | [`Color::White`] | 97 | 107 |
///
/// - `gray` is sometimes called `white` - this is not supported as we use `white` for bright white
/// - `gray` is sometimes called `silver` - this is supported
/// - `darkgray` is sometimes called `light black` or `bright black` (both are supported)
/// - `white` is sometimes called `light white` or `bright white` (both are supported)
/// - we support `bright` and `light` prefixes for all colors
/// - we support `-` and `_` and ` ` as separators for all colors
/// - we support both `gray` and `grey` spellings
///
/// # Example
///
/// ```
/// use std::str::FromStr;
/// use ratatui::prelude::*;
///
/// assert_eq!(Color::from_str("red"), Ok(Color::Red));
/// assert_eq!("red".parse(), Ok(Color::Red));
/// assert_eq!("lightred".parse(), Ok(Color::LightRed));
/// assert_eq!("light red".parse(), Ok(Color::LightRed));
/// assert_eq!("light-red".parse(), Ok(Color::LightRed));
/// assert_eq!("light_red".parse(), Ok(Color::LightRed));
/// assert_eq!("lightRed".parse(), Ok(Color::LightRed));
/// assert_eq!("bright red".parse(), Ok(Color::LightRed));
/// assert_eq!("bright-red".parse(), Ok(Color::LightRed));
/// assert_eq!("silver".parse(), Ok(Color::Gray));
/// assert_eq!("dark-grey".parse(), Ok(Color::DarkGray));
/// assert_eq!("dark gray".parse(), Ok(Color::DarkGray));
/// assert_eq!("light-black".parse(), Ok(Color::DarkGray));
/// assert_eq!("white".parse(), Ok(Color::White));
/// assert_eq!("bright white".parse(), Ok(Color::White));
/// ```
///
/// [ANSI color table]: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Color {
/// Resets the foreground or background color
#[default]
Reset,
/// ANSI Color: Black. Foreground: 30, Background: 40
Black,
/// ANSI Color: Red. Foreground: 31, Background: 41
Red,
/// ANSI Color: Green. Foreground: 32, Background: 42
Green,
/// ANSI Color: Yellow. Foreground: 33, Background: 43
Yellow,
/// ANSI Color: Blue. Foreground: 34, Background: 44
Blue,
/// ANSI Color: Magenta. Foreground: 35, Background: 45
Magenta,
/// ANSI Color: Cyan. Foreground: 36, Background: 46
Cyan,
/// ANSI Color: White. Foreground: 37, Background: 47
///
/// Note that this is sometimes called `silver` or `white` but we use `white` for bright white
Gray,
/// ANSI Color: Bright Black. Foreground: 90, Background: 100
///
/// Note that this is sometimes called `light black` or `bright black` but we use `dark gray`
DarkGray,
/// ANSI Color: Bright Red. Foreground: 91, Background: 101
LightRed,
/// ANSI Color: Bright Green. Foreground: 92, Background: 102
LightGreen,
/// ANSI Color: Bright Yellow. Foreground: 93, Background: 103
LightYellow,
/// ANSI Color: Bright Blue. Foreground: 94, Background: 104
LightBlue,
/// ANSI Color: Bright Magenta. Foreground: 95, Background: 105
LightMagenta,
/// ANSI Color: Bright Cyan. Foreground: 96, Background: 106
LightCyan,
/// ANSI Color: Bright White. Foreground: 97, Background: 107
/// Sometimes called `bright white` or `light white` in some terminals
White,
/// An RGB color.
///
/// Note that only terminals that support 24-bit true color will display this correctly.
/// Notably versions of Windows Terminal prior to Windows 10 and macOS Terminal.app do not
/// support this.
///
/// If the terminal does not support true color, code using the [`TermwizBackend`] will
/// fallback to the default text color. Crossterm and Termion do not have this capability and
/// the display will be unpredictable (e.g. Terminal.app may display glitched blinking text).
/// See <https://github.com/ratatui-org/ratatui/issues/475> for an example of this problem.
///
/// See also: <https://en.wikipedia.org/wiki/ANSI_escape_code#24-bit>
///
/// [`TermwizBackend`]: crate::backend::TermwizBackend
Rgb(u8, u8, u8),
/// An 8-bit 256 color.
///
/// See also <https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit>
Indexed(u8),
}
/// Error type indicating a failure to parse a color string.
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub struct ParseColorError;
impl std::fmt::Display for ParseColorError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Failed to parse Colors")
}
}
impl std::error::Error for ParseColorError {}
/// Converts a string representation to a `Color` instance.
///
/// The `from_str` function attempts to parse the given string and convert it to the corresponding
/// `Color` variant. It supports named colors, RGB values, and indexed colors. If the string cannot
/// be parsed, a `ParseColorError` is returned.
///
/// See the [`Color`] documentation for more information on the supported color names.
///
/// # Examples
///
/// ```
/// use std::str::FromStr;
/// use ratatui::prelude::*;
///
/// let color: Color = Color::from_str("blue").unwrap();
/// assert_eq!(color, Color::Blue);
///
/// let color: Color = Color::from_str("#FF0000").unwrap();
/// assert_eq!(color, Color::Rgb(255, 0, 0));
///
/// let color: Color = Color::from_str("10").unwrap();
/// assert_eq!(color, Color::Indexed(10));
///
/// let color: Result<Color, _> = Color::from_str("invalid_color");
/// assert!(color.is_err());
/// ```
impl FromStr for Color {
type Err = ParseColorError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(
// There is a mix of different color names and formats in the wild.
// This is an attempt to support as many as possible.
match s
.to_lowercase()
.replace([' ', '-', '_'], "")
.replace("bright", "light")
.replace("grey", "gray")
.replace("silver", "gray")
.replace("lightblack", "darkgray")
.replace("lightwhite", "white")
.replace("lightgray", "white")
.as_ref()
{
"reset" => Self::Reset,
"black" => Self::Black,
"red" => Self::Red,
"green" => Self::Green,
"yellow" => Self::Yellow,
"blue" => Self::Blue,
"magenta" => Self::Magenta,
"cyan" => Self::Cyan,
"gray" => Self::Gray,
"darkgray" => Self::DarkGray,
"lightred" => Self::LightRed,
"lightgreen" => Self::LightGreen,
"lightyellow" => Self::LightYellow,
"lightblue" => Self::LightBlue,
"lightmagenta" => Self::LightMagenta,
"lightcyan" => Self::LightCyan,
"white" => Self::White,
_ => {
if let Ok(index) = s.parse::<u8>() {
Self::Indexed(index)
} else if let (Ok(r), Ok(g), Ok(b)) = {
if !s.starts_with('#') || s.len() != 7 {
return Err(ParseColorError);
}
(
u8::from_str_radix(&s[1..3], 16),
u8::from_str_radix(&s[3..5], 16),
u8::from_str_radix(&s[5..7], 16),
)
} {
Self::Rgb(r, g, b)
} else {
return Err(ParseColorError);
}
}
},
)
}
}
impl Display for Color {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Color::Reset => write!(f, "Reset"),
Color::Black => write!(f, "Black"),
Color::Red => write!(f, "Red"),
Color::Green => write!(f, "Green"),
Color::Yellow => write!(f, "Yellow"),
Color::Blue => write!(f, "Blue"),
Color::Magenta => write!(f, "Magenta"),
Color::Cyan => write!(f, "Cyan"),
Color::Gray => write!(f, "Gray"),
Color::DarkGray => write!(f, "DarkGray"),
Color::LightRed => write!(f, "LightRed"),
Color::LightGreen => write!(f, "LightGreen"),
Color::LightYellow => write!(f, "LightYellow"),
Color::LightBlue => write!(f, "LightBlue"),
Color::LightMagenta => write!(f, "LightMagenta"),
Color::LightCyan => write!(f, "LightCyan"),
Color::White => write!(f, "White"),
Color::Rgb(r, g, b) => write!(f, "#{:02X}{:02X}{:02X}", r, g, b),
Color::Indexed(i) => write!(f, "{}", i),
}
}
}
#[cfg(test)]
mod tests {
use std::error::Error;
use super::*;
#[test]
fn from_rgb_color() {
let color: Color = Color::from_str("#FF0000").unwrap();
assert_eq!(color, Color::Rgb(255, 0, 0));
}
#[test]
fn from_indexed_color() {
let color: Color = Color::from_str("10").unwrap();
assert_eq!(color, Color::Indexed(10));
}
#[test]
fn from_ansi_color() -> Result<(), Box<dyn Error>> {
assert_eq!(Color::from_str("reset")?, Color::Reset);
assert_eq!(Color::from_str("black")?, Color::Black);
assert_eq!(Color::from_str("red")?, Color::Red);
assert_eq!(Color::from_str("green")?, Color::Green);
assert_eq!(Color::from_str("yellow")?, Color::Yellow);
assert_eq!(Color::from_str("blue")?, Color::Blue);
assert_eq!(Color::from_str("magenta")?, Color::Magenta);
assert_eq!(Color::from_str("cyan")?, Color::Cyan);
assert_eq!(Color::from_str("gray")?, Color::Gray);
assert_eq!(Color::from_str("darkgray")?, Color::DarkGray);
assert_eq!(Color::from_str("lightred")?, Color::LightRed);
assert_eq!(Color::from_str("lightgreen")?, Color::LightGreen);
assert_eq!(Color::from_str("lightyellow")?, Color::LightYellow);
assert_eq!(Color::from_str("lightblue")?, Color::LightBlue);
assert_eq!(Color::from_str("lightmagenta")?, Color::LightMagenta);
assert_eq!(Color::from_str("lightcyan")?, Color::LightCyan);
assert_eq!(Color::from_str("white")?, Color::White);
// aliases
assert_eq!(Color::from_str("lightblack")?, Color::DarkGray);
assert_eq!(Color::from_str("lightwhite")?, Color::White);
assert_eq!(Color::from_str("lightgray")?, Color::White);
// silver = grey = gray
assert_eq!(Color::from_str("grey")?, Color::Gray);
assert_eq!(Color::from_str("silver")?, Color::Gray);
// spaces are ignored
assert_eq!(Color::from_str("light black")?, Color::DarkGray);
assert_eq!(Color::from_str("light white")?, Color::White);
assert_eq!(Color::from_str("light gray")?, Color::White);
// dashes are ignored
assert_eq!(Color::from_str("light-black")?, Color::DarkGray);
assert_eq!(Color::from_str("light-white")?, Color::White);
assert_eq!(Color::from_str("light-gray")?, Color::White);
// underscores are ignored
assert_eq!(Color::from_str("light_black")?, Color::DarkGray);
assert_eq!(Color::from_str("light_white")?, Color::White);
assert_eq!(Color::from_str("light_gray")?, Color::White);
// bright = light
assert_eq!(Color::from_str("bright-black")?, Color::DarkGray);
assert_eq!(Color::from_str("bright-white")?, Color::White);
// bright = light
assert_eq!(Color::from_str("brightblack")?, Color::DarkGray);
assert_eq!(Color::from_str("brightwhite")?, Color::White);
Ok(())
}
#[test]
fn from_invalid_colors() {
let bad_colors = [
"invalid_color", // not a color string
"abcdef0", // 7 chars is not a color
" bcdefa", // doesn't start with a '#'
"#abcdef00", // too many chars
"resett", // typo
"lightblackk", // typo
];
for bad_color in bad_colors {
assert!(
Color::from_str(bad_color).is_err(),
"bad color: '{bad_color}'"
);
}
}
#[test]
fn display() {
assert_eq!(format!("{}", Color::Black), "Black");
assert_eq!(format!("{}", Color::Red), "Red");
assert_eq!(format!("{}", Color::Green), "Green");
assert_eq!(format!("{}", Color::Yellow), "Yellow");
assert_eq!(format!("{}", Color::Blue), "Blue");
assert_eq!(format!("{}", Color::Magenta), "Magenta");
assert_eq!(format!("{}", Color::Cyan), "Cyan");
assert_eq!(format!("{}", Color::Gray), "Gray");
assert_eq!(format!("{}", Color::DarkGray), "DarkGray");
assert_eq!(format!("{}", Color::LightRed), "LightRed");
assert_eq!(format!("{}", Color::LightGreen), "LightGreen");
assert_eq!(format!("{}", Color::LightYellow), "LightYellow");
assert_eq!(format!("{}", Color::LightBlue), "LightBlue");
assert_eq!(format!("{}", Color::LightMagenta), "LightMagenta");
assert_eq!(format!("{}", Color::LightCyan), "LightCyan");
assert_eq!(format!("{}", Color::White), "White");
assert_eq!(format!("{}", Color::Indexed(10)), "10");
assert_eq!(format!("{}", Color::Rgb(255, 0, 0)), "#FF0000");
assert_eq!(format!("{}", Color::Reset), "Reset");
}
}

View File

@@ -90,41 +90,19 @@ macro_rules! modifier {
};
}
/// An extension trait for styling objects.
///
/// For any type that implements `Stylize`, the provided methods in this trait can be used to style
/// the type further. This trait is automatically implemented for any type that implements the
/// [`Styled`] trait which e.g.: [`String`], [`&str`], [`Span`], [`Style`] and many Widget types.
///
/// This results in much more ergonomic styling of text and widgets. For example, instead of
/// writing:
///
/// ```rust,ignore
/// let text = Span::styled("Hello", Style::default().fg(Color::Red).bg(Color::Blue));
/// ```
///
/// You can write:
///
/// ```rust,ignore
/// let text = "Hello".red().on_blue();
/// ```
///
/// This trait implements a provided method for every color as both foreground and background
/// (prefixed by `on_`), and all modifiers as both an additive and subtractive modifier (prefixed
/// by `not_`). The `reset()` method is also provided to reset the style.
/// The trait that enables something to be have a style.
///
/// # Examples
/// ```
/// use ratatui::{prelude::*, widgets::*};
/// use ratatui::{
/// style::{Color, Modifier, Style, Styled, Stylize},
/// text::Span,
/// };
///
/// let span = "hello".red().on_blue().bold();
/// let line = Line::from(vec![
/// "hello".red().on_blue().bold(),
/// "world".green().on_yellow().not_bold(),
/// ]);
/// let paragraph = Paragraph::new(line).italic().underlined();
/// let block = Block::default().title("Title").borders(Borders::ALL).on_white().bold();
/// ```
/// assert_eq!(
/// "hello".red().on_blue().bold(),
/// Span::styled("hello", Style::default().fg(Color::Red).bg(Color::Blue).add_modifier(Modifier::BOLD))
/// )
pub trait Stylize<'a, T>: Sized {
fn bg(self, color: Color) -> T;
fn fg<S: Into<Color>>(self, color: S) -> T;
@@ -201,145 +179,16 @@ impl<'a> Styled for &'a str {
}
}
impl Styled for String {
type Item = Span<'static>;
fn style(&self) -> Style {
Style::default()
}
fn set_style(self, style: Style) -> Self::Item {
Span::styled(self, style)
}
}
#[cfg(test)]
mod tests {
use itertools::Itertools;
use super::*;
#[test]
fn str_styled() {
assert_eq!("hello".style(), Style::default());
assert_eq!(
"hello".set_style(Style::new().cyan()),
Span::styled("hello", Style::new().cyan())
);
assert_eq!("hello".black(), Span::from("hello").black());
assert_eq!("hello".red(), Span::from("hello").red());
assert_eq!("hello".green(), Span::from("hello").green());
assert_eq!("hello".yellow(), Span::from("hello").yellow());
assert_eq!("hello".blue(), Span::from("hello").blue());
assert_eq!("hello".magenta(), Span::from("hello").magenta());
assert_eq!("hello".cyan(), Span::from("hello").cyan());
assert_eq!("hello".gray(), Span::from("hello").gray());
assert_eq!("hello".dark_gray(), Span::from("hello").dark_gray());
assert_eq!("hello".light_red(), Span::from("hello").light_red());
assert_eq!("hello".light_green(), Span::from("hello").light_green());
assert_eq!("hello".light_yellow(), Span::from("hello").light_yellow());
assert_eq!("hello".light_blue(), Span::from("hello").light_blue());
assert_eq!("hello".light_magenta(), Span::from("hello").light_magenta());
assert_eq!("hello".light_cyan(), Span::from("hello").light_cyan());
assert_eq!("hello".white(), Span::from("hello").white());
assert_eq!("hello".on_black(), Span::from("hello").on_black());
assert_eq!("hello".on_red(), Span::from("hello").on_red());
assert_eq!("hello".on_green(), Span::from("hello").on_green());
assert_eq!("hello".on_yellow(), Span::from("hello").on_yellow());
assert_eq!("hello".on_blue(), Span::from("hello").on_blue());
assert_eq!("hello".on_magenta(), Span::from("hello").on_magenta());
assert_eq!("hello".on_cyan(), Span::from("hello").on_cyan());
assert_eq!("hello".on_gray(), Span::from("hello").on_gray());
assert_eq!("hello".on_dark_gray(), Span::from("hello").on_dark_gray());
assert_eq!("hello".on_light_red(), Span::from("hello").on_light_red());
assert_eq!(
"hello".on_light_green(),
Span::from("hello").on_light_green()
);
assert_eq!(
"hello".on_light_yellow(),
Span::from("hello").on_light_yellow()
);
assert_eq!("hello".on_light_blue(), Span::from("hello").on_light_blue());
assert_eq!(
"hello".on_light_magenta(),
Span::from("hello").on_light_magenta()
);
assert_eq!("hello".on_light_cyan(), Span::from("hello").on_light_cyan());
assert_eq!("hello".on_white(), Span::from("hello").on_white());
assert_eq!("hello".bold(), Span::from("hello").bold());
assert_eq!("hello".dim(), Span::from("hello").dim());
assert_eq!("hello".italic(), Span::from("hello").italic());
assert_eq!("hello".underlined(), Span::from("hello").underlined());
assert_eq!("hello".slow_blink(), Span::from("hello").slow_blink());
assert_eq!("hello".rapid_blink(), Span::from("hello").rapid_blink());
assert_eq!("hello".reversed(), Span::from("hello").reversed());
assert_eq!("hello".hidden(), Span::from("hello").hidden());
assert_eq!("hello".crossed_out(), Span::from("hello").crossed_out());
assert_eq!("hello".not_bold(), Span::from("hello").not_bold());
assert_eq!("hello".not_dim(), Span::from("hello").not_dim());
assert_eq!("hello".not_italic(), Span::from("hello").not_italic());
assert_eq!(
"hello".not_underlined(),
Span::from("hello").not_underlined()
);
assert_eq!(
"hello".not_slow_blink(),
Span::from("hello").not_slow_blink()
);
assert_eq!(
"hello".not_rapid_blink(),
Span::from("hello").not_rapid_blink()
);
assert_eq!("hello".not_reversed(), Span::from("hello").not_reversed());
assert_eq!("hello".not_hidden(), Span::from("hello").not_hidden());
assert_eq!(
"hello".not_crossed_out(),
Span::from("hello").not_crossed_out()
);
assert_eq!("hello".reset(), Span::from("hello").reset());
}
#[test]
fn string_styled() {
let s = String::from("hello");
assert_eq!(s.style(), Style::default());
assert_eq!(
s.clone().set_style(Style::new().cyan()),
Span::styled("hello", Style::new().cyan())
);
assert_eq!(s.clone().black(), Span::from("hello").black());
assert_eq!(s.clone().on_black(), Span::from("hello").on_black());
assert_eq!(s.clone().bold(), Span::from("hello").bold());
assert_eq!(s.clone().not_bold(), Span::from("hello").not_bold());
assert_eq!(s.clone().reset(), Span::from("hello").reset());
}
#[test]
fn temporary_string_styled() {
// to_string() is used to create a temporary String, which is then styled. Without the
// `Styled` trait impl for `String`, this would fail to compile with the error: "temporary
// value dropped while borrowed"
let s = "hello".to_string().red();
assert_eq!(s, Span::from("hello").red());
// format!() is used to create a temporary String inside a closure, which suffers the same
// issue as above without the `Styled` trait impl for `String`
let items = vec![String::from("a"), String::from("b")];
let sss = items.iter().map(|s| format!("{s}{s}").red()).collect_vec();
assert_eq!(sss, vec![Span::from("aa").red(), Span::from("bb").red()]);
}
#[test]
fn reset() {
assert_eq!(
"hello".on_cyan().light_red().bold().underlined().reset(),
Span::styled("hello", Style::reset())
);
)
}
#[test]
@@ -362,14 +211,14 @@ mod tests {
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD);
assert_eq!("hello".cyan().bold(), Span::styled("hello", cyan_bold));
assert_eq!("hello".cyan().bold(), Span::styled("hello", cyan_bold))
}
#[test]
fn fg_bg() {
let cyan_fg_bg = Style::default().bg(Color::Cyan).fg(Color::Cyan);
assert_eq!("hello".cyan().on_cyan(), Span::styled("hello", cyan_fg_bg));
assert_eq!("hello".cyan().on_cyan(), Span::styled("hello", cyan_fg_bg))
}
#[test]

View File

@@ -1,5 +1,3 @@
use strum::{Display, EnumString};
pub mod block {
pub const FULL: &str = "";
pub const SEVEN_EIGHTHS: &str = "";
@@ -10,7 +8,7 @@ pub mod block {
pub const ONE_QUARTER: &str = "";
pub const ONE_EIGHTH: &str = "";
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
#[derive(Debug, Clone)]
pub struct Set {
pub full: &'static str,
pub seven_eighths: &'static str,
@@ -64,7 +62,7 @@ pub mod bar {
pub const ONE_QUARTER: &str = "";
pub const ONE_EIGHTH: &str = "";
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
#[derive(Debug, Clone)]
pub struct Set {
pub full: &'static str,
pub seven_eighths: &'static str,
@@ -157,7 +155,7 @@ pub mod line {
pub const DOUBLE_CROSS: &str = "";
pub const THICK_CROSS: &str = "";
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
#[derive(Debug, Clone)]
pub struct Set {
pub vertical: &'static str,
pub horizontal: &'static str,
@@ -242,23 +240,16 @@ pub mod braille {
}
/// Marker to use when plotting data points
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
#[derive(Debug, Default, Clone, Copy)]
pub enum Marker {
/// One point per cell in shape of dot ("•")
/// One point per cell in shape of dot
#[default]
Dot,
/// One point per cell in shape of a block ("█")
/// One point per cell in shape of a block
Block,
/// One point per cell in the shape of a bar ("▄")
/// One point per cell in the shape of a bar
Bar,
/// Use the [Unicode Braille Patterns](https://en.wikipedia.org/wiki/Braille_Patterns) block to
/// represent data points.
///
/// This is a 2x4 grid of dots, where each dot can be either on or off.
///
/// Note: Support for this marker is limited to terminals and fonts that support Unicode
/// Braille Patterns. If your terminal does not support this, you will see unicode replacement
/// characters (<28>) instead of Braille dots.
/// Up to 8 points per cell
Braille,
}
@@ -274,7 +265,7 @@ pub mod scrollbar {
/// │ └──────── thumb
/// └─────────── begin
/// ```
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
#[derive(Debug, Default, Clone)]
pub struct Set {
pub track: &'static str,
pub thumb: &'static str,
@@ -310,27 +301,3 @@ pub mod scrollbar {
end: "",
};
}
#[cfg(test)]
mod tests {
use strum::ParseError;
use super::*;
#[test]
fn marker_tostring() {
assert_eq!(Marker::Dot.to_string(), "Dot");
assert_eq!(Marker::Block.to_string(), "Block");
assert_eq!(Marker::Bar.to_string(), "Bar");
assert_eq!(Marker::Braille.to_string(), "Braille");
}
#[test]
fn marker_from_str() {
assert_eq!("Dot".parse::<Marker>(), Ok(Marker::Dot));
assert_eq!("Block".parse::<Marker>(), Ok(Marker::Block));
assert_eq!("Bar".parse::<Marker>(), Ok(Marker::Bar));
assert_eq!("Braille".parse::<Marker>(), Ok(Marker::Braille));
assert_eq!("".parse::<Marker>(), Err(ParseError::VariantNotFound));
}
}

View File

@@ -1,35 +1,4 @@
#![deny(missing_docs)]
//! Provides the [`Terminal`], [`Frame`] and related types.
//!
//! The [`Terminal`] is the main interface of this library. It is responsible for drawing and
//! maintaining the state of the different widgets that compose your application.
//!
//! The [`Frame`] is a consistent view into the terminal state for rendering. It is obtained via
//! the closure argument of [`Terminal::draw`]. It is used to render widgets to the terminal and
//! control the cursor position.
//!
//! # Example
//!
//! ```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);
//! })?;
//! # 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
use std::{fmt, io};
use std::io;
use crate::{
backend::{Backend, ClearType},
@@ -38,98 +7,27 @@ use crate::{
widgets::{StatefulWidget, Widget},
};
/// 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)]
#[derive(Debug, Default, Clone, Eq, PartialEq)]
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)]
#[derive(Debug, Default, Clone, Eq, PartialEq)]
pub struct TerminalOptions {
/// Viewport used to draw to the terminal
pub viewport: Viewport,
}
/// An interface that Ratatui 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 has two buffers that are used to draw the application. The first buffer is the
/// current buffer and the second buffer is the previous buffer. The two buffers are compared at
/// the end of each draw pass to output only the changes to the terminal.
///
/// The terminal 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)]
/// Interface to the terminal backed by Termion
#[derive(Debug, Default, Clone)]
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
@@ -140,7 +38,6 @@ where
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,
@@ -149,6 +46,105 @@ where
last_known_cursor_pos: (u16, u16),
}
/// Represents a consistent terminal interface for rendering.
#[derive(Debug)]
pub struct Frame<'a, B: 'a>
where
B: Backend,
{
terminal: &'a mut Terminal<B>,
/// 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)>,
}
impl<'a, B> Frame<'a, B>
where
B: Backend,
{
/// Frame size, guaranteed not to change when rendering.
pub fn size(&self) -> Rect {
self.terminal.viewport_area
}
/// Render a [`Widget`] to the current buffer using [`Widget::render`].
///
/// # Examples
///
/// ```rust
/// # use ratatui::Terminal;
/// # use ratatui::backend::TestBackend;
/// # use ratatui::layout::Rect;
/// # use ratatui::widgets::Block;
/// # let backend = TestBackend::new(5, 5);
/// # let mut terminal = Terminal::new(backend).unwrap();
/// let block = Block::default();
/// let area = Rect::new(0, 0, 5, 5);
/// let mut frame = terminal.get_frame();
/// frame.render_widget(block, area);
/// ```
pub fn render_widget<W>(&mut self, widget: W, area: Rect)
where
W: Widget,
{
widget.render(area, self.terminal.current_buffer_mut());
}
/// Render a [`StatefulWidget`] to the current buffer using [`StatefulWidget::render`].
///
/// The last argument should be an instance of the [`StatefulWidget::State`] associated to the
/// given [`StatefulWidget`].
///
/// # Examples
///
/// ```rust
/// # use ratatui::Terminal;
/// # use ratatui::backend::TestBackend;
/// # use ratatui::layout::Rect;
/// # use ratatui::widgets::{List, ListItem, ListState};
/// # let backend = TestBackend::new(5, 5);
/// # let mut terminal = Terminal::new(backend).unwrap();
/// let mut state = ListState::default();
/// state.select(Some(1));
/// let items = vec![
/// ListItem::new("Item 1"),
/// ListItem::new("Item 2"),
/// ];
/// let list = List::new(items);
/// let area = Rect::new(0, 0, 5, 5);
/// let mut frame = terminal.get_frame();
/// frame.render_stateful_widget(list, area, &mut state);
/// ```
pub fn render_stateful_widget<W>(&mut self, widget: W, area: Rect, state: &mut W::State)
where
W: StatefulWidget,
{
widget.render(area, self.terminal.current_buffer_mut(), 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));
}
}
/// `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)]
pub struct CompletedFrame<'a> {
pub buffer: &'a Buffer,
pub area: Rect,
}
impl<B> Drop for Terminal<B>
where
B: Backend,
@@ -167,17 +163,8 @@ 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(())
/// ```
/// Wrapper around Terminal initialization. Each buffer is initialized with a blank string and
/// default colors for the foreground and the background
pub fn new(backend: B) -> io::Result<Terminal<B>> {
Terminal::with_options(
backend,
@@ -187,21 +174,6 @@ where
)
}
/// 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()?,
@@ -232,17 +204,14 @@ where
}
}
/// 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
}
@@ -259,10 +228,9 @@ where
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.
/// 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,
@@ -302,23 +270,6 @@ where
/// 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<B>),
@@ -356,29 +307,22 @@ where
})
}
/// 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);
@@ -449,7 +393,11 @@ where
/// ## Insert a single line before the current viewport
///
/// ```rust
/// # use ratatui::{backend::TestBackend, prelude::*, widgets::*};
/// # use ratatui::widgets::{Paragraph, Widget};
/// # use ratatui::text::{Line, Span};
/// # use ratatui::style::{Color, Style};
/// # use ratatui::{Terminal};
/// # use ratatui::backend::TestBackend;
/// # let backend = TestBackend::new(10, 10);
/// # let mut terminal = Terminal::new(backend).unwrap();
/// terminal.insert_before(1, |buf| {
@@ -539,144 +487,3 @@ fn compute_inline_size<B: Backend>(
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 not immediately applied to the terminal. They are only
/// applied after the closure returns. This allows for widgets to be drawn in any order. The
/// changes are then compared to the previous frame and only the necessary updates are applied to
/// the terminal.
///
/// The [`Frame`] is generic over a [`Backend`] implementation which is used to interface with the
/// underlying terminal library. The [`Backend`] trait is implemented for three popular Rust
/// terminal libraries: [Crossterm], [Termion] and [Termwiz]. See the [`backend`] module for more
/// information.
///
/// [Crossterm]: https://crates.io/crates/crossterm
/// [Termion]: https://crates.io/crates/termion
/// [Termwiz]: https://crates.io/crates/termwiz
/// [`backend`]: crate::backend
/// [`Backend`]: crate::backend::Backend
/// [`Buffer`]: crate::buffer::Buffer
#[derive(Debug, Hash)]
pub struct Frame<'a, B: 'a>
where
B: Backend,
{
/// The terminal that this frame is associated with.
terminal: &'a mut Terminal<B>,
/// 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)>,
}
impl<'a, B> Frame<'a, B>
where
B: Backend,
{
/// The size of the current frame
///
/// This is guaranteed not to change when rendering.
pub fn size(&self) -> Rect {
self.terminal.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.terminal.current_buffer_mut());
}
/// 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.terminal.current_buffer_mut(), 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));
}
}
/// `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

@@ -5,7 +5,7 @@ use crate::style::{Style, Styled};
/// it actually is not a member of the text type hierarchy (`Text` -> `Line` -> `Span`).
/// It is a separate type used mostly for rendering purposes. A `Span` consists of components that
/// can be split into `StyledGrapheme`s, but it does not contain a collection of `StyledGrapheme`s.
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct StyledGrapheme<'a> {
pub symbol: &'a str,
pub style: Style,
@@ -29,39 +29,3 @@ impl<'a> Styled for StyledGrapheme<'a> {
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::prelude::*;
#[test]
fn new() {
let style = Style::new().yellow();
let sg = StyledGrapheme::new("a", style);
assert_eq!(sg.symbol, "a");
assert_eq!(sg.style, style);
}
#[test]
fn style() {
let style = Style::new().yellow();
let sg = StyledGrapheme::new("a", style);
assert_eq!(sg.style(), style);
}
#[test]
fn set_style() {
let style = Style::new().yellow().on_red();
let style2 = Style::new().green();
let sg = StyledGrapheme::new("a", style).set_style(style2);
assert_eq!(sg.style, style2);
}
#[test]
fn stylize() {
let style = Style::new().yellow().on_red();
let sg = StyledGrapheme::new("a", style).green();
assert_eq!(sg.style, Style::new().green().on_red());
}
}

View File

@@ -4,7 +4,7 @@ use std::borrow::Cow;
use super::{Span, Spans, Style, StyledGrapheme};
use crate::layout::Alignment;
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
#[derive(Debug, Default, Clone, Eq, PartialEq)]
pub struct Line<'a> {
pub spans: Vec<Span<'a>>,
pub alignment: Option<Alignment>,
@@ -16,7 +16,8 @@ impl<'a> Line<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// # use ratatui::text::Line;
/// # use ratatui::style::{Color, Modifier, Style};
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
/// Line::styled("My text", style);
/// Line::styled(String::from("My text"), style);
@@ -33,7 +34,8 @@ impl<'a> Line<'a> {
/// ## Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// # use ratatui::text::{Span, Line};
/// # use ratatui::style::{Color, Style};
/// let line = Line::from(vec![
/// Span::styled("My", Style::default().fg(Color::Yellow)),
/// Span::raw(" text"),
@@ -52,9 +54,9 @@ impl<'a> Line<'a> {
/// ## Examples
///
/// ```rust
/// use std::iter::Iterator;
/// use ratatui::{prelude::*, text::StyledGrapheme};
///
/// # use ratatui::text::{Line, StyledGrapheme};
/// # use ratatui::style::{Color, Modifier, Style};
/// # use std::iter::Iterator;
/// let line = Line::styled("Text", Style::default().fg(Color::Yellow));
/// let style = Style::default().fg(Color::Green).bg(Color::Black);
/// assert_eq!(
@@ -81,7 +83,8 @@ impl<'a> Line<'a> {
/// ## Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// # use ratatui::text::{Span, Line};
/// # use ratatui::style::{Color, Style, Modifier};
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
/// let mut raw_line = Line::from(vec![
/// Span::raw("My"),
@@ -109,7 +112,8 @@ impl<'a> Line<'a> {
/// ## Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// # use ratatui::text::{Span, Line};
/// # use ratatui::style::{Color, Style, Modifier};
/// let mut line = Line::from(vec![
/// Span::styled("My", Style::default().fg(Color::Yellow)),
/// Span::styled(" text", Style::default().add_modifier(Modifier::BOLD)),
@@ -131,7 +135,10 @@ impl<'a> Line<'a> {
/// ## Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// # use std::borrow::Cow;
/// # use ratatui::layout::Alignment;
/// # use ratatui::text::{Span, Line};
/// # use ratatui::style::{Color, Style, Modifier};
/// let mut line = Line::from("Hi, what's up?");
/// assert_eq!(None, line.alignment);
/// assert_eq!(Some(Alignment::Right), line.alignment(Alignment::Right).alignment)

View File

@@ -13,7 +13,7 @@ use super::Text;
/// # Examples
///
/// ```rust
/// use ratatui::{prelude::*, widgets::*};
/// use ratatui::{buffer::Buffer, layout::Rect, text::Masked, widgets::{Paragraph, Widget}};
///
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
/// let password = Masked::new("12345", 'x');

View File

@@ -19,8 +19,9 @@
//! its `title` property (which is a [`Line`] under the hood):
//!
//! ```rust
//! use ratatui::{prelude::*, widgets::*};
//!
//! # use ratatui::widgets::Block;
//! # use ratatui::text::{Span, Line};
//! # use ratatui::style::{Color, Style};
//! // A simple string with no styling.
//! // Converted to Line(vec![
//! // Span { content: Cow::Borrowed("My title"), style: Style { .. } }

View File

@@ -6,69 +6,22 @@ use unicode_width::UnicodeWidthStr;
use super::StyledGrapheme;
use crate::style::{Style, Styled};
/// Represents a part of a line that is contiguous and where all characters share the same style.
///
/// A `Span` is the smallest unit of text that can be styled. It is usually combined in the [`Line`]
/// type to represent a line of text where each `Span` may have a different style.
///
/// # Examples
///
/// A `Span` with `style` set to [`Style::default()`] can be created from a `&str`, a `String`, or
/// any type convertible to [`Cow<str>`].
///
/// ```rust
/// use ratatui::prelude::*;
///
/// let span = Span::raw("test content");
/// let span = Span::raw(String::from("test content"));
/// let span = Span::from("test content");
/// let span = Span::from(String::from("test content"));
/// let span: Span = "test content".into();
/// let span: Span = String::from("test content").into();
/// ```
///
/// Styled spans can be created using [`Span::styled`] or by converting strings using methods from
/// the [`Stylize`] trait.
///
/// ```rust
/// use ratatui::prelude::*;
///
/// let span = Span::styled("test content", Style::new().green());
/// let span = Span::styled(String::from("test content"), Style::new().green());
/// let span = "test content".green();
/// let span = String::from("test content").green();
/// ```
///
/// `Span` implements [`Stylize`], which allows it to be styled using the shortcut methods. Styles
/// applied are additive.
///
/// ```rust
/// use ratatui::prelude::*;
///
/// let span = Span::raw("test content").green().on_yellow().italic();
/// let span = Span::raw(String::from("test content")).green().on_yellow().italic();
/// ```
///
/// [`Line`]: crate::text::Line
/// [`Stylize`]: crate::style::Stylize
/// [`Cow<str>`]: std::borrow::Cow
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
/// A string where all graphemes have the same style.
#[derive(Debug, Default, Clone, Eq, PartialEq)]
pub struct Span<'a> {
/// The content of the span as a Clone-on-write string.
pub content: Cow<'a, str>,
/// The style of the span.
pub style: Style,
}
impl<'a> Span<'a> {
/// Create a span with the default style.
/// Create a span with no style.
///
/// # Examples
/// ## Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// Span::raw("test content");
/// Span::raw(String::from("test content"));
/// # use ratatui::text::Span;
/// Span::raw("My text");
/// Span::raw(String::from("My text"));
/// ```
pub fn raw<T>(content: T) -> Span<'a>
where
@@ -80,15 +33,16 @@ impl<'a> Span<'a> {
}
}
/// Create a span with the specified style.
/// Create a span with a style.
///
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// let style = Style::new().yellow().on_green().italic();
/// Span::styled("test content", style);
/// Span::styled(String::from("test content"), style);
/// # use ratatui::text::Span;
/// # use ratatui::style::{Color, Modifier, Style};
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
/// Span::styled("My text", style);
/// Span::styled(String::from("My text"), style);
/// ```
pub fn styled<T>(content: T, style: Style) -> Span<'a>
where
@@ -100,31 +54,31 @@ impl<'a> Span<'a> {
}
}
/// Returns the unicode width of the content held by this span.
/// Returns the width of the content held by this span.
pub fn width(&self) -> usize {
self.content.width()
}
/// Returns an iterator over the graphemes held by this span.
///
/// `base_style` is the [`Style`] that will be patched with the `Span`'s `style` to get the
/// resulting [`Style`].
/// `base_style` is the [`Style`] that will be patched with each grapheme [`Style`] to get
/// the resulting [`Style`].
///
/// # Example
/// ## Examples
///
/// ```rust
/// use std::iter::Iterator;
/// use ratatui::{prelude::*, text::StyledGrapheme};
///
/// let span = Span::styled("Test", Style::new().green().italic());
/// let style = Style::new().red().on_yellow();
/// # use ratatui::text::{Span, StyledGrapheme};
/// # use ratatui::style::{Color, Modifier, Style};
/// # use std::iter::Iterator;
/// let span = Span::styled("Text", Style::default().fg(Color::Yellow));
/// let style = Style::default().fg(Color::Green).bg(Color::Black);
/// assert_eq!(
/// span.styled_graphemes(style).collect::<Vec<StyledGrapheme>>(),
/// vec![
/// StyledGrapheme::new("T", Style::new().green().on_yellow().italic()),
/// StyledGrapheme::new("e", Style::new().green().on_yellow().italic()),
/// StyledGrapheme::new("s", Style::new().green().on_yellow().italic()),
/// StyledGrapheme::new("t", Style::new().green().on_yellow().italic()),
/// StyledGrapheme::new("T", Style::default().fg(Color::Yellow).bg(Color::Black)),
/// StyledGrapheme::new("e", Style::default().fg(Color::Yellow).bg(Color::Black)),
/// StyledGrapheme::new("x", Style::default().fg(Color::Yellow).bg(Color::Black)),
/// StyledGrapheme::new("t", Style::default().fg(Color::Yellow).bg(Color::Black)),
/// ],
/// );
/// ```
@@ -132,59 +86,66 @@ impl<'a> Span<'a> {
&'a self,
base_style: Style,
) -> impl Iterator<Item = StyledGrapheme<'a>> {
self.content
.as_ref()
.graphemes(true)
.filter(|g| *g != "\n")
UnicodeSegmentation::graphemes(self.content.as_ref(), true)
.map(move |g| StyledGrapheme {
symbol: g,
style: base_style.patch(self.style),
})
.filter(|s| s.symbol != "\n")
}
/// Patches the style of the Span, adding modifiers from the given style.
/// Patches the style an existing Span, adding modifiers from the given style.
///
/// # Example
/// ## Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// 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());
/// # use ratatui::text::Span;
/// # use ratatui::style::{Color, Style, Modifier};
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
/// let mut raw_span = Span::raw("My text");
/// let mut styled_span = Span::styled("My text", style);
///
/// assert_ne!(raw_span, styled_span);
///
/// raw_span.patch_style(style);
/// assert_eq!(raw_span, styled_span);
/// ```
pub fn patch_style(&mut self, style: Style) {
self.style = self.style.patch(style);
}
/// Resets the style of the Span.
/// Equivalent to calling `patch_style(Style::reset())`.
///
/// This is Equivalent to calling `patch_style(Style::reset())`.
///
/// # Example
/// ## Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// let mut span = Span::styled("Test Content", Style::new().green().on_yellow().italic());
/// # use ratatui::text::Span;
/// # use ratatui::style::{Color, Style, Modifier};
/// let mut span = Span::styled("My text", Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC));
///
/// span.reset_style();
/// assert_eq!(span.style, Style::reset());
/// assert_eq!(Style::reset(), span.style);
/// ```
pub fn reset_style(&mut self) {
self.patch_style(Style::reset());
}
}
impl<'a, T> From<T> for Span<'a>
where
T: Into<Cow<'a, str>>,
{
fn from(s: T) -> Self {
Span::raw(s.into())
impl<'a> From<String> for Span<'a> {
fn from(s: String) -> Span<'a> {
Span::raw(s)
}
}
impl<'a> From<&'a str> for Span<'a> {
fn from(s: &'a str) -> Span<'a> {
Span::raw(s)
}
}
impl<'a> Styled for Span<'a> {
type Item = Span<'a>;
fn style(&self) -> Style {
self.style
}
@@ -194,113 +155,3 @@ impl<'a> Styled for Span<'a> {
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::style::Stylize;
#[test]
fn default() {
let span = Span::default();
assert_eq!(span.content, Cow::Borrowed(""));
assert_eq!(span.style, Style::default());
}
#[test]
fn raw_str() {
let span = Span::raw("test content");
assert_eq!(span.content, Cow::Borrowed("test content"));
assert_eq!(span.style, Style::default());
}
#[test]
fn raw_string() {
let content = String::from("test content");
let span = Span::raw(content.clone());
assert_eq!(span.content, Cow::Owned::<str>(content));
assert_eq!(span.style, Style::default());
}
#[test]
fn styled_str() {
let style = Style::new().red();
let span = Span::styled("test content", style);
assert_eq!(span.content, Cow::Borrowed("test content"));
assert_eq!(span.style, Style::new().red());
}
#[test]
fn styled_string() {
let content = String::from("test content");
let style = Style::new().green();
let span = Span::styled(content.clone(), style);
assert_eq!(span.content, Cow::Owned::<str>(content));
assert_eq!(span.style, style);
}
#[test]
fn from_ref_str_borrowed_cow() {
let content = "test content";
let span = Span::from(content);
assert_eq!(span.content, Cow::Borrowed(content));
assert_eq!(span.style, Style::default());
}
#[test]
fn from_string_ref_str_borrowed_cow() {
let content = String::from("test content");
let span = Span::from(content.as_str());
assert_eq!(span.content, Cow::Borrowed(content.as_str()));
assert_eq!(span.style, Style::default());
}
#[test]
fn from_string_owned_cow() {
let content = String::from("test content");
let span = Span::from(content.clone());
assert_eq!(span.content, Cow::Owned::<str>(content));
assert_eq!(span.style, Style::default());
}
#[test]
fn from_ref_string_borrowed_cow() {
let content = String::from("test content");
let span = Span::from(&content);
assert_eq!(span.content, Cow::Borrowed(content.as_str()));
assert_eq!(span.style, Style::default());
}
#[test]
fn 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 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());
}
#[test]
fn width() {
assert_eq!(Span::raw("").width(), 0);
assert_eq!(Span::raw("test").width(), 4);
assert_eq!(Span::raw("test content").width(), 12);
}
#[test]
fn stylize() {
let span = Span::raw("test content").green();
assert_eq!(span.content, Cow::Borrowed("test content"));
assert_eq!(span.style, Style::new().green());
let span = Span::styled("test content", Style::new().green());
let stylized = span.on_yellow().bold();
assert_eq!(stylized.content, Cow::Borrowed("test content"));
assert_eq!(stylized.style, Style::new().green().on_yellow().bold());
}
}

View File

@@ -9,7 +9,7 @@ use crate::{layout::Alignment, text::Line};
/// future. All methods that accept Spans have been replaced with methods that
/// accept Into<Line<'a>> (which is implemented on `Spans`) to allow users of
/// this crate to gradually transition to Line.
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
#[derive(Debug, Default, Clone, Eq, PartialEq)]
#[deprecated(note = "Use `ratatui::text::Line` instead")]
pub struct Spans<'a>(pub Vec<Span<'a>>);
@@ -19,8 +19,8 @@ impl<'a> Spans<'a> {
/// ## Examples
///
/// ```rust
/// use ratatui::{prelude::*, text::Spans};
///
/// # use ratatui::text::{Span, Spans};
/// # use ratatui::style::{Color, Style};
/// let spans = Spans::from(vec![
/// Span::styled("My", Style::default().fg(Color::Yellow)),
/// Span::raw(" text"),
@@ -36,8 +36,8 @@ impl<'a> Spans<'a> {
/// ## Examples
///
/// ```rust
/// use ratatui::{prelude::*, text::Spans};
///
/// # use ratatui::text::{Span, Spans};
/// # use ratatui::style::{Color, Style, Modifier};
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
/// let mut raw_spans = Spans::from(vec![
/// Span::raw("My"),
@@ -65,8 +65,8 @@ impl<'a> Spans<'a> {
/// ## Examples
///
/// ```rust
/// use ratatui::{prelude::*, text::Spans};
///
/// # use ratatui::text::{Span, Spans};
/// # use ratatui::style::{Color, Style, Modifier};
/// let mut spans = Spans::from(vec![
/// Span::styled("My", Style::default().fg(Color::Yellow)),
/// Span::styled(" text", Style::default().add_modifier(Modifier::BOLD)),
@@ -88,8 +88,10 @@ impl<'a> Spans<'a> {
/// ## Examples
///
/// ```rust
/// use ratatui::{prelude::*, text::Spans};
///
/// # use std::borrow::Cow;
/// # use ratatui::layout::Alignment;
/// # use ratatui::text::{Span, Spans};
/// # use ratatui::style::{Color, Style, Modifier};
/// let mut line = Spans::from("Hi, what's up?").alignment(Alignment::Right);
/// assert_eq!(Some(Alignment::Right), line.alignment)
/// ```

View File

@@ -12,8 +12,8 @@ use crate::style::Style;
/// [`core::iter::Extend`] which enables the concatenation of several [`Text`] blocks.
///
/// ```rust
/// use ratatui::prelude::*;
///
/// # use ratatui::text::Text;
/// # use ratatui::style::{Color, Modifier, Style};
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
///
/// // An initial two lines of `Text` built from a `&str`
@@ -28,7 +28,7 @@ use crate::style::Style;
/// text.extend(Text::styled("Some more lines\nnow with more style!", style));
/// assert_eq!(6, text.height());
/// ```
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
#[derive(Debug, Default, Clone, Eq, PartialEq)]
pub struct Text<'a> {
pub lines: Vec<Line<'a>>,
}
@@ -39,7 +39,7 @@ impl<'a> Text<'a> {
/// ## Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// # use ratatui::text::Text;
/// Text::raw("The first line\nThe second line");
/// Text::raw(String::from("The first line\nThe second line"));
/// ```
@@ -62,7 +62,8 @@ impl<'a> Text<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// # use ratatui::text::Text;
/// # use ratatui::style::{Color, Modifier, Style};
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
/// Text::styled("The first line\nThe second line", style);
/// Text::styled(String::from("The first line\nThe second line"), style);
@@ -81,7 +82,7 @@ impl<'a> Text<'a> {
/// ## Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::text::Text;
/// let text = Text::from("The first line\nThe second line");
/// assert_eq!(15, text.width());
/// ```
@@ -94,7 +95,7 @@ impl<'a> Text<'a> {
/// ## Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::text::Text;
/// let text = Text::from("The first line\nThe second line");
/// assert_eq!(2, text.height());
/// ```
@@ -107,7 +108,8 @@ impl<'a> Text<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// # use ratatui::text::Text;
/// # use ratatui::style::{Color, Modifier, Style};
/// 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);
@@ -128,7 +130,8 @@ impl<'a> Text<'a> {
/// ## Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// # use ratatui::text::{Span, Line, Text};
/// # use ratatui::style::{Color, Style, Modifier};
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
/// let mut text = Text::styled("The first line\nThe second line", style);
///
@@ -220,223 +223,3 @@ where
self.lines.extend(lines);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::style::Stylize;
#[test]
fn raw() {
let text = Text::raw("The first line\nThe second line");
assert_eq!(
text.lines,
vec![Line::from("The first line"), Line::from("The second line")]
);
}
#[test]
fn styled() {
let style = Style::new().yellow().italic();
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]
fn width() {
let text = Text::from("The first line\nThe second line");
assert_eq!(15, text.width());
}
#[test]
fn height() {
let text = Text::from("The first line\nThe second line");
assert_eq!(2, text.height());
}
#[test]
fn patch_style() {
let style = Style::new().yellow().italic();
let style2 = Style::new().red().underlined();
let mut text = Text::styled("The first line\nThe second line", style);
text.patch_style(style2);
let expected_style = Style::new().red().italic().underlined();
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 mut text = Text::styled("The first line\nThe second line", style);
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]
fn from_string() {
let text = Text::from(String::from("The first line\nThe second line"));
assert_eq!(
text.lines,
vec![Line::from("The first line"), Line::from("The second line")]
);
}
#[test]
fn from_str() {
let text = Text::from("The first line\nThe second line");
assert_eq!(
text.lines,
vec![Line::from("The first line"), Line::from("The second line")]
);
}
#[test]
fn from_cow() {
let text = Text::from(Cow::Borrowed("The first line\nThe second line"));
assert_eq!(
text.lines,
vec![Line::from("The first line"), Line::from("The second line")]
);
}
#[test]
fn from_span() {
let style = Style::new().yellow().italic();
let text = Text::from(Span::styled("The first line\nThe second line", style));
assert_eq!(
text.lines,
vec![Line::from(Span::styled(
"The first line\nThe second line",
style
))]
);
}
#[test]
#[allow(deprecated)]
fn from_spans() {
let style = Style::new().yellow().italic();
let text = Text::from(Spans::from(vec![
Span::styled("The first line", style),
Span::styled("The second line", style),
]));
assert_eq!(
text.lines,
vec![Line::from(Spans::from(vec![
Span::styled("The first line", style),
Span::styled("The second line", style),
]))]
);
}
#[test]
fn from_line() {
let text = Text::from(Line::from("The first line"));
assert_eq!(text.lines, vec![Line::from("The first line")]);
}
#[test]
#[allow(deprecated)]
fn from_vec_spans() {
let text = Text::from(vec![
Spans::from("The first line"),
Spans::from("The second line"),
]);
assert_eq!(
text.lines,
vec![Line::from("The first line"), Line::from("The second line"),]
);
}
#[test]
fn from_vec_line() {
let text = Text::from(vec![
Line::from("The first line"),
Line::from("The second line"),
]);
assert_eq!(
text.lines,
vec![Line::from("The first line"), Line::from("The second line")]
);
}
#[test]
fn into_iter() {
let text = Text::from("The first line\nThe second line");
let mut iter = text.into_iter();
assert_eq!(iter.next(), Some(Line::from("The first line")));
assert_eq!(iter.next(), Some(Line::from("The second line")));
assert_eq!(iter.next(), None);
}
#[test]
fn extend() {
let mut text = Text::from("The first line\nThe second line");
text.extend(vec![
Line::from("The third line"),
Line::from("The fourth line"),
]);
assert_eq!(
text.lines,
vec![
Line::from("The first line"),
Line::from("The second line"),
Line::from("The third line"),
Line::from("The fourth line"),
]
);
}
#[test]
fn extend_from_iter() {
let mut text = Text::from("The first line\nThe second line");
text.extend(vec![
Line::from("The third line"),
Line::from("The fourth line"),
]);
assert_eq!(
text.lines,
vec![
Line::from("The first line"),
Line::from("The second line"),
Line::from("The third line"),
Line::from("The fourth line"),
]
);
}
#[test]
fn extend_from_iter_str() {
let mut text = Text::from("The first line\nThe second line");
text.extend(vec!["The third line", "The fourth line"]);
assert_eq!(
text.lines,
vec![
Line::from("The first line"),
Line::from("The second line"),
Line::from("The third line"),
Line::from("The fourth line"),
]
);
}
}

View File

@@ -1,93 +1,23 @@
//! This module holds the [`Title`] element and its related configuration types.
//! A title is a piece of [`Block`](crate::widgets::Block) configuration.
use strum::{Display, EnumString};
use crate::{layout::Alignment, text::Line};
/// A [`Block`](crate::widgets::Block) title.
///
/// It can be aligned (see [`Alignment`]) and positioned (see [`Position`]).
///
/// # Example
///
/// Title with no style.
/// ```
/// use ratatui::widgets::block::Title;
///
/// Title::from("Title");
/// ```
///
/// Blue title on a white background (via [`Stylize`](crate::style::Stylize) trait).
/// ```
/// use ratatui::{prelude::*, widgets::block::*};
///
/// Title::from("Title".blue().on_white());
/// ```
///
/// Title with multiple styles (see [`Line`] and [`Stylize`](crate::style::Stylize)).
/// ```
/// use ratatui::{prelude::*, widgets::block::*};
///
/// Title::from(
/// Line::from(vec!["Q".white().underlined(), "uit".gray()])
/// );
/// ```
///
/// Complete example
/// ```
/// use ratatui::{prelude::*, widgets::{*, block::*}};
///
/// Title::from("Title")
/// .position(Position::Top)
/// .alignment(Alignment::Right);
/// ```
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
#[derive(Debug, Default, Clone, Eq, PartialEq)]
pub struct Title<'a> {
/// Title content
pub content: Line<'a>,
/// Title alignment
///
/// If [`None`], defaults to the alignment defined with
/// [`Block::title_alignment`](crate::widgets::Block::title_alignment) in the associated
/// [`Block`](crate::widgets::Block).
/// Defaults to Left if unset
pub alignment: Option<Alignment>,
/// Title position
///
/// If [`None`], defaults to the position defined with
/// [`Block::title_position`](crate::widgets::Block::title_position) in the associated
/// [`Block`](crate::widgets::Block).
/// Defaults to Top if unset
pub position: Option<Position>,
}
/// Defines the [title](crate::widgets::block::Title) position.
///
/// The title can be positioned on top or at the bottom of the block.
/// Defaults to [`Position::Top`].
///
/// # Example
///
/// ```
/// use ratatui::widgets::{*, block::*};
///
/// Block::new().title(
/// Title::from("title").position(Position::Bottom)
/// );
/// ```
#[derive(Debug, Default, Display, EnumString, Clone, Copy, PartialEq, Eq, Hash)]
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Position {
/// Position the title at the top of the block.
///
/// This is the default.
#[default]
Top,
/// Position the title at the bottom of the block.
Bottom,
}
impl<'a> Title<'a> {
/// Builder pattern method for setting the title content.
pub fn content<T>(mut self, content: T) -> Title<'a>
where
T: Into<Line<'a>>,
@@ -96,13 +26,11 @@ impl<'a> Title<'a> {
self
}
/// Builder pattern method for setting the title alignment.
pub fn alignment(mut self, alignment: Alignment) -> Title<'a> {
self.alignment = Some(alignment);
self
}
/// Builder pattern method for setting the title position.
pub fn position(mut self, position: Position) -> Title<'a> {
self.position = Some(position);
self
@@ -117,23 +45,3 @@ where
Self::default().content(value.into())
}
}
#[cfg(test)]
mod tests {
use strum::ParseError;
use super::*;
#[test]
fn position_tostring() {
assert_eq!(Position::Top.to_string(), "Top");
assert_eq!(Position::Bottom.to_string(), "Bottom");
}
#[test]
fn position_from_str() {
assert_eq!("Top".parse::<Position>(), Ok(Position::Top));
assert_eq!("Bottom".parse::<Position>(), Ok(Position::Bottom));
assert_eq!("".parse::<Position>(), Err(ParseError::VariantNotFound));
}
}

View File

@@ -1,22 +1,13 @@
use crate::{buffer::Buffer, prelude::Rect, style::Style, text::Line};
use crate::{buffer::Buffer, style::Style, text::Line};
/// A bar to be shown by the [`BarChart`](crate::widgets::BarChart) widget.
/// represent a bar to be shown by the Barchart
///
/// # Examples
/// the following example creates a bar with the label "Bar 1", a value "10",
/// red background and a white value foreground
///
/// Here is an explanation of a `Bar`'s components.
/// ```plain
/// ███ ┐
/// █2█ <- text_value or value │ bar
/// foo <- label ┘
/// ```
/// Note that every element can be styled individually.
///
/// # Example
///
/// The following example creates a bar with the label "Bar 1", a value "10",
/// red background and a white value foreground.
/// ```
/// use ratatui::{prelude::*, widgets::*};
///
/// # use ratatui::{prelude::*, widgets::*};
/// Bar::default()
/// .label("Bar 1".into())
/// .value(10)
@@ -24,7 +15,7 @@ use crate::{buffer::Buffer, prelude::Rect, style::Style, text::Line};
/// .value_style(Style::default().bg(Color::Red).fg(Color::White))
/// .text_value("10°C".to_string());
/// ```
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
#[derive(Debug, Default, Clone)]
pub struct Bar<'a> {
/// Value to display on the bar (computed when the data is passed to the widget)
pub(super) value: u64,
@@ -39,104 +30,32 @@ pub struct Bar<'a> {
}
impl<'a> Bar<'a> {
/// Set the value of this bar.
///
/// The value will be displayed inside the bar.
///
/// # See also
///
/// [`Bar::value_style`] to style the value.
/// [`Bar::text_value`] to set the displayed value.
pub fn value(mut self, value: u64) -> Bar<'a> {
self.value = value;
self
}
/// Set the label of the bar.
///
/// For [`Vertical`](crate::layout::Direction::Vertical) bars,
/// display the label **under** the bar.
/// For [`Horizontal`](crate::layout::Direction::Horizontal) bars,
/// display the label **in** the bar.
/// See [`BarChart::direction`](crate::widgets::BarChart::direction) to set the direction.
pub fn label(mut self, label: Line<'a>) -> Bar<'a> {
self.label = Some(label);
self
}
/// Set the style of the bar.
///
/// This will apply to every non-styled element.
/// It can be seen and used as a default value.
pub fn style(mut self, style: Style) -> Bar<'a> {
self.style = style;
self
}
/// Set the style of the value.
///
/// # See also
///
/// [`Bar::value`] to set the value.
pub fn value_style(mut self, style: Style) -> Bar<'a> {
self.value_style = style;
self
}
/// Set the text value printed in the bar.
///
/// If `text_value` is not set, then the [ToString] representation of `value` will be shown on
/// the bar.
///
/// # See also
///
/// [`Bar::value`] to set the value.
/// set the text value printed in the bar. (By default self.value is printed)
pub fn text_value(mut self, text_value: String) -> Bar<'a> {
self.text_value = Some(text_value);
self
}
/// Render the value of the bar.
///
/// [`text_value`](Bar::text_value) is used if set, otherwise the value is converted to string.
/// The value is rendered using value_style. If the value width is greater than the
/// bar width, then the value is split into 2 parts. the first part is rendered in the bar
/// using value_style. The second part is rendered outside the bar using bar_style
pub(super) fn render_value_with_different_styles(
self,
buf: &mut Buffer,
area: Rect,
bar_length: usize,
default_value_style: Style,
bar_style: Style,
) {
let text = if let Some(text) = self.text_value {
text
} else {
self.value.to_string()
};
if !text.is_empty() {
let style = default_value_style.patch(self.value_style);
// Since the value may be longer than the bar itself, we need to use 2 different styles
// while rendering. Render the first part with the default value style
buf.set_stringn(area.x, area.y, &text, bar_length, style);
// render the second part with the bar_style
if text.len() > bar_length {
let (first, second) = text.split_at(bar_length);
let style = bar_style.patch(self.style);
buf.set_stringn(
area.x + first.len() as u16,
area.y,
second,
area.width as usize - first.len(),
style,
);
}
}
}
pub(super) fn render_label_and_value(
self,
buf: &mut Buffer,
@@ -160,18 +79,14 @@ impl<'a> Bar<'a> {
x + (max_width.saturating_sub(value_label.len() as u16) >> 1),
y,
value_label,
default_value_style.patch(self.value_style),
self.value_style.patch(default_value_style),
);
}
}
// render the label
if let Some(mut label) = self.label {
// patch label styles
for span in &mut label.spans {
span.style = default_label_style.patch(span.style);
}
label.patch_style(default_label_style);
buf.set_line(
x + (max_width.saturating_sub(label.width() as u16) >> 1),
y + 1,

View File

@@ -1,22 +1,16 @@
use super::Bar;
use crate::{
prelude::{Alignment, Buffer, Rect},
style::Style,
text::Line,
};
use crate::text::Line;
/// A group of bars to be shown by the Barchart.
/// represent a group of bars to be shown by the Barchart
///
/// # Examples
///
/// ```
/// use ratatui::{prelude::*, widgets::*};
///
/// # use ratatui::{prelude::*, widgets::*};
/// BarGroup::default()
/// .label("Group 1".into())
/// .bars(&[Bar::default().value(200), Bar::default().value(150)]);
/// ```
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
#[derive(Debug, Default, Clone)]
pub struct BarGroup<'a> {
/// label of the group. It will be printed centered under this group of bars
pub(super) label: Option<Line<'a>>,
@@ -37,27 +31,10 @@ impl<'a> BarGroup<'a> {
self
}
/// The maximum bar value of this group
/// return the maximum bar value of this group
pub(super) fn max(&self) -> Option<u64> {
self.bars.iter().max_by_key(|v| v.value).map(|v| v.value)
}
pub(super) fn render_label(self, buf: &mut Buffer, area: Rect, default_label_style: Style) {
if let Some(mut label) = self.label {
// patch label styles
for span in &mut label.spans {
span.style = default_label_style.patch(span.style);
}
let x_offset = match label.alignment {
Some(Alignment::Center) => area.width.saturating_sub(label.width() as u16) >> 1,
Some(Alignment::Right) => area.width.saturating_sub(label.width() as u16),
_ => 0,
};
buf.set_line(area.x + x_offset, area.y, &label, area.width);
}
}
}
impl<'a> From<&[(&'a str, u64)]> for BarGroup<'a> {

View File

@@ -1,4 +1,3 @@
#![warn(missing_docs)]
use crate::prelude::*;
mod bar;
@@ -9,41 +8,14 @@ pub use bar_group::BarGroup;
use super::{Block, Widget};
/// A chart showing values as [bars](Bar).
///
/// Here is a possible `BarChart` output.
/// ```plain
/// ┌─────────────────────────────────┐
/// │ ████│
/// │ ▅▅▅▅ ████│
/// │ ▇▇▇▇ ████ ████│
/// │ ▄▄▄▄ ████ ████ ████ ████│
/// │▆10▆ █20█ █50█ █40█ █60█ █90█│
/// │ B1 B2 B1 B2 B1 B2 │
/// │ Group1 Group2 Group3 │
/// └─────────────────────────────────┘
/// ```
///
/// A `BarChart` is composed of a set of [`Bar`] which can be set via [`BarChart::data`].
/// Bars can be styled globally ([`BarChart::bar_style`]) or individually ([`Bar::style`]).
/// There are other methods available to style even more precisely. See [`Bar`] to find out about
/// each bar component.
///
/// The `BarChart` widget can also show groups of bars via [`BarGroup`].
/// A [`BarGroup`] is a set of [`Bar`], multiple can be added to a `BarChart` using
/// [`BarChart::data`] multiple time as demonstrated in the example below.
///
/// The chart can have a [`Direction`] (by default the bars are [`Vertical`](Direction::Vertical)).
/// This is set using [`BarChart::direction`].
/// Display multiple bars in a single widgets
///
/// # Examples
///
/// The following example creates a `BarChart` with two groups of bars.
/// The first group is added by an array slice (`&[(&str, u64)]`).
/// The second group is added by a [`BarGroup`] instance.
/// The following example creates a BarChart with two groups of bars.
/// The first group is added by an array slice (&[(&str, u64)]).
/// The second group is added by a slice of Groups (&[BarGroup]).
/// ```
/// use ratatui::{prelude::*, widgets::*};
///
/// # use ratatui::{prelude::*, widgets::*};
/// BarChart::default()
/// .block(Block::default().title("BarChart").borders(Borders::ALL))
/// .bar_width(3)
@@ -56,7 +28,7 @@ use super::{Block, Widget};
/// .data(BarGroup::default().bars(&[Bar::default().value(10), Bar::default().value(20)]))
/// .max(4);
/// ```
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
#[derive(Debug, Clone)]
pub struct BarChart<'a> {
/// Block to wrap the widget in
block: Option<Block<'a>>,
@@ -81,8 +53,6 @@ pub struct BarChart<'a> {
/// Value necessary for a bar to reach the maximum height (if no value is specified,
/// the maximum value in the data is taken as reference)
max: Option<u64>,
/// direction of the bars
direction: Direction,
}
impl<'a> Default for BarChart<'a> {
@@ -99,24 +69,22 @@ impl<'a> Default for BarChart<'a> {
group_gap: 0,
bar_set: symbols::bar::NINE_LEVELS,
style: Style::default(),
direction: Direction::Vertical,
}
}
}
impl<'a> BarChart<'a> {
/// Add group of bars to the BarChart
///
/// # Examples
///
/// The following example creates a BarChart with two groups of bars.
/// The first group is added by an array slice (`&[(&str, u64)]`).
/// The second group is added by a [`BarGroup`] instance.
/// The following example creates a BarChart with two groups of bars.
/// The first group is added by an array slice (&[(&str, u64)]).
/// The second group is added by a BarGroup instance.
/// ```
/// # use ratatui::{prelude::*, widgets::*};
///
/// BarChart::default()
/// .data(&[("B0", 0), ("B1", 2), ("B2", 4), ("B3", 3)])
/// .data(BarGroup::default().bars(&[Bar::default().value(10), Bar::default().value(20)]));
/// .data(&[("B0", 0), ("B1", 2), ("B2", 4), ("B3", 3)])
/// .data(BarGroup::default().bars(&[Bar::default().value(10), Bar::default().value(20)]));
/// ```
pub fn data(mut self, data: impl Into<BarGroup<'a>>) -> BarChart<'a> {
let group: BarGroup = data.into();
@@ -126,161 +94,55 @@ impl<'a> BarChart<'a> {
self
}
/// Surround the [`BarChart`] with a [`Block`].
pub fn block(mut self, block: Block<'a>) -> BarChart<'a> {
self.block = Some(block);
self
}
/// Set the value necessary for a [`Bar`] to reach the maximum height.
///
/// If not set, the maximum value in the data is taken as reference.
///
/// # Examples
///
/// This example shows the default behavior when `max` is not set.
/// The maximum value in the dataset is taken (here, `100`).
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// BarChart::default().data(&[("foo", 1), ("bar", 2), ("baz", 100)]);
/// // Renders
/// // █
/// // █
/// // f b b
/// ```
///
/// This example shows a custom max value.
/// The maximum height being `2`, `bar` & `baz` render as the max.
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// BarChart::default()
/// .data(&[("foo", 1), ("bar", 2), ("baz", 100)])
/// .max(2);
/// // Renders
/// // █ █
/// // █ █ █
/// // f b b
/// ```
pub fn max(mut self, max: u64) -> BarChart<'a> {
self.max = Some(max);
self
}
/// Set the default style of the bar.
///
/// It is also possible to set individually the style of each [`Bar`].
/// In this case the default style will be patched by the individual style
pub fn bar_style(mut self, style: Style) -> BarChart<'a> {
self.bar_style = style;
self
}
/// Set the width of the displayed bars.
///
/// For [`Horizontal`](crate::layout::Direction::Horizontal) bars this becomes the height of
/// the bar.
///
/// If not set, this defaults to `1`.
/// The bar label also uses this value as its width.
pub fn bar_width(mut self, width: u16) -> BarChart<'a> {
self.bar_width = width;
self
}
/// Set the gap between each bar.
///
/// If not set, this defaults to `1`.
/// The bar label will never be larger than the bar itself, even if the gap is sufficient.
///
/// # Example
///
/// This shows two bars with a gap of `3`. Notice the labels will always stay under the bar.
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// BarChart::default()
/// .data(&[("foo", 1), ("bar", 2)])
/// .bar_gap(3);
/// // Renders
/// // █
/// // █ █
/// // f b
/// ```
pub fn bar_gap(mut self, gap: u16) -> BarChart<'a> {
self.bar_gap = gap;
self
}
/// The [`bar::Set`](crate::symbols::bar::Set) to use for displaying the bars.
///
/// If not set, the default is [`bar::NINE_LEVELS`](crate::symbols::bar::NINE_LEVELS).
pub fn bar_set(mut self, bar_set: symbols::bar::Set) -> BarChart<'a> {
self.bar_set = bar_set;
self
}
/// Set the default value style of the bar.
///
/// It is also possible to set individually the value style of each [`Bar`].
/// In this case the default value style will be patched by the individual value style
///
/// # See also
///
/// [Bar::value_style] to set the value style individually.
pub fn value_style(mut self, style: Style) -> BarChart<'a> {
self.value_style = style;
self
}
/// Set the default label style of the groups and bars.
///
/// It is also possible to set individually the label style of each [`Bar`] or [`BarGroup`].
/// In this case the default label style will be patched by the individual label style
///
/// # See also
///
/// [Bar::label] to set the label style individually.
pub fn label_style(mut self, style: Style) -> BarChart<'a> {
self.label_style = style;
self
}
/// Set the gap between [`BarGroup`].
pub fn group_gap(mut self, gap: u16) -> BarChart<'a> {
self.group_gap = gap;
self
}
/// Set the style of the entire chart.
///
/// The style will be applied to everything that isn't styled (borders, bars, labels, ...).
pub fn style(mut self, style: Style) -> BarChart<'a> {
self.style = style;
self
}
/// Set the direction of the bars.
///
/// [`Vertical`](crate::layout::Direction::Vertical) bars are the default.
///
/// # Examples
///
/// Vertical bars
/// ```plain
/// █
/// █ █
/// f b
/// ```
///
/// Horizontal bars
/// ```plain
/// █foo██
///
/// █bar██
/// ```
pub fn direction(mut self, direction: Direction) -> BarChart<'a> {
self.direction = direction;
self
}
}
impl<'a> BarChart<'a> {
@@ -334,72 +196,7 @@ impl<'a> BarChart<'a> {
}
}
fn render_horizontal_bars(self, buf: &mut Buffer, bars_area: Rect, max: u64) {
// convert the bar values to ratatui::symbols::bar::Set
let groups: Vec<Vec<u16>> = self
.data
.iter()
.map(|group| {
group
.bars
.iter()
.map(|bar| (bar.value * u64::from(bars_area.width) / max) as u16)
.collect()
})
.collect();
// print all visible bars
let mut bar_y = bars_area.top();
for (group_data, mut group) in groups.into_iter().zip(self.data) {
let bars = std::mem::take(&mut group.bars);
for (bar_length, bar) in group_data.into_iter().zip(bars) {
let bar_style = self.bar_style.patch(bar.style);
for y in 0..self.bar_width {
let bar_y = bar_y + y;
for x in 0..bars_area.width {
let symbol = if x < bar_length {
self.bar_set.full
} else {
self.bar_set.empty
};
buf.get_mut(bars_area.left() + x, bar_y)
.set_symbol(symbol)
.set_style(bar_style);
}
}
let bar_value_area = Rect {
y: bar_y + (self.bar_width >> 1),
..bars_area
};
bar.render_value_with_different_styles(
buf,
bar_value_area,
bar_length as usize,
self.value_style,
self.bar_style,
);
bar_y += self.bar_gap + self.bar_width;
}
// if group_gap is zero, then there is no place to print the group label
// check also if the group label is still inside the visible area
let label_y = bar_y - self.bar_gap;
if self.group_gap > 0 && label_y < bars_area.bottom() {
let label_rect = Rect {
y: label_y,
..bars_area
};
group.render_label(buf, label_rect, self.label_style);
bar_y += self.group_gap;
}
}
}
fn render_vertical_bars(&self, buf: &mut Buffer, bars_area: Rect, max: u64) {
fn render_bars(&self, buf: &mut Buffer, bars_area: Rect, max: u64) {
// convert the bar values to ratatui::symbols::bar::Set
let mut groups: Vec<Vec<u64>> = self
.data
@@ -430,7 +227,7 @@ impl<'a> BarChart<'a> {
_ => self.bar_set.full,
};
let bar_style = self.bar_style.patch(bar.style);
let bar_style = bar.style.patch(self.bar_style);
for x in 0..self.bar_width {
buf.get_mut(bar_x + x, bars_area.top() + j)
@@ -467,24 +264,23 @@ impl<'a> BarChart<'a> {
// print labels and values in one go
let mut bar_x = area.left();
let bar_y = area.bottom() - label_height - 1;
for mut group in self.data.into_iter() {
if group.bars.is_empty() {
continue;
}
let bars = std::mem::take(&mut group.bars);
for group in self.data.into_iter() {
// print group labels under the bars or the previous labels
let label_max_width =
bars.len() as u16 * (self.bar_width + self.bar_gap) - self.bar_gap;
let group_area = Rect {
x: bar_x,
y: area.bottom() - 1,
width: label_max_width,
height: 1,
};
group.render_label(buf, group_area, self.label_style);
if let Some(mut label) = group.label {
label.patch_style(self.label_style);
let label_max_width = group.bars.len() as u16 * self.bar_width
+ (group.bars.len() as u16 - 1) * self.bar_gap;
buf.set_line(
bar_x + (label_max_width.saturating_sub(label.width() as u16) >> 1),
area.bottom() - 1,
&label,
label_max_width,
);
}
// print the bar values and numbers
for bar in bars.into_iter() {
for bar in group.bars.into_iter() {
bar.render_label_and_value(
buf,
self.bar_width,
@@ -518,23 +314,16 @@ impl<'a> Widget for BarChart<'a> {
let max = self.maximum_data_value();
match self.direction {
Direction::Horizontal => {
// remove invisible groups and bars, since we don't need to print them
self.remove_invisible_groups_and_bars(area.height);
self.render_horizontal_bars(buf, area, max);
}
Direction::Vertical => {
// remove invisible groups and bars, since we don't need to print them
self.remove_invisible_groups_and_bars(area.width);
let bars_area = Rect {
height: area.height - label_height,
..area
};
self.render_vertical_bars(buf, bars_area, max);
self.render_labels_and_values(area, buf, label_height);
}
}
// remove invisible groups and bars, since we don't need to print them
self.remove_invisible_groups_and_bars(area.width);
let bars_area = Rect {
height: area.height - label_height,
..area
};
self.render_bars(buf, bars_area, max);
self.render_labels_and_values(area, buf, label_height);
}
}
@@ -825,7 +614,7 @@ mod tests {
let expected = Buffer::with_lines(vec![
"█ █ █ █ ",
"█ █ █ █ █ █ █ █ █",
"G1 G2 G3 ",
" G1 G2 G3 ",
]);
assert_buffer_eq!(buffer, expected);
@@ -848,7 +637,7 @@ mod tests {
let expected = Buffer::with_lines(vec![
"█ █ █ ",
"█ █ █ █ █ █ █",
"G1 G2 G",
" G1 G2 G",
]);
assert_buffer_eq!(buffer, expected);
}
@@ -870,7 +659,7 @@ mod tests {
let expected = Buffer::with_lines(vec![
"█ █ █ ",
"█ █ █ █ █ █ ",
"G1 G2 ",
" G1 G2 ",
]);
assert_buffer_eq!(buffer, expected);
}
@@ -964,189 +753,7 @@ mod tests {
let mut buffer = Buffer::empty(Rect::new(0, 0, 3, 3));
chart.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines(vec!["", "█ █", "G "]);
assert_buffer_eq!(buffer, expected);
}
fn build_test_barchart<'a>() -> BarChart<'a> {
BarChart::default()
.data(BarGroup::default().label("G1".into()).bars(&[
Bar::default().value(2),
Bar::default().value(3),
Bar::default().value(4),
]))
.data(BarGroup::default().label("G2".into()).bars(&[
Bar::default().value(3),
Bar::default().value(4),
Bar::default().value(5),
]))
.group_gap(1)
.direction(Direction::Horizontal)
.bar_gap(0)
}
#[test]
fn test_horizontal_bars() {
let chart: BarChart<'_> = build_test_barchart();
let mut buffer = Buffer::empty(Rect::new(0, 0, 5, 8));
chart.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines(vec![
"2█ ",
"3██ ",
"4███ ",
"G1 ",
"3██ ",
"4███ ",
"5████",
"G2 ",
]);
assert_buffer_eq!(buffer, expected);
}
#[test]
fn test_horizontal_bars_no_space_for_group_label() {
let chart: BarChart<'_> = build_test_barchart();
let mut buffer = Buffer::empty(Rect::new(0, 0, 5, 7));
chart.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines(vec![
"2█ ",
"3██ ",
"4███ ",
"G1 ",
"3██ ",
"4███ ",
"5████",
]);
assert_buffer_eq!(buffer, expected);
}
#[test]
fn test_horizontal_bars_no_space_for_all_bars() {
let chart: BarChart<'_> = build_test_barchart();
let mut buffer = Buffer::empty(Rect::new(0, 0, 5, 5));
chart.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines(vec!["2█ ", "3██ ", "4███ ", "G1 ", "3██ "]);
assert_buffer_eq!(buffer, expected);
}
fn test_horizontal_bars_label_width_greater_than_bar(bar_color: Option<Color>) {
let mut bar = Bar::default()
.value(2)
.text_value("label".into())
.value_style(Style::default().red());
if let Some(color) = bar_color {
bar = bar.style(Style::default().fg(color));
}
let chart: BarChart<'_> = BarChart::default()
.data(BarGroup::default().bars(&[bar, Bar::default().value(5)]))
.direction(Direction::Horizontal)
.bar_style(Style::default().yellow())
.value_style(Style::default().italic())
.bar_gap(0);
let mut buffer = Buffer::empty(Rect::new(0, 0, 5, 2));
chart.render(buffer.area, &mut buffer);
let mut expected = Buffer::with_lines(vec!["label", "5████"]);
// first line has a yellow foreground. first cell contains italic "5"
expected.get_mut(0, 1).modifier.insert(Modifier::ITALIC);
for x in 0..5 {
expected.get_mut(x, 1).set_fg(Color::Yellow);
}
let expected_color = if let Some(color) = bar_color {
color
} else {
Color::Yellow
};
// second line contains the word "label". Since the bar value is 2,
// then the first 2 characters of "label" are italic red.
// the rest is white (using the Bar's style).
let cell = expected.get_mut(0, 0).set_fg(Color::Red);
cell.modifier.insert(Modifier::ITALIC);
let cell = expected.get_mut(1, 0).set_fg(Color::Red);
cell.modifier.insert(Modifier::ITALIC);
expected.get_mut(2, 0).set_fg(expected_color);
expected.get_mut(3, 0).set_fg(expected_color);
expected.get_mut(4, 0).set_fg(expected_color);
assert_buffer_eq!(buffer, expected);
}
#[test]
fn test_horizontal_bars_label_width_greater_than_bar_without_style() {
test_horizontal_bars_label_width_greater_than_bar(None);
}
#[test]
fn test_horizontal_bars_label_width_greater_than_bar_with_style() {
test_horizontal_bars_label_width_greater_than_bar(Some(Color::White))
}
#[test]
fn test_group_label_style() {
let chart: BarChart<'_> = BarChart::default()
.data(
BarGroup::default()
.label(Span::from("G1").red().into())
.bars(&[Bar::default().value(2)]),
)
.group_gap(1)
.direction(Direction::Horizontal)
.label_style(Style::default().bold().yellow());
let mut buffer = Buffer::empty(Rect::new(0, 0, 5, 2));
chart.render(buffer.area, &mut buffer);
// G1 should have the bold red style
// bold: because of BarChart::label_style
// red: is included with the label itself
let mut expected = Buffer::with_lines(vec!["2████", "G1 "]);
let cell = expected.get_mut(0, 1).set_fg(Color::Red);
cell.modifier.insert(Modifier::BOLD);
let cell = expected.get_mut(1, 1).set_fg(Color::Red);
cell.modifier.insert(Modifier::BOLD);
assert_buffer_eq!(buffer, expected);
}
#[test]
fn test_group_label_center() {
let chart: BarChart<'_> = BarChart::default().data(
BarGroup::default()
.label(Line::from(Span::from("G")).alignment(Alignment::Center))
.bars(&[Bar::default().value(2), Bar::default().value(5)]),
);
let mut buffer = Buffer::empty(Rect::new(0, 0, 3, 3));
chart.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines(vec!["", "▆ █", " G "]);
assert_buffer_eq!(buffer, expected);
}
#[test]
fn test_group_label_right() {
let chart: BarChart<'_> = BarChart::default().data(
BarGroup::default()
.label(Line::from(Span::from("G")).alignment(Alignment::Right))
.bars(&[Bar::default().value(2), Bar::default().value(5)]),
);
let mut buffer = Buffer::empty(Rect::new(0, 0, 3, 3));
chart.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines(vec!["", "▆ █", " G"]);
let expected = Buffer::with_lines(vec!["", "█ █", " G "]);
assert_buffer_eq!(buffer, expected);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -21,7 +21,7 @@ use crate::{
};
/// Display a month calendar for the month containing `display_date`
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
#[derive(Debug, Clone)]
pub struct Monthly<'a, S: DateStyler> {
display_date: Date,
events: S,
@@ -72,7 +72,7 @@ impl<'a, S: DateStyler> Monthly<'a, S> {
self
}
/// Render the calendar within a [Block]
/// Render the calendar within a [Block](crate::widgets::Block)
pub fn block(mut self, b: Block<'a>) -> Self {
self.block = Some(b);
self

View File

@@ -4,7 +4,7 @@ use crate::{
};
/// Shape to draw a circle with a given center and radius and with the given color
#[derive(Debug, Default, Clone, PartialEq)]
#[derive(Debug, Default, Clone)]
pub struct Circle {
pub x: f64,
pub y: f64,

View File

@@ -4,7 +4,7 @@ use crate::{
};
/// Shape to draw a line from (x1, y1) to (x2, y2) with the given color
#[derive(Debug, Default, Clone, PartialEq)]
#[derive(Debug, Default, Clone)]
pub struct Line {
pub x1: f64,
pub y1: f64,
@@ -13,19 +13,6 @@ pub struct Line {
pub color: Color,
}
impl Line {
/// Create a new line from (x1, y1) to (x2, y2) with the given color
pub fn new(x1: f64, y1: f64, x2: f64, y2: f64, color: Color) -> Self {
Self {
x1,
y1,
x2,
y2,
color,
}
}
}
impl Shape for Line {
fn draw(&self, painter: &mut Painter) {
let Some((x1, y1)) = painter.get_point(self.x1, self.y1) else {
@@ -104,160 +91,3 @@ fn draw_line_high(painter: &mut Painter, x1: usize, y1: usize, x2: usize, y2: us
d += 2 * dx;
}
}
#[cfg(test)]
mod tests {
use super::Line;
use crate::{
assert_buffer_eq,
prelude::*,
widgets::{canvas::Canvas, Widget},
};
#[track_caller]
fn test(line: Line, expected_lines: Vec<&str>) {
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 10));
let canvas = Canvas::default()
.marker(Marker::Dot)
.x_bounds([0.0, 10.0])
.y_bounds([0.0, 10.0])
.paint(|context| {
context.draw(&line);
});
canvas.render(buffer.area, &mut buffer);
let mut expected = Buffer::with_lines(expected_lines);
for cell in expected.content.iter_mut() {
if cell.symbol == "" {
cell.set_style(Style::new().red());
}
}
assert_buffer_eq!(buffer, expected);
}
#[test]
fn off_grid() {
test(
Line::new(-1.0, -1.0, 10.0, 10.0, Color::Red),
vec![" "; 10],
);
test(
Line::new(0.0, 0.0, 11.0, 11.0, Color::Red),
vec![" "; 10],
);
}
#[test]
fn horizontal() {
test(
Line::new(0.0, 0.0, 10.0, 0.0, Color::Red),
vec![
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
"••••••••••",
],
);
test(
Line::new(10.0, 10.0, 0.0, 10.0, Color::Red),
vec![
"••••••••••",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
],
);
}
#[test]
fn vertical() {
test(
Line::new(0.0, 0.0, 0.0, 10.0, Color::Red),
vec![""; 10],
);
test(
Line::new(10.0, 10.0, 10.0, 0.0, Color::Red),
vec![""; 10],
);
}
#[test]
fn diagonal() {
// dy < dx, x1 < x2
test(
Line::new(0.0, 0.0, 10.0, 5.0, Color::Red),
vec![
" ",
" ",
" ",
" ",
"",
" •• ",
" •• ",
" •• ",
" •• ",
"",
],
);
// dy < dx, x1 > x2
test(
Line::new(10.0, 0.0, 0.0, 5.0, Color::Red),
vec![
" ",
" ",
" ",
" ",
"",
" •• ",
" •• ",
" •• ",
" •• ",
"",
],
);
// dy > dx, y1 < y2
test(
Line::new(0.0, 0.0, 5.0, 10.0, Color::Red),
vec![
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
],
);
// dy > dx, y1 > y2
test(
Line::new(0.0, 10.0, 5.0, 0.0, Color::Red),
vec![
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
],
);
}
}

View File

@@ -1,5 +1,3 @@
use strum::{Display, EnumString};
use crate::{
style::Color,
widgets::canvas::{
@@ -8,7 +6,7 @@ use crate::{
},
};
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
#[derive(Debug, Default, Clone, Copy)]
pub enum MapResolution {
#[default]
Low,
@@ -25,7 +23,7 @@ impl MapResolution {
}
/// Shape to draw a world map with the given resolution and color
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
#[derive(Debug, Default, Clone)]
pub struct Map {
pub resolution: MapResolution,
pub color: Color,
@@ -40,153 +38,3 @@ impl Shape for Map {
}
}
}
#[cfg(test)]
mod tests {
use strum::ParseError;
use super::*;
use crate::{
assert_buffer_eq,
prelude::*,
widgets::{canvas::Canvas, Widget},
};
#[test]
fn map_resolution_to_string() {
assert_eq!(MapResolution::Low.to_string(), "Low");
assert_eq!(MapResolution::High.to_string(), "High");
}
#[test]
fn map_resolution_from_str() {
assert_eq!("Low".parse(), Ok(MapResolution::Low));
assert_eq!("High".parse(), Ok(MapResolution::High));
assert_eq!(
"".parse::<MapResolution>(),
Err(ParseError::VariantNotFound)
);
}
#[test]
fn default() {
let map = Map::default();
assert_eq!(map.resolution, MapResolution::Low);
assert_eq!(map.color, Color::Reset);
}
#[test]
fn draw_low() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 80, 40));
let canvas = Canvas::default()
.marker(Marker::Dot)
.x_bounds([-180.0, 180.0])
.y_bounds([-90.0, 90.0])
.paint(|context| {
context.draw(&Map::default());
});
canvas.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines(vec![
" ",
" ••••••• •• •• •• • ",
" •••••••••••••• ••• •••• ••• •• •••• ",
" •••••••••••••••• •• ••• ••••••• •• •• ••• ",
"• • •• •••••• •••••••••••• •• ••• • ••••• ••••••••• •• • • • • ",
"••••• •••• •••••••• •• •• ••• •••• •••• •• • • ",
" •••••••• ••••••• ••••• ••• •••••••• • ••••• ",
" •• •• •• ••••••• •• ••• •••• •• • ",
"••• ••• •••••• •••• •••• •• • •• ",
" • ••••••••• •• • ••• • •• •• •• ",
" • • •••• •• ••••••••• ••• • • • •• ",
" • • ••••• •••• •• •••••• ",
" • •• • • •• • ••••• ",
" •• •• • • •• •• • ",
" •• ••• ••• • • ••••• • ••• ",
" • •••• ••• • • • • • •• ",
" •••• • • •• • • •• •• ",
" ••• •• • • • •• ••• ••• ",
" • • • •• • • • • • ",
" • • • • • • ••• • • ",
" • • • • •• • • • ",
" • • •• ••• • ",
" • • • • • • • • ",
" • • • •• • • • • • ",
" • • • • ",
" • • • • • • ",
" • •• • • • • •• • ",
" • • • •••• •• ",
" • • •• ••• ",
" •• • ",
" •• • ",
" •• ",
" ",
" ••• • •••• • • •• • ",
" •••• •••••• •••••• •••••• • ••• ",
" •• •••••• ••••• •• • ••• • •• ",
"• ••••• •• •• •••••• • •• ",
"• • • • • • • ",
"",
" ",
]);
assert_buffer_eq!(buffer, expected);
}
#[test]
fn draw_high() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 80, 40));
let canvas = Canvas::default()
.marker(Marker::Braille)
.x_bounds([-180.0, 180.0])
.y_bounds([-90.0, 90.0])
.paint(|context| {
context.draw(&Map {
resolution: MapResolution::High,
..Default::default()
});
});
canvas.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines(vec![
" ",
" ⢀⣠⠤⠤⠤⠔⢤⣤⡄⠤⡠⣄⠢⠂⢢⠰⣠⡄⣀⡀ ⣀ ",
" ⢀⣀⡤⣦⠲⢶⣿⣮⣿⡉⣰⢶⢏⡂ ⢀⣟⠁ ⢺⣻⢿⠏ ⠈⠉⠁ ⢀⣀ ⠈⠓⢳⣢⣂⡀ ",
" ⡞⣳⣿⣻⡧⣷⣿⣿⢿⢿⣧⡀⠉⠉⠙⢆ ⣰⠇ ⣠⠞⠃⢉⣄⣀⣠⠴⠊⠉⠁ ⠐⠾⠤⢤⠤⡄⠐⣻⠜⢓⠂ ",
"⢍ ⢀⡴⠊⠙⠓⠒⠒⠤⠖⠺⠿⠽⣷⣬⢬⣾⣷⢻⣷⢲⢲⣍⠱⡀ ⠹⡗ ⢀⢐⠟ ⡔⠒⠉⠲⠤⢀⢄⡀⢩⣣⠦⢷⢼⡏⠈ ⠉⠉⠉ ⠈⠈⠉⠖⠤⠆⠒⠭",
"⠶⢽⡲⣽⡆ ⠈⣠⣽⣯⡼⢯⣘⡯⠃⠘⡆ ⢰⠒⠁ ⢾⣚⠟ ⢀⠆ ⣔⠆ ⢷⠾⠋⠁ ⠙⠁ ⠠⡤",
" ⠠⢧⣄⣀⡶⠦⠤⡀ ⢰⡁ ⠉⡻⠙⣎⡥ ⠘⠲⠇ ⢀⡀⠨⣁⡄⣸⢫⡤⠄ ⣀⢠⣤⠊⣼⠅⠖⠋⠁",
" ⣠⠾⠛⠁ ⠈⣱ ⠋⠦⢤⡼ ⠈⠈⠦⡀ ⢀⣿⣇ ⢹⣷⣂⡞⠃ ⢀⣂⡀ ⠏⣜ ",
" ⠙⣷⡄ ⠘⠆ ⢀⣀⡠⣗ ⠘⣻⣽⡟⠉⠈ ⢹⡇ ⠟⠁ ",
" ⠈⡟ ⢎⣻⡿⠾⠇ ⠘⠇ ⣀⡀ ⣤⣤⡆ ⡠⡦ ⢀⠎⡏ ",
" ⡇ ⣀⠏⠋ ⢸⠒⢃⡖⢻⢟⣷⣄⣰⣡⠥⣱ ⢏⣧ ⣀ ⡴⠚⢰⠟ ",
" ⢳ ⢸⠃ ⠸⣄⣼⣠⢼⡴⡟⢿⢿⣀⣄ ⠸⡹ ⠘⡯⢿⡇⡠⢼⠁ ",
" ⢳⣀ ⢀⠞⠁ ⢠⠋⠁ ⠐⠧⡄⣬⣉⣈⡽ ⢧⠘⢽⠟⠉ ",
" ⣿⣄ ⡴⠚⠛⣿⣀ ⢠⠖ ⠈⠁ ⠹⣧ ⢾⣄⡀ ⡼ ⠈ ",
" ⣀ ⠘⣿⡄ ⡇ ⣘⣻ ⡏ ⢻⡄ ⠘⠿⢿⠒⠲⡀ ⢀⡀ ⢀⡰⣗ ",
" ⠉⠷ ⢫⡀⢧⡼⡟⠉⣛⣳⣦⡀ ⠈⡇ ⠸⣱ ⢀⡼ ⢺ ⡸⠉⢇ ⣾⡏ ⣁ ",
" ⠉⠒⢆⡓⡆ ⠠⡃ ⢳⣇⡠⠏ ⠐⡄⡞ ⠘⣇⡀⢱ ⣾⡀ ",
" ⢹⣇⣀⣾⡷⠤⡆ ⢣ ⠯⢺⠇ ⢣⣅ ⣽⢱⡔ ⢠⢿⣗ ",
" ⠙⢱ ⠘⠦⡄ ⠈⢦⡠⣠⢶⣀ ⡜ ⠈⠿ ⢠⣽⢆ ⢀⣼⡜⠿ ",
" ⢀⡞ ⢱⡀ ⢸ ⡔⠁ ⢻⢿⢰⠏⢸⣤⣴⣆ ",
" ⢘⠆ ⠙⠢⢄ ⠸⡀ ⡸⠁ ⠈⣞⡎⠥⡟⣿⠠⠿⣷⠒⢤⢀⣆ ",
" ⠘⠆ ⢈⠂ ⢳ ⡇ ⠈⠳⠶⣤⣭⣠ ⠋⢧⡬⣟⠉⠷⡄ ",
" ⢨ ⡜ ⢸ ⠸ ⣠ ⠁⢁⣰⢶ ⡇⠉⠁ ⠛ ",
"⠆ ⠈⢱⡀ ⡆ ⡇ ⢀⡜⡴⢹ ⢰⠏⠁⠘⢶⠹⡀ ⠸ ⢠⡶",
" ⠅ ⣸ ⢸ ⢫ ⡞⡊ ⢠⠔⠋ ⢳⡀ ⠐⣦ ",
" ⡅ ⡏ ⠈⡆ ⢠⠎ ⠳⠃ ⢸ ⢳ ",
" ⠨ ⡸⠁ ⢱ ⡸ ⠈⡇ ⢀⣀⡀ ⢸ ",
" ⠸ ⠐⡶⠁ ⠘⠖⠚ ⠣⠒⠋ ⠱⣇ ⢀⠇ ⠰⡄ ",
" ⠽ ⣰⡖⠁ ⠘⢚⡊ ⢀⣿⠇",
" ⡯⢀⡟ ⠘⠏ ⢠⢾⠃ ",
" ⠇⢨⠆ ⢠⡄ ⠈⠁ ",
" ⢧⣷⡀⠚ ",
" ⠉⠁ ",
" ⢀⡀ ",
" ⢠⡾⠋ ⣀⡠⠖⢦⣀⣀ ⣀⠤⠦⢤⠤⠶⠤⠖⠦⠤⠤⠤⠴⠤⢤⣄ ",
" ⢀⣤⣀ ⡀ ⣼⣻⠙⡆ ⢀⡤⠤⠤⠴⠒⠖⠒⠒⠒⠚⠉⠋⠁ ⢰⡳⠊⠁ ⠈⠉⠉⠒⠤⣤ ",
" ⢀⣀⣀⡴⠖⠒⠒⠚⠛⠛⠛⠒⠚⠳⠉⠉⠉⠉⢉⣉⡥⠔⠃ ⢀⣠⠤⠴⠃ ⢠⠞⠁ ",
" ⠘⠛⣓⣒⠆ ⠸⠥⣀⣤⡦⠠⣞⣭⣇⣘⠿⠆ ⣖⠛ ",
"⠶⠔⠲⠤⠠⠜⢗⠤⠄ ⠘⠉ ⠁ ⠈⠉⠒⠔⠤",
" ",
]);
assert_buffer_eq!(buffer, expected);
}
}

View File

@@ -29,14 +29,14 @@ pub trait Shape {
}
/// Label to draw some text on the canvas
#[derive(Debug, Default, Clone, PartialEq)]
#[derive(Debug, Default, Clone)]
pub struct Label<'a> {
x: f64,
y: f64,
line: TextLine<'a>,
}
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
#[derive(Debug, Default, Clone)]
struct Layer {
string: String,
colors: Vec<Color>,
@@ -51,7 +51,7 @@ trait Grid: Debug {
fn reset(&mut self);
}
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
#[derive(Debug, Default, Clone)]
struct BrailleGrid {
width: u16,
height: u16,
@@ -114,7 +114,7 @@ impl Grid for BrailleGrid {
}
}
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
#[derive(Debug, Default, Clone)]
struct CharGrid {
width: u16,
height: u16,
@@ -187,7 +187,7 @@ impl<'a, 'b> Painter<'a, 'b> {
///
/// # Examples:
/// ```
/// use ratatui::{prelude::*, widgets::canvas::*};
/// use ratatui::{symbols, widgets::canvas::{Painter, Context}};
///
/// let mut ctx = Context::new(2, 2, [1.0, 2.0], [0.0, 2.0], symbols::Marker::Braille);
/// let mut painter = Painter::from(&mut ctx);
@@ -224,7 +224,7 @@ impl<'a, 'b> Painter<'a, 'b> {
///
/// # Examples:
/// ```
/// use ratatui::{prelude::*, widgets::canvas::*};
/// use ratatui::{style::Color, symbols, widgets::canvas::{Painter, Context}};
///
/// let mut ctx = Context::new(1, 1, [0.0, 2.0], [0.0, 2.0], symbols::Marker::Braille);
/// let mut painter = Painter::from(&mut ctx);
@@ -320,21 +320,15 @@ impl<'a> Context<'a> {
}
}
/// The Canvas widget provides a means to draw shapes (Lines, Rectangles, Circles, etc.) on a grid.
///
/// By default the grid is made of Braille patterns but you may change the marker to use a different
/// set of symbols. If your terminal or font does not support this unicode block, you will see
/// unicode replacement characters (<28>) instead of braille dots. The Braille patterns provide a more
/// fine grained result (2x4 dots) but you might want to use a simple dot, block, or bar instead by
/// calling the [`marker`] method if your target environment does not support those symbols,
///
/// See [Unicode Braille Patterns](https://en.wikipedia.org/wiki/Braille_Patterns) for more info.
///
/// The Canvas widget may be used to draw more detailed figures using braille patterns (each
/// cell can have a braille character in 8 different positions).
/// # Examples
///
/// ```
/// use ratatui::{style::Color, widgets::{*, canvas::*}};
///
/// # use ratatui::widgets::{Block, Borders};
/// # use ratatui::layout::Rect;
/// # use ratatui::widgets::canvas::{Canvas, Shape, Line, Rectangle, Map, MapResolution};
/// # use ratatui::style::Color;
/// Canvas::default()
/// .block(Block::default().title("Canvas").borders(Borders::ALL))
/// .x_bounds([-180.0, 180.0])
@@ -361,9 +355,7 @@ impl<'a> Context<'a> {
/// });
/// });
/// ```
///
/// [`marker`]: #method.marker
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone)]
pub struct Canvas<'a, F>
where
F: Fn(&mut Context),
@@ -436,10 +428,12 @@ where
/// # Examples
///
/// ```
/// use ratatui::{prelude::*, widgets::canvas::*};
///
/// # use ratatui::widgets::canvas::Canvas;
/// # use ratatui::symbols;
/// Canvas::default().marker(symbols::Marker::Braille).paint(|ctx| {});
///
/// Canvas::default().marker(symbols::Marker::Dot).paint(|ctx| {});
///
/// Canvas::default().marker(symbols::Marker::Block).paint(|ctx| {});
/// ```
pub fn marker(mut self, marker: symbols::Marker) -> Canvas<'a, F> {

View File

@@ -4,7 +4,7 @@ use crate::{
};
/// A shape to draw a group of points with the given color
#[derive(Debug, Default, Clone, PartialEq)]
#[derive(Debug, Default, Clone)]
pub struct Points<'a> {
pub coords: &'a [(f64, f64)],
pub color: Color,

View File

@@ -4,7 +4,7 @@ use crate::{
};
/// Shape to draw a rectangle from a `Rect` with the given color
#[derive(Debug, Default, Clone, PartialEq)]
#[derive(Debug, Default, Clone)]
pub struct Rectangle {
pub x: f64,
pub y: f64,
@@ -50,91 +50,3 @@ impl Shape for Rectangle {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
assert_buffer_eq,
prelude::*,
widgets::{canvas::Canvas, Widget},
};
#[test]
fn draw_block_lines() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 10));
let canvas = Canvas::default()
.marker(Marker::Block)
.x_bounds([0.0, 10.0])
.y_bounds([0.0, 10.0])
.paint(|context| {
context.draw(&Rectangle {
x: 0.0,
y: 0.0,
width: 10.0,
height: 10.0,
color: Color::Red,
});
});
canvas.render(buffer.area, &mut buffer);
let mut expected = Buffer::with_lines(vec![
"██████████",
"█ █",
"█ █",
"█ █",
"█ █",
"█ █",
"█ █",
"█ █",
"█ █",
"██████████",
]);
expected.set_style(buffer.area, Style::new().red());
expected.set_style(buffer.area.inner(&Margin::new(1, 1)), Style::reset());
assert_buffer_eq!(buffer, expected);
}
#[test]
fn draw_braille_lines() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 10));
let canvas = Canvas::default()
.marker(Marker::Braille)
.x_bounds([0.0, 10.0])
.y_bounds([0.0, 10.0])
.paint(|context| {
// a rectangle that will draw the outside part of the braille
context.draw(&Rectangle {
x: 0.0,
y: 0.0,
width: 10.0,
height: 10.0,
color: Color::Red,
});
// a rectangle that will draw the inside part of the braille
context.draw(&Rectangle {
x: 2.0,
y: 1.75,
width: 6.5,
height: 6.5,
color: Color::Green,
});
});
canvas.render(buffer.area, &mut buffer);
let mut expected = Buffer::with_lines(vec![
"⡏⠉⠉⠉⠉⠉⠉⠉⠉⢹",
"⡇⢠⠤⠤⠤⠤⠤⠤⡄⢸",
"⡇⢸ ⡇⢸",
"⡇⢸ ⡇⢸",
"⡇⢸ ⡇⢸",
"⡇⢸ ⡇⢸",
"⡇⢸ ⡇⢸",
"⡇⢸ ⡇⢸",
"⡇⠈⠉⠉⠉⠉⠉⠉⠁⢸",
"⣇⣀⣀⣀⣀⣀⣀⣀⣀⣸",
]);
expected.set_style(buffer.area, Style::new().red());
expected.set_style(buffer.area.inner(&Margin::new(1, 1)), Style::new().green());
expected.set_style(buffer.area.inner(&Margin::new(2, 2)), Style::reset());
assert_buffer_eq!(buffer, expected);
}
}

View File

@@ -1,6 +1,5 @@
use std::{borrow::Cow, cmp::max};
use strum::{Display, EnumString};
use unicode_width::UnicodeWidthStr;
use crate::{
@@ -16,7 +15,7 @@ use crate::{
};
/// An X or Y axis for the chart widget
#[derive(Debug, Default, Clone, PartialEq)]
#[derive(Debug, Default, Clone)]
pub struct Axis<'a> {
/// Title displayed next to axis end
title: Option<TextLine<'a>>,
@@ -77,7 +76,7 @@ impl<'a> Axis<'a> {
}
/// Used to determine which style of graphing to use
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
#[derive(Debug, Default, Clone, Copy)]
pub enum GraphType {
/// Draw each point
#[default]
@@ -87,7 +86,7 @@ pub enum GraphType {
}
/// A group of data points
#[derive(Debug, Default, Clone, PartialEq)]
#[derive(Debug, Default, Clone)]
pub struct Dataset<'a> {
/// Name of the dataset (used in the legend if shown)
name: Cow<'a, str>,
@@ -133,7 +132,7 @@ impl<'a> Dataset<'a> {
/// A container that holds all the infos about where to display each elements of the chart (axis,
/// labels, legend, ...).
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
#[derive(Debug, Default, Clone, PartialEq)]
struct ChartLayout {
/// Location of the title of the x axis
title_x: Option<(u16, u16)>,
@@ -158,8 +157,10 @@ struct ChartLayout {
/// # Examples
///
/// ```
/// use ratatui::{prelude::*, widgets::*};
///
/// # use ratatui::symbols;
/// # use ratatui::widgets::{Block, Borders, Chart, Axis, Dataset, GraphType};
/// # use ratatui::style::{Style, Color};
/// # use ratatui::text::Span;
/// let datasets = vec![
/// Dataset::default()
/// .name("data1")
@@ -187,7 +188,7 @@ struct ChartLayout {
/// .bounds([0.0, 10.0])
/// .labels(["0.0", "5.0", "10.0"].iter().cloned().map(Span::from).collect()));
/// ```
#[derive(Debug, Default, Clone, PartialEq)]
#[derive(Debug, Default, Clone)]
pub struct Chart<'a> {
/// A block to display around the widget eventually
block: Option<Block<'a>>,
@@ -240,7 +241,8 @@ impl<'a> Chart<'a> {
/// # Examples
///
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// # use ratatui::widgets::Chart;
/// # use ratatui::layout::Constraint;
/// let constraints = (
/// Constraint::Ratio(1, 3),
/// Constraint::Ratio(1, 4)
@@ -557,7 +559,7 @@ impl<'a> Widget for Chart<'a> {
if let Some((x, y)) = layout.title_x {
let title = self.x_axis.title.unwrap();
let width = title.width() as u16;
let width = graph_area.right().saturating_sub(x);
buf.set_style(
Rect {
x,
@@ -572,7 +574,7 @@ impl<'a> Widget for Chart<'a> {
if let Some((x, y)) = layout.title_y {
let title = self.y_axis.title.unwrap();
let width = title.width() as u16;
let width = graph_area.right().saturating_sub(x);
buf.set_style(
Rect {
x,
@@ -625,8 +627,6 @@ impl<'a> Styled for Chart<'a> {
#[cfg(test)]
mod tests {
use strum::ParseError;
use super::*;
use crate::style::{Modifier, Stylize};
@@ -702,28 +702,4 @@ mod tests {
.remove_modifier(Modifier::DIM)
)
}
#[test]
fn graph_type_to_string() {
assert_eq!(GraphType::Scatter.to_string(), "Scatter");
assert_eq!(GraphType::Line.to_string(), "Line");
}
#[test]
fn graph_type_from_str() {
assert_eq!("Scatter".parse::<GraphType>(), Ok(GraphType::Scatter));
assert_eq!("Line".parse::<GraphType>(), Ok(GraphType::Line));
assert_eq!("".parse::<GraphType>(), Err(ParseError::VariantNotFound));
}
#[test]
fn it_does_not_panic_if_title_is_wider_than_buffer() {
let widget = Chart::default()
.y_axis(Axis::default().title("xxxxxxxxxxxxxxxx"))
.x_axis(Axis::default().title("xxxxxxxxxxxxxxxx"));
let mut buffer = Buffer::empty(Rect::new(0, 0, 8, 4));
widget.render(buffer.area, &mut buffer);
assert_eq!(buffer, Buffer::with_lines(vec![" ".repeat(8); 4]))
}
}

View File

@@ -8,8 +8,10 @@ use crate::{buffer::Buffer, layout::Rect, widgets::Widget};
/// # Examples
///
/// ```
/// use ratatui::{prelude::*, widgets::*};
///
/// # use ratatui::widgets::{Clear, Block, Borders};
/// # use ratatui::layout::Rect;
/// # use ratatui::Frame;
/// # use ratatui::backend::Backend;
/// fn draw_on_clear<B: Backend>(f: &mut Frame<B>, area: Rect) {
/// let block = Block::default().title("Block").borders(Borders::ALL);
/// f.render_widget(Clear, area); // <- this will clear/reset the area first
@@ -21,7 +23,7 @@ use crate::{buffer::Buffer, layout::Rect, widgets::Widget};
///
/// For a more complete example how to utilize `Clear` to realize popups see
/// the example `examples/popup.rs`
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
#[derive(Debug, Default, Clone)]
pub struct Clear;
impl Widget for Clear {
@@ -33,28 +35,3 @@ impl Widget for Clear {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::assert_buffer_eq;
#[test]
fn render() {
let mut buf = Buffer::with_lines(vec!["xxxxxxxxxxxxxxx"; 7]);
let clear = Clear;
clear.render(Rect::new(1, 2, 3, 4), &mut buf);
assert_buffer_eq!(
buf,
Buffer::with_lines(vec![
"xxxxxxxxxxxxxxx",
"xxxxxxxxxxxxxxx",
"x xxxxxxxxxxx",
"x xxxxxxxxxxx",
"x xxxxxxxxxxx",
"x xxxxxxxxxxx",
"xxxxxxxxxxxxxxx",
])
);
}
}

View File

@@ -12,14 +12,14 @@ use crate::{
/// # Examples:
///
/// ```
/// use ratatui::{prelude::*, widgets::*};
///
/// # use ratatui::widgets::{Widget, Gauge, Block, Borders};
/// # use ratatui::style::{Style, Color, Modifier};
/// Gauge::default()
/// .block(Block::default().borders(Borders::ALL).title("Progress"))
/// .gauge_style(Style::default().fg(Color::White).bg(Color::Black).add_modifier(Modifier::ITALIC))
/// .percent(20);
/// ```
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone)]
pub struct Gauge<'a> {
block: Option<Block<'a>>,
ratio: f64,
@@ -170,15 +170,16 @@ fn get_unicode_block<'a>(frac: f64) -> &'a str {
/// # Examples:
///
/// ```
/// use ratatui::{prelude::*, widgets::*};
///
/// # use ratatui::widgets::{Widget, LineGauge, Block, Borders};
/// # use ratatui::style::{Style, Color, Modifier};
/// # use ratatui::symbols;
/// LineGauge::default()
/// .block(Block::default().borders(Borders::ALL).title("Progress"))
/// .gauge_style(Style::default().fg(Color::White).bg(Color::Black).add_modifier(Modifier::BOLD))
/// .line_set(symbols::line::THICK)
/// .ratio(0.4);
/// ```
#[derive(Debug, Default, Clone, PartialEq)]
#[derive(Debug, Default, Clone)]
pub struct LineGauge<'a> {
block: Option<Block<'a>>,
ratio: f64,

View File

@@ -5,10 +5,10 @@ use crate::{
layout::{Corner, Rect},
style::{Style, Styled},
text::Text,
widgets::{Block, HighlightSpacing, StatefulWidget, Widget},
widgets::{Block, StatefulWidget, Widget},
};
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
#[derive(Debug, Default, Clone)]
pub struct ListState {
offset: usize,
selected: Option<usize>,
@@ -45,7 +45,7 @@ impl ListState {
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ListItem<'a> {
content: Text<'a>,
style: Style,
@@ -81,8 +81,8 @@ impl<'a> ListItem<'a> {
/// # Examples
///
/// ```
/// use ratatui::{prelude::*, widgets::*};
///
/// # use ratatui::widgets::{Block, Borders, List, ListItem};
/// # use ratatui::style::{Style, Color, Modifier};
/// let items = [ListItem::new("Item 1"), ListItem::new("Item 2"), ListItem::new("Item 3")];
/// List::new(items)
/// .block(Block::default().title("List").borders(Borders::ALL))
@@ -90,7 +90,7 @@ impl<'a> ListItem<'a> {
/// .highlight_style(Style::default().add_modifier(Modifier::ITALIC))
/// .highlight_symbol(">>");
/// ```
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
#[derive(Debug, Clone)]
pub struct List<'a> {
block: Option<Block<'a>>,
items: Vec<ListItem<'a>>,
@@ -103,8 +103,6 @@ pub struct List<'a> {
highlight_symbol: Option<&'a str>,
/// Whether to repeat the highlight symbol for each line of the selected item
repeat_highlight_symbol: bool,
/// Decides when to allocate spacing for the selection symbol
highlight_spacing: HighlightSpacing,
}
impl<'a> List<'a> {
@@ -120,7 +118,6 @@ impl<'a> List<'a> {
highlight_style: Style::default(),
highlight_symbol: None,
repeat_highlight_symbol: false,
highlight_spacing: HighlightSpacing::default(),
}
}
@@ -149,14 +146,6 @@ impl<'a> List<'a> {
self
}
/// Set when to show the highlight spacing
///
/// See [`HighlightSpacing`] about which variant affects spacing in which way
pub fn highlight_spacing(mut self, value: HighlightSpacing) -> Self {
self.highlight_spacing = value;
self
}
pub fn start_corner(mut self, corner: Corner) -> List<'a> {
self.start_corner = corner;
self
@@ -239,7 +228,7 @@ impl<'a> StatefulWidget for List<'a> {
let blank_symbol = " ".repeat(highlight_symbol.width());
let mut current_height = 0;
let selection_spacing = self.highlight_spacing.should_add(state.selected.is_some());
let has_selection = state.selected.is_some();
for (i, item) in self
.items
.iter_mut()
@@ -274,7 +263,7 @@ impl<'a> StatefulWidget for List<'a> {
} else {
&blank_symbol
};
let (elem_x, max_element_width) = if selection_spacing {
let (elem_x, max_element_width) = if has_selection {
let (elem_x, _) = buf.set_stringn(
x,
y + j as u16,
@@ -750,17 +739,17 @@ mod tests {
fn test_list_style() {
let items = list_items(vec!["Item 0", "Item 1", "Item 2"]);
let list = List::new(items).style(Style::default().fg(Color::Red));
let buffer = render_widget(list, 10, 5);
assert_buffer_eq!(
render_widget(list, 10, 5),
Buffer::with_lines(vec![
"Item 0 ".red(),
"Item 1 ".red(),
"Item 2 ".red(),
" ".red(),
" ".red(),
])
);
let mut expected = Buffer::with_lines(vec![
"Item 0 ",
"Item 1 ",
"Item 2 ",
" ",
" ",
]);
expected.set_style(buffer.area, Style::default().fg(Color::Red));
assert_buffer_eq!(buffer, expected);
}
#[test]
@@ -772,144 +761,17 @@ mod tests {
let mut state = ListState::default();
state.select(Some(1));
assert_buffer_eq!(
render_stateful_widget(list, &mut state, 10, 5),
Buffer::with_lines(vec![
" Item 0 ".into(),
">>Item 1 ".yellow(),
" Item 2 ".into(),
" ".into(),
" ".into(),
])
);
}
let buffer = render_stateful_widget(list, &mut state, 10, 5);
#[test]
fn test_list_highlight_spacing_default_whenselected() {
// when not selected
{
let items = list_items(vec!["Item 0", "Item 1", "Item 2"]);
let list = List::new(items).highlight_symbol(">>");
let mut state = ListState::default();
let buffer = render_stateful_widget(list, &mut state, 10, 5);
let expected = Buffer::with_lines(vec![
"Item 0 ",
"Item 1 ",
"Item 2 ",
" ",
" ",
]);
assert_buffer_eq!(buffer, expected);
}
// when selected
{
let items = list_items(vec!["Item 0", "Item 1", "Item 2"]);
let list = List::new(items).highlight_symbol(">>");
let mut state = ListState::default();
state.select(Some(1));
let buffer = render_stateful_widget(list, &mut state, 10, 5);
let expected = Buffer::with_lines(vec![
" Item 0 ",
">>Item 1 ",
" Item 2 ",
" ",
" ",
]);
assert_buffer_eq!(buffer, expected);
}
}
#[test]
fn test_list_highlight_spacing_default_always() {
// when not selected
{
let items = list_items(vec!["Item 0", "Item 1", "Item 2"]);
let list = List::new(items)
.highlight_symbol(">>")
.highlight_spacing(HighlightSpacing::Always);
let mut state = ListState::default();
let buffer = render_stateful_widget(list, &mut state, 10, 5);
let expected = Buffer::with_lines(vec![
" Item 0 ",
" Item 1 ",
" Item 2 ",
" ",
" ",
]);
assert_buffer_eq!(buffer, expected);
}
// when selected
{
let items = list_items(vec!["Item 0", "Item 1", "Item 2"]);
let list = List::new(items)
.highlight_symbol(">>")
.highlight_spacing(HighlightSpacing::Always);
let mut state = ListState::default();
state.select(Some(1));
let buffer = render_stateful_widget(list, &mut state, 10, 5);
let expected = Buffer::with_lines(vec![
" Item 0 ",
">>Item 1 ",
" Item 2 ",
" ",
" ",
]);
assert_buffer_eq!(buffer, expected);
}
}
#[test]
fn test_list_highlight_spacing_default_never() {
// when not selected
{
let items = list_items(vec!["Item 0", "Item 1", "Item 2"]);
let list = List::new(items)
.highlight_symbol(">>")
.highlight_spacing(HighlightSpacing::Never);
let mut state = ListState::default();
let buffer = render_stateful_widget(list, &mut state, 10, 5);
let expected = Buffer::with_lines(vec![
"Item 0 ",
"Item 1 ",
"Item 2 ",
" ",
" ",
]);
assert_buffer_eq!(buffer, expected);
}
// when selected
{
let items = list_items(vec!["Item 0", "Item 1", "Item 2"]);
let list = List::new(items)
.highlight_symbol(">>")
.highlight_spacing(HighlightSpacing::Never);
let mut state = ListState::default();
state.select(Some(1));
let buffer = render_stateful_widget(list, &mut state, 10, 5);
let expected = Buffer::with_lines(vec![
"Item 0 ",
"Item 1 ",
"Item 2 ",
" ",
" ",
]);
assert_buffer_eq!(buffer, expected);
}
let mut expected = Buffer::with_lines(vec![
" Item 0 ",
">>Item 1 ",
" Item 2 ",
" ",
" ",
]);
expected.set_style(Rect::new(0, 1, 10, 1), Style::default().fg(Color::Yellow));
assert_buffer_eq!(buffer, expected);
}
#[test]
@@ -922,16 +784,17 @@ mod tests {
let mut state = ListState::default();
state.select(Some(0));
assert_buffer_eq!(
render_stateful_widget(list, &mut state, 10, 5),
Buffer::with_lines(vec![
">>Item 0 ".yellow(),
">>Line 2 ".yellow(),
" Item 1 ".into(),
" Item 2 ".into(),
" ".into(),
])
);
let buffer = render_stateful_widget(list, &mut state, 10, 5);
let mut expected = Buffer::with_lines(vec![
">>Item 0 ",
">>Line 2 ",
" Item 1 ",
" Item 2 ",
" ",
]);
expected.set_style(Rect::new(0, 0, 10, 2), Style::default().fg(Color::Yellow));
assert_buffer_eq!(buffer, expected);
}
#[test]

View File

@@ -4,22 +4,18 @@
//! meant to be stored but used as *commands* to draw common figures in the UI.
//!
//! The available widgets are:
//! - [`Block`]: a basic widget that draws a block with optional borders, titles and styles.
//! - [`BarChart`]: displays multiple datasets as bars with optional grouping.
//! - [`calendar::Monthly`]: displays a single month.
//! - [`Canvas`]: draws arbitrary shapes using drawing characters.
//! - [`Chart`]: displays multiple datasets as a lines or scatter graph.
//! - [`Clear`]: clears the area it occupies. Useful to render over previously drawn widgets.
//! - [`Gauge`]: displays progress percentage using block characters.
//! - [`LineGauge`]: display progress as a line.
//! - [`List`]: displays a list of items and allows selection.
//! - [`Paragraph`]: displays a paragraph of optionally styled and wrapped text.
//! - [`Scrollbar`]: displays a scrollbar.
//! - [`Sparkline`]: display a single data set as a sparkline.
//! - [`Table`]: displays multiple rows and columns in a grid and allows selection.
//! - [`Tabs`]: displays a tab bar and allows selection.
//!
//! [`Canvas`]: crate::widgets::canvas::Canvas
//! - [`Block`]
//! - [`Tabs`]
//! - [`List`]
//! - [`Table`]
//! - [`Paragraph`]
//! - [`Chart`]
//! - [`BarChart`]
//! - [`Gauge`]
//! - [`Sparkline`]
//! - [`calendar::Monthly`]
//! - [`Clear`]
mod barchart;
pub mod block;
#[cfg(feature = "widget-calendar")]
@@ -50,14 +46,14 @@ pub use self::{
paragraph::{Paragraph, Wrap},
scrollbar::{ScrollDirection, Scrollbar, ScrollbarOrientation, ScrollbarState},
sparkline::{RenderDirection, Sparkline},
table::{Cell, HighlightSpacing, Row, Table, TableState},
table::{Cell, Row, Table, TableState},
tabs::Tabs,
};
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)]
#[derive(Default, Clone, Copy, PartialEq, Eq)]
pub struct Borders: u8 {
/// Show no border (default)
const NONE = 0b0000;
@@ -129,8 +125,10 @@ pub trait Widget {
/// ## Examples
///
/// ```rust,no_run
/// use std::io;
/// use ratatui::{backend::TestBackend, prelude::*, widgets::*};
/// # use std::io;
/// # use ratatui::Terminal;
/// # use ratatui::backend::{Backend, TestBackend};
/// # use ratatui::widgets::{Widget, List, ListItem, ListState};
///
/// // Let's say we have some events to display.
/// struct Events {
@@ -232,7 +230,9 @@ pub trait StatefulWidget {
/// ## Examples
///
///```
/// use ratatui::{border, prelude::*, widgets::*};
/// # use ratatui::widgets::{Block, Borders};
/// # use ratatui::style::{Style, Color};
/// # use ratatui::border;
///
/// Block::default()
/// //Construct a `Borders` object and use it in place

View File

@@ -21,29 +21,28 @@ fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Alignment)
/// A widget to display some text.
///
/// # Example
/// # Examples
///
/// ```
/// use ratatui::{prelude::*, widgets::*};
///
/// # use ratatui::text::{Text, Line, Span};
/// # use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
/// # use ratatui::style::{Style, Color, Modifier};
/// # use ratatui::layout::{Alignment};
/// let text = vec![
/// Line::from(vec![
/// Span::raw("First"),
/// Span::styled("line",Style::new().green().italic()),
/// ".".into(),
/// Span::styled("line",Style::default().add_modifier(Modifier::ITALIC)),
/// Span::raw("."),
/// ]),
/// Line::from("Second line".red()),
/// "Third line".into(),
/// Line::from(Span::styled("Second line", Style::default().fg(Color::Red))),
/// ];
/// Paragraph::new(text)
/// .block(Block::new()
/// .title("Paragraph")
/// .borders(Borders::ALL))
/// .style(Style::new().white().on_black())
/// .block(Block::default().title("Paragraph").borders(Borders::ALL))
/// .style(Style::default().fg(Color::White).bg(Color::Black))
/// .alignment(Alignment::Center)
/// .wrap(Wrap { trim: true });
/// ```
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
#[derive(Debug, Default, Clone)]
pub struct Paragraph<'a> {
/// A block to wrap the widget in
block: Option<Block<'a>>,
@@ -64,8 +63,8 @@ pub struct Paragraph<'a> {
/// ## Examples
///
/// ```
/// use ratatui::{prelude::*, widgets::*};
///
/// # use ratatui::widgets::{Paragraph, Wrap};
/// # use ratatui::text::Text;
/// let bullet_points = Text::from(r#"Some indented points:
/// - First thing goes here and is long so that it wraps
/// - Here is another point that is long enough to wrap"#);
@@ -86,33 +85,13 @@ pub struct Paragraph<'a> {
/// // - Here is another point
/// // that is long enough to wrap
/// ```
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
#[derive(Debug, Default, Clone, Copy)]
pub struct Wrap {
/// Should leading whitespace be trimmed
pub trim: bool,
}
type Horizontal = u16;
type Vertical = u16;
impl<'a> Paragraph<'a> {
/// Creates a new [`Paragraph`] widget with the given text.
///
/// The `text` parameter can be a [`Text`] or any type that can be converted into a [`Text`]. By
/// default, the text is styled with [`Style::default()`], not wrapped, and aligned to the left.
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let paragraph = Paragraph::new("Hello, world!");
/// let paragraph = Paragraph::new(String::from("Hello, world!"));
/// let paragraph = Paragraph::new(Text::raw("Hello, world!"));
/// let paragraph = Paragraph::new(
/// Text::styled("Hello, world!", Style::default()));
/// let paragraph = Paragraph::new(
/// Line::from(vec!["Hello, ".into(), "world!".red()]));
/// ```
pub fn new<T>(text: T) -> Paragraph<'a>
where
T: Into<Text<'a>>,
@@ -127,83 +106,26 @@ impl<'a> Paragraph<'a> {
}
}
/// Surrounds the [`Paragraph`] widget with a [`Block`].
///
/// # Example
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let paragraph = Paragraph::new("Hello, world!")
/// .block(Block::default()
/// .title("Paragraph")
/// .borders(Borders::ALL));
/// ```
pub fn block(mut self, block: Block<'a>) -> Paragraph<'a> {
self.block = Some(block);
self
}
/// Sets the style of the entire widget.
///
/// This applies to the entire widget, including the block if one is present. Any style set on
/// the block or text will be added to this style.
///
/// # Example
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let paragraph = Paragraph::new("Hello, world!")
/// .style(Style::new().red().on_white());
/// ```
pub fn style(mut self, style: Style) -> Paragraph<'a> {
self.style = style;
self
}
/// Sets the wrapping configuration for the widget.
///
/// See [`Wrap`] for more information on the different options.
///
/// # Example
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let paragraph = Paragraph::new("Hello, world!")
/// .wrap(Wrap { trim: true });
/// ```
pub fn wrap(mut self, wrap: Wrap) -> Paragraph<'a> {
self.wrap = Some(wrap);
self
}
/// Set the scroll offset for the given paragraph
///
/// The scroll offset is a tuple of (y, x) offset. The y offset is the number of lines to
/// scroll, and the x offset is the number of characters to scroll. The scroll offset is applied
/// after the text is wrapped and aligned.
///
/// Note: the order of the tuple is (y, x) instead of (x, y), which is different from general
/// convention across the crate.
///
/// For more information about future scrolling design and concerns, see [RFC: Design of
/// Scrollable Widgets](https://github.com/ratatui-org/ratatui/issues/174) on GitHub.
pub fn scroll(mut self, offset: (Vertical, Horizontal)) -> Paragraph<'a> {
pub fn scroll(mut self, offset: (u16, u16)) -> Paragraph<'a> {
self.scroll = offset;
self
}
/// Set the text alignment for the given paragraph
///
/// The alignment is a variant of the [`Alignment`] enum which can be one of Left, Right, or
/// Center.
///
/// # Example
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let paragraph = Paragraph::new("Hello World")
/// .alignment(Alignment::Center);
/// ```
pub fn alignment(mut self, alignment: Alignment) -> Paragraph<'a> {
self.alignment = alignment;
self

View File

@@ -1,5 +1,3 @@
use strum::{Display, EnumString};
use super::StatefulWidget;
use crate::{
buffer::Buffer,
@@ -9,7 +7,7 @@ use crate::{
};
/// An enum representing the direction of scrolling in a Scrollbar widget.
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)]
pub enum ScrollDirection {
/// Forward scroll direction, usually corresponds to scrolling downwards or rightwards.
#[default]
@@ -20,16 +18,10 @@ pub enum ScrollDirection {
/// A struct representing the state of a Scrollbar widget.
///
/// # Important
///
/// It's essential to set the `content_length` field when using this struct. This field
/// represents the total length of the scrollable content. The default value is zero
/// which will result in the Scrollbar not rendering.
///
/// For example, in the following list, assume there are 4 bullet points:
///
/// - the `content_length` is 4
/// - the `position` is 0
/// - the `content_length` is 4
/// - the `viewport_content_length` is 2
///
/// ```text
@@ -43,38 +35,31 @@ 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)]
#[derive(Debug, Default, Clone, Copy)]
pub struct ScrollbarState {
// The total length of the scrollable content.
content_length: usize,
// The current position within the scrollable content.
position: usize,
position: u16,
// The total length of the scrollable content.
content_length: u16,
// The length of content in current viewport.
viewport_content_length: usize,
viewport_content_length: u16,
}
impl ScrollbarState {
/// Constructs a new ScrollbarState with the specified content length.
pub fn new(content_length: usize) -> Self {
Self {
content_length,
..Default::default()
}
}
/// Sets the scroll position of the scrollbar and returns the modified ScrollbarState.
pub fn position(mut self, position: usize) -> Self {
pub fn position(mut self, position: u16) -> Self {
self.position = position;
self
}
/// Sets the length of the scrollable content and returns the modified ScrollbarState.
pub fn content_length(mut self, content_length: usize) -> Self {
pub fn content_length(mut self, content_length: u16) -> Self {
self.content_length = content_length;
self
}
/// Sets the length of the viewport content and returns the modified ScrollbarState.
pub fn viewport_content_length(mut self, viewport_content_length: usize) -> Self {
pub fn viewport_content_length(mut self, viewport_content_length: u16) -> Self {
self.viewport_content_length = viewport_content_length;
self
}
@@ -116,7 +101,7 @@ impl ScrollbarState {
}
/// Scrollbar Orientation
#[derive(Debug, Default, Display, EnumString, Clone, Eq, PartialEq, Hash)]
#[derive(Debug, Default, Clone)]
pub enum ScrollbarOrientation {
#[default]
VerticalRight,
@@ -137,44 +122,13 @@ pub enum ScrollbarOrientation {
/// │ └──────── thumb
/// └─────────── begin
/// ```
///
/// # Examples
///
/// ```rust
/// use ratatui::{prelude::*, widgets::*};
///
/// # fn render_paragraph_with_scrollbar<B: Backend>(frame: &mut Frame<B>, area: Rect) {
///
/// let vertical_scroll = 0; // from app state
///
/// let items = vec![Line::from("Item 1"), Line::from("Item 2"), Line::from("Item 3")];
/// let paragraph = Paragraph::new(items.clone())
/// .scroll((vertical_scroll as u16, 0))
/// .block(Block::new().borders(Borders::RIGHT)); // to show a background for the scrollbar
///
/// let scrollbar = Scrollbar::default()
/// .orientation(ScrollbarOrientation::VerticalRight)
/// .begin_symbol(Some("↑"))
/// .end_symbol(Some("↓"));
/// let mut scrollbar_state = ScrollbarState::new(items.iter().len()).position(vertical_scroll);
///
/// let area = frame.size();
/// frame.render_widget(paragraph, area);
/// frame.render_stateful_widget(scrollbar,
/// area.inner(&Margin {
/// vertical: 1,
/// horizontal: 0,
/// }), // using a inner vertical margin of 1 unit makes the scrollbar inside the block
/// &mut scrollbar_state);
/// # }
/// ```
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
#[derive(Debug, Clone)]
pub struct Scrollbar<'a> {
orientation: ScrollbarOrientation,
thumb_style: Style,
thumb_symbol: &'a str,
track_style: Style,
track_symbol: Option<&'a str>,
track_symbol: &'a str,
begin_symbol: Option<&'a str>,
begin_style: Style,
end_symbol: Option<&'a str>,
@@ -187,7 +141,7 @@ impl<'a> Default for Scrollbar<'a> {
orientation: ScrollbarOrientation::default(),
thumb_symbol: DOUBLE_VERTICAL.thumb,
thumb_style: Style::default(),
track_symbol: Some(DOUBLE_VERTICAL.track),
track_symbol: DOUBLE_VERTICAL.track,
track_style: Style::default(),
begin_symbol: Some(DOUBLE_VERTICAL.begin),
begin_style: Style::default(),
@@ -233,7 +187,7 @@ impl<'a> Scrollbar<'a> {
}
/// Sets the symbol that represents the track of the scrollbar.
pub fn track_symbol(mut self, track_symbol: Option<&'a str>) -> Self {
pub fn track_symbol(mut self, track_symbol: &'a str) -> Self {
self.track_symbol = track_symbol;
self
}
@@ -279,13 +233,12 @@ impl<'a> Scrollbar<'a> {
/// └─────────── begin
/// ```
///
/// 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.
/// Only sets begin_symbol and end_symbol if they already contain a value.
/// If begin_symbol and/or end_symbol were set to `None` explicitly, this function will respect
/// that choice.
pub fn symbols(mut self, symbol: Set) -> Self {
self.track_symbol = symbol.track;
self.thumb_symbol = symbol.thumb;
if self.track_symbol.is_some() {
self.track_symbol = Some(symbol.track);
}
if self.begin_symbol.is_some() {
self.begin_symbol = Some(symbol.begin);
}
@@ -356,7 +309,7 @@ impl<'a> Scrollbar<'a> {
}
}
fn should_not_render(&self, track_start: u16, track_end: u16, content_length: usize) -> bool {
fn should_not_render(&self, track_start: u16, track_end: u16, content_length: u16) -> bool {
if track_end - track_start == 0 || content_length == 0 {
return true;
}
@@ -407,7 +360,7 @@ impl<'a> Scrollbar<'a> {
let (track_start, track_end) = track_start_end;
let viewport_content_length = if state.viewport_content_length == 0 {
(track_end - track_start) as usize
track_end - track_start
} else {
state.viewport_content_length
};
@@ -472,10 +425,8 @@ impl<'a> StatefulWidget for Scrollbar<'a> {
for i in track_start..track_end {
let (style, symbol) = if i >= thumb_start && i < thumb_end {
(self.thumb_style, self.thumb_symbol)
} else if let Some(track_symbol) = self.track_symbol {
(self.track_style, track_symbol)
} else {
continue;
(self.track_style, self.track_symbol)
};
if self.is_vertical() {
@@ -504,105 +455,12 @@ impl<'a> StatefulWidget for Scrollbar<'a> {
#[cfg(test)]
mod tests {
use strum::ParseError;
use super::*;
use crate::{
assert_buffer_eq,
symbols::scrollbar::{HORIZONTAL, VERTICAL},
};
#[test]
fn scroll_direction_to_string() {
assert_eq!(ScrollDirection::Forward.to_string(), "Forward");
assert_eq!(ScrollDirection::Backward.to_string(), "Backward");
}
#[test]
fn scroll_direction_from_str() {
assert_eq!(
"Forward".parse::<ScrollDirection>(),
Ok(ScrollDirection::Forward)
);
assert_eq!(
"Backward".parse::<ScrollDirection>(),
Ok(ScrollDirection::Backward)
);
assert_eq!(
"".parse::<ScrollDirection>(),
Err(ParseError::VariantNotFound)
);
}
#[test]
fn scrollbar_orientation_to_string() {
assert_eq!(
ScrollbarOrientation::VerticalRight.to_string(),
"VerticalRight"
);
assert_eq!(
ScrollbarOrientation::VerticalLeft.to_string(),
"VerticalLeft"
);
assert_eq!(
ScrollbarOrientation::HorizontalBottom.to_string(),
"HorizontalBottom"
);
assert_eq!(
ScrollbarOrientation::HorizontalTop.to_string(),
"HorizontalTop"
);
}
#[test]
fn scrollbar_orientation_from_str() {
assert_eq!(
"VerticalRight".parse::<ScrollbarOrientation>(),
Ok(ScrollbarOrientation::VerticalRight)
);
assert_eq!(
"VerticalLeft".parse::<ScrollbarOrientation>(),
Ok(ScrollbarOrientation::VerticalLeft)
);
assert_eq!(
"HorizontalBottom".parse::<ScrollbarOrientation>(),
Ok(ScrollbarOrientation::HorizontalBottom)
);
assert_eq!(
"HorizontalTop".parse::<ScrollbarOrientation>(),
Ok(ScrollbarOrientation::HorizontalTop)
);
assert_eq!(
"".parse::<ScrollbarOrientation>(),
Err(ParseError::VariantNotFound)
);
}
#[test]
fn test_renders_empty_with_content_length_is_zero() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 2, 8));
let mut state = ScrollbarState::default().position(0);
Scrollbar::default()
.begin_symbol(None)
.end_symbol(None)
.render(buffer.area, &mut buffer, &mut state);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![" ", " ", " ", " ", " ", " ", " ", " "])
);
let mut buffer = Buffer::empty(Rect::new(0, 0, 2, 8));
let mut state = ScrollbarState::new(8).position(0);
Scrollbar::default()
.begin_symbol(None)
.end_symbol(None)
.render(buffer.area, &mut buffer, &mut state);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec!["", "", "", "", "", "", "", ""])
);
}
#[test]
fn test_no_render_when_area_zero() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 0, 0));
@@ -977,28 +835,4 @@ mod tests {
assert_buffer_eq!(buffer, Buffer::with_lines(expected.clone()));
}
}
#[test]
fn test_rendering_without_track_horizontal_bottom() {
for i in 0..=16 {
let mut buffer = Buffer::empty(Rect::new(0, 0, 8, 2));
let mut state = ScrollbarState::default().position(i).content_length(16);
Scrollbar::default()
.orientation(ScrollbarOrientation::HorizontalBottom)
.track_symbol(None)
.render(buffer.area, &mut buffer, &mut state);
let expected = if i <= 1 {
vec![" ", "◄██ ►"]
} else if i <= 5 {
vec![" ", "◄ ██ ►"]
} else if i <= 9 {
vec![" ", "◄ ██ ►"]
} else if i <= 13 {
vec![" ", "◄ ██ ►"]
} else {
vec![" ", "◄ ██►"]
};
assert_buffer_eq!(buffer, Buffer::with_lines(expected.clone()));
}
}
}

View File

@@ -1,7 +1,5 @@
use std::cmp::min;
use strum::{Display, EnumString};
use crate::{
buffer::Buffer,
layout::Rect,
@@ -15,15 +13,15 @@ use crate::{
/// # Examples
///
/// ```
/// use ratatui::{prelude::*, widgets::*};
///
/// # use ratatui::widgets::{Block, Borders, Sparkline};
/// # use ratatui::style::{Style, Color};
/// Sparkline::default()
/// .block(Block::default().title("Sparkline").borders(Borders::ALL))
/// .data(&[0, 2, 3, 4, 1, 4, 10])
/// .max(5)
/// .style(Style::default().fg(Color::Red).bg(Color::White));
/// ```
#[derive(Debug, Clone, Eq, PartialEq)]
#[derive(Debug, Clone)]
pub struct Sparkline<'a> {
/// A block to wrap the widget in
block: Option<Block<'a>>,
@@ -40,7 +38,7 @@ pub struct Sparkline<'a> {
direction: RenderDirection,
}
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
#[derive(Debug, Default, Clone, Copy)]
pub enum RenderDirection {
#[default]
LeftToRight,
@@ -169,8 +167,6 @@ impl<'a> Widget for Sparkline<'a> {
#[cfg(test)]
mod tests {
use strum::ParseError;
use super::*;
use crate::{
assert_buffer_eq,
@@ -178,28 +174,6 @@ mod tests {
style::{Color, Modifier, Stylize},
};
#[test]
fn render_direction_to_string() {
assert_eq!(RenderDirection::LeftToRight.to_string(), "LeftToRight");
assert_eq!(RenderDirection::RightToLeft.to_string(), "RightToLeft");
}
#[test]
fn render_direction_from_str() {
assert_eq!(
"LeftToRight".parse::<RenderDirection>(),
Ok(RenderDirection::LeftToRight)
);
assert_eq!(
"RightToLeft".parse::<RenderDirection>(),
Ok(RenderDirection::RightToLeft)
);
assert_eq!(
"".parse::<RenderDirection>(),
Err(ParseError::VariantNotFound)
);
}
// Helper function to render a sparkline to a buffer with a given width
// filled with x symbols to make it easier to assert on the result
fn render(widget: Sparkline, width: u16) -> Buffer {

View File

@@ -1,9 +1,8 @@
use strum::{Display, EnumString};
use unicode_width::UnicodeWidthStr;
use crate::{
buffer::Buffer,
layout::{Alignment, Constraint, Direction, Layout, Rect, SegmentSize},
layout::{Constraint, Direction, Layout, Rect},
style::{Style, Styled},
text::Text,
widgets::{Block, StatefulWidget, Widget},
@@ -13,9 +12,10 @@ use crate::{
///
/// It can be created from anything that can be converted to a [`Text`].
/// ```rust
/// use std::borrow::Cow;
/// use ratatui::{prelude::*, widgets::*};
///
/// # use ratatui::widgets::Cell;
/// # use ratatui::style::{Style, Modifier};
/// # use ratatui::text::{Span, Line, Text};
/// # use std::borrow::Cow;
/// Cell::from("simple string");
///
/// Cell::from(Span::from("span"));
@@ -32,7 +32,7 @@ use crate::{
///
/// You can apply a [`Style`] on the entire [`Cell`] using [`Cell::style`] or rely on the styling
/// capabilities of [`Text`].
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct Cell<'a> {
content: Text<'a>,
style: Style,
@@ -74,15 +74,14 @@ impl<'a> Styled for Cell<'a> {
///
/// A [`Row`] is a collection of cells. It can be created from simple strings:
/// ```rust
/// use ratatui::{prelude::*, widgets::*};
///
/// # use ratatui::widgets::Row;
/// Row::new(vec!["Cell1", "Cell2", "Cell3"]);
/// ```
///
/// But if you need a bit more control over individual cells, you can explicitly create [`Cell`]s:
/// ```rust
/// use ratatui::{prelude::*, widgets::*};
///
/// # use ratatui::widgets::{Row, Cell};
/// # use ratatui::style::{Style, Color};
/// Row::new(vec![
/// Cell::from("Cell1"),
/// Cell::from("Cell2").style(Style::default().fg(Color::Yellow)),
@@ -91,9 +90,8 @@ impl<'a> Styled for Cell<'a> {
///
/// You can also construct a row from any type that can be converted into [`Text`]:
/// ```rust
/// use std::borrow::Cow;
/// use ratatui::{prelude::*, widgets::*};
///
/// # use std::borrow::Cow;
/// # use ratatui::widgets::Row;
/// Row::new(vec![
/// Cow::Borrowed("hello"),
/// Cow::Owned("world".to_uppercase()),
@@ -101,7 +99,7 @@ impl<'a> Styled for Cell<'a> {
/// ```
///
/// By default, a row has a height of 1 but you can change this using [`Row::height`].
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct Row<'a> {
cells: Vec<Cell<'a>>,
height: u16,
@@ -162,45 +160,14 @@ impl<'a> Styled for Row<'a> {
}
}
/// This option allows the user to configure the "highlight symbol" column width spacing
#[derive(Debug, Display, EnumString, PartialEq, Eq, Clone, Default, Hash)]
pub enum HighlightSpacing {
/// Always add spacing for the selection symbol column
///
/// With this variant, the column for the selection symbol will always be allocated, and so the
/// table will never change size, regardless of if a row is selected or not
Always,
/// Only add spacing for the selection symbol column if a row is selected
///
/// With this variant, the column for the selection symbol will only be allocated if there is a
/// selection, causing the table to shift if selected / unselected
#[default]
WhenSelected,
/// Never add spacing to the selection symbol column, regardless of whether something is
/// selected or not
///
/// This means that the highlight symbol will never be drawn
Never,
}
impl HighlightSpacing {
/// Determine if a selection should be done, based on variant
/// Input "selection_state" should be similar to `state.selected.is_some()`
pub fn should_add(&self, selection_state: bool) -> bool {
match self {
HighlightSpacing::Always => true,
HighlightSpacing::WhenSelected => selection_state,
HighlightSpacing::Never => false,
}
}
}
/// A widget to display data in formatted columns.
///
/// It is a collection of [`Row`]s, themselves composed of [`Cell`]s:
/// ```rust
/// use ratatui::{prelude::*, widgets::*};
///
/// # use ratatui::widgets::{Block, Borders, Table, Row, Cell};
/// # use ratatui::layout::Constraint;
/// # use ratatui::style::{Style, Color, Modifier};
/// # use ratatui::text::{Text, Line, Span};
/// Table::new(vec![
/// // Row can be created from simple strings.
/// Row::new(vec!["Row11", "Row12", "Row13"]),
@@ -244,7 +211,7 @@ impl HighlightSpacing {
/// // ...and potentially show a symbol in front of the selection.
/// .highlight_symbol(">>");
/// ```
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
#[derive(Debug, Default, Clone, Eq, PartialEq)]
pub struct Table<'a> {
/// A block to wrap the widget in
block: Option<Block<'a>>,
@@ -262,31 +229,9 @@ pub struct Table<'a> {
header: Option<Row<'a>>,
/// Data to display in each row
rows: Vec<Row<'a>>,
/// Decides when to allocate spacing for the row selection
highlight_spacing: HighlightSpacing,
}
impl<'a> Table<'a> {
/// Creates a new [`Table`] widget with the given rows.
///
/// The `rows` parameter is a Vector of [`Row`], this holds the data to be displayed by the
/// table
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let table = Table::new(vec![
/// Row::new(vec![
/// Cell::from("Cell1"),
/// Cell::from("Cell2")
/// ]),
/// Row::new(vec![
/// Cell::from("Cell3"),
/// Cell::from("Cell4")
/// ]),
/// ]);
/// ```
pub fn new<T>(rows: T) -> Self
where
T: IntoIterator<Item = Row<'a>>,
@@ -300,7 +245,6 @@ impl<'a> Table<'a> {
highlight_symbol: None,
header: None,
rows: rows.into_iter().collect(),
highlight_spacing: HighlightSpacing::default(),
}
}
@@ -342,24 +286,17 @@ impl<'a> Table<'a> {
self
}
/// Set when to show the highlight spacing
///
/// See [`HighlightSpacing`] about which variant affects spacing in which way
pub fn highlight_spacing(mut self, value: HighlightSpacing) -> Self {
self.highlight_spacing = value;
self
}
pub fn column_spacing(mut self, spacing: u16) -> Self {
self.column_spacing = spacing;
self
}
/// Get all offsets and widths of all user specified columns
/// Returns (x, width)
fn get_columns_widths(&self, max_width: u16, selection_width: u16) -> Vec<(u16, u16)> {
fn get_columns_widths(&self, max_width: u16, has_selection: bool) -> Vec<u16> {
let mut constraints = Vec::with_capacity(self.widths.len() * 2 + 1);
constraints.push(Constraint::Length(selection_width));
if has_selection {
let highlight_symbol_width = self.highlight_symbol.map_or(0, |s| s.width() as u16);
constraints.push(Constraint::Length(highlight_symbol_width));
}
for constraint in self.widths {
constraints.push(*constraint);
constraints.push(Constraint::Length(self.column_spacing));
@@ -370,19 +307,18 @@ impl<'a> Table<'a> {
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints(constraints)
.segment_size(SegmentSize::None)
.expand_to_fill(false)
.split(Rect {
x: 0,
y: 0,
width: max_width,
height: 1,
});
chunks
.iter()
.skip(1)
.step_by(2)
.map(|c| (c.x, c.width))
.collect()
let mut chunks = &chunks[..];
if has_selection {
chunks = &chunks[1..];
}
chunks.iter().step_by(2).map(|c| c.width).collect()
}
fn get_row_bounds(
@@ -436,7 +372,7 @@ impl<'a> Styled for Table<'a> {
}
}
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
#[derive(Debug, Default, Clone)]
pub struct TableState {
offset: usize,
selected: Option<usize>,
@@ -490,13 +426,10 @@ impl<'a> StatefulWidget for Table<'a> {
None => area,
};
let selection_width = if self.highlight_spacing.should_add(state.selected.is_some()) {
self.highlight_symbol.map_or(0, |s| s.width() as u16)
} else {
0
};
let columns_widths = self.get_columns_widths(table_area.width, selection_width);
let has_selection = state.selected.is_some();
let columns_widths = self.get_columns_widths(table_area.width, has_selection);
let highlight_symbol = self.highlight_symbol.unwrap_or("");
let blank_symbol = " ".repeat(highlight_symbol.width());
let mut current_height = 0;
let mut rows_height = table_area.height;
@@ -512,18 +445,22 @@ impl<'a> StatefulWidget for Table<'a> {
},
header.style,
);
let inner_offset = table_area.left();
for ((x, width), cell) in columns_widths.iter().zip(header.cells.iter()) {
let mut col = table_area.left();
if has_selection {
col += (highlight_symbol.width() as u16).min(table_area.width);
}
for (width, cell) in columns_widths.iter().zip(header.cells.iter()) {
render_cell(
buf,
cell,
Rect {
x: inner_offset + x,
x: col,
y: table_area.top(),
width: *width,
height: max_header_height,
},
);
col += *width + self.column_spacing;
}
current_height += max_header_height;
rows_height = rows_height.saturating_sub(max_header_height);
@@ -542,39 +479,41 @@ impl<'a> StatefulWidget for Table<'a> {
.skip(state.offset)
.take(end - start)
{
let (row, inner_offset) = (table_area.top() + current_height, table_area.left());
let (row, col) = (table_area.top() + current_height, table_area.left());
current_height += table_row.total_height();
let table_row_area = Rect {
x: inner_offset,
x: col,
y: row,
width: table_area.width,
height: table_row.height,
};
buf.set_style(table_row_area, table_row.style);
let is_selected = state.selected.map_or(false, |s| s == i);
if selection_width > 0 && is_selected {
// this should in normal cases be safe, because "get_columns_widths" allocates
// "highlight_symbol.width()" space but "get_columns_widths"
// currently does not bind it to max table.width()
buf.set_stringn(
inner_offset,
row,
highlight_symbol,
table_area.width as usize,
table_row.style,
);
let table_row_start_col = if has_selection {
let symbol = if is_selected {
highlight_symbol
} else {
&blank_symbol
};
let (col, _) =
buf.set_stringn(col, row, symbol, table_area.width as usize, table_row.style);
col
} else {
col
};
for ((x, width), cell) in columns_widths.iter().zip(table_row.cells.iter()) {
let mut col = table_row_start_col;
for (width, cell) in columns_widths.iter().zip(table_row.cells.iter()) {
render_cell(
buf,
cell,
Rect {
x: inner_offset + x,
x: col,
y: row,
width: *width,
height: table_row.height,
},
);
col += *width + self.column_spacing;
}
if is_selected {
buf.set_style(table_row_area, self.highlight_style);
@@ -589,14 +528,7 @@ fn render_cell(buf: &mut Buffer, cell: &Cell, area: Rect) {
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,
};
buf.set_line(area.x + x_offset, area.y + i as u16, line, area.width);
buf.set_line(area.x, area.y + i as u16, line, area.width);
}
}
@@ -612,145 +544,13 @@ mod tests {
use std::vec;
use super::*;
use crate::{
layout::Constraint::*,
style::{Color, Modifier, Style, Stylize},
text::Line,
};
use crate::style::{Color, Modifier, Style, Stylize};
#[test]
#[should_panic]
fn table_invalid_percentages() {
Table::new(vec![]).widths(&[Constraint::Percentage(110)]);
}
// test how constraints interact with table column width allocation
mod table_column_widths {
use super::*;
/// Construct a a new table with the given constraints, available and selection widths and
/// tests that the widths match the expected list of (x, width) tuples.
#[track_caller]
fn test(
constraints: &[Constraint],
available_width: u16,
selection_width: u16,
expected: &[(u16, u16)],
) {
let table = Table::new(vec![]).widths(constraints);
let widths = table.get_columns_widths(available_width, selection_width);
assert_eq!(widths, expected);
}
#[test]
fn length_constraint() {
// without selection, more than needed width
test(&[Length(4), Length(4)], 20, 0, &[(0, 4), (5, 4)]);
// with selection, more than needed width
test(&[Length(4), Length(4)], 20, 3, &[(3, 4), (8, 4)]);
// without selection, less than needed width
test(&[Length(4), Length(4)], 7, 0, &[(0, 4), (5, 2)]);
// with selection, less than needed width
test(&[Length(4), Length(4)], 7, 3, &[(3, 4), (7, 0)]);
}
#[test]
fn max_constraint() {
// without selection, more than needed width
test(&[Max(4), Max(4)], 20, 0, &[(0, 4), (5, 4)]);
// with selection, more than needed width
test(&[Max(4), Max(4)], 20, 3, &[(3, 4), (8, 4)]);
// without selection, less than needed width
test(&[Max(4), Max(4)], 7, 0, &[(0, 4), (5, 2)]);
// with selection, less than needed width
test(&[Max(4), Max(4)], 7, 3, &[(3, 3), (7, 0)]);
}
#[test]
fn min_constraint() {
// in its currently stage, the "Min" constraint does not grow to use the possible
// available length and enabling "expand_to_fill" will just stretch the last
// constraint and not split it with all available constraints
// without selection, more than needed width
test(&[Min(4), Min(4)], 20, 0, &[(0, 4), (5, 4)]);
// with selection, more than needed width
test(&[Min(4), Min(4)], 20, 3, &[(3, 4), (8, 4)]);
// without selection, less than needed width
// allocates no spacer
test(&[Min(4), Min(4)], 7, 0, &[(0, 4), (4, 3)]);
// with selection, less than needed width
// allocates no selection and no spacer
test(&[Min(4), Min(4)], 7, 3, &[(0, 4), (4, 3)]);
}
#[test]
fn percentage_constraint() {
// without selection, more than needed width
test(&[Percentage(30), Percentage(30)], 20, 0, &[(0, 6), (7, 6)]);
// with selection, more than needed width
test(&[Percentage(30), Percentage(30)], 20, 3, &[(3, 6), (10, 6)]);
// without selection, less than needed width
// rounds from positions: [0.0, 0.0, 2.1, 3.1, 5.2, 7.0]
test(&[Percentage(30), Percentage(30)], 7, 0, &[(0, 2), (3, 2)]);
// with selection, less than needed width
// rounds from positions: [0.0, 3.0, 5.1, 6.1, 7.0, 7.0]
test(&[Percentage(30), Percentage(30)], 7, 3, &[(3, 2), (6, 1)]);
}
#[test]
fn ratio_constraint() {
// without selection, more than needed width
// rounds from positions: [0.00, 0.00, 6.67, 7.67, 14.33]
test(&[Ratio(1, 3), Ratio(1, 3)], 20, 0, &[(0, 7), (8, 6)]);
// with selection, more than needed width
// rounds from positions: [0.00, 3.00, 10.67, 17.33, 20.00]
test(&[Ratio(1, 3), Ratio(1, 3)], 20, 3, &[(3, 7), (11, 6)]);
// without selection, less than needed width
// rounds from positions: [0.00, 2.33, 3.33, 5.66, 7.00]
test(&[Ratio(1, 3), Ratio(1, 3)], 7, 0, &[(0, 2), (3, 3)]);
// with selection, less than needed width
// rounds from positions: [0.00, 3.00, 5.33, 6.33, 7.00, 7.00]
test(&[Ratio(1, 3), Ratio(1, 3)], 7, 3, &[(3, 2), (6, 1)]);
}
}
#[test]
fn test_render_table_with_alignment() {
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 3));
let table = Table::new(vec![
Row::new(vec![Line::from("Left").alignment(Alignment::Left)]),
Row::new(vec![Line::from("Center").alignment(Alignment::Center)]),
Row::new(vec![Line::from("Right").alignment(Alignment::Right)]),
])
.widths(&[Percentage(100)]);
Widget::render(table, Rect::new(0, 0, 20, 3), &mut buf);
let expected = Buffer::with_lines(vec![
"Left ",
" Center ",
" Right",
]);
assert_eq!(buf, expected);
}
#[test]
fn cell_can_be_stylized() {
assert_eq!(
@@ -796,34 +596,4 @@ mod tests {
.remove_modifier(Modifier::CROSSED_OUT)
)
}
#[test]
fn highlight_spacing_to_string() {
assert_eq!(HighlightSpacing::Always.to_string(), "Always".to_string());
assert_eq!(
HighlightSpacing::WhenSelected.to_string(),
"WhenSelected".to_string()
);
assert_eq!(HighlightSpacing::Never.to_string(), "Never".to_string());
}
#[test]
fn highlight_spacing_from_str() {
assert_eq!(
"Always".parse::<HighlightSpacing>(),
Ok(HighlightSpacing::Always)
);
assert_eq!(
"WhenSelected".parse::<HighlightSpacing>(),
Ok(HighlightSpacing::WhenSelected)
);
assert_eq!(
"Never".parse::<HighlightSpacing>(),
Ok(HighlightSpacing::Never)
);
assert_eq!(
"".parse::<HighlightSpacing>(),
Err(strum::ParseError::VariantNotFound)
);
}
}

View File

@@ -12,16 +12,18 @@ use crate::{
/// # Examples
///
/// ```
/// use ratatui::{prelude::*, widgets::*};
///
/// # use ratatui::widgets::{Block, Borders, Tabs};
/// # use ratatui::style::{Style, Color};
/// # use ratatui::text::{Line};
/// # use ratatui::symbols::{DOT};
/// let titles = ["Tab1", "Tab2", "Tab3", "Tab4"].iter().cloned().map(Line::from).collect();
/// Tabs::new(titles)
/// .block(Block::default().title("Tabs").borders(Borders::ALL))
/// .style(Style::default().fg(Color::White))
/// .highlight_style(Style::default().fg(Color::Yellow))
/// .divider(symbols::DOT);
/// .divider(DOT);
/// ```
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
#[derive(Debug, Default, Clone)]
pub struct Tabs<'a> {
/// A block to wrap this widget in if necessary
block: Option<Block<'a>>,
@@ -144,135 +146,7 @@ impl<'a> Widget for Tabs<'a> {
#[cfg(test)]
mod tests {
use super::*;
use crate::{assert_buffer_eq, prelude::*, widgets::Borders};
#[test]
fn new() {
let titles = vec!["Tab1", "Tab2", "Tab3", "Tab4"];
let tabs = Tabs::new(titles.clone());
assert_eq!(
tabs,
Tabs {
block: None,
titles: vec![
Line::from("Tab1"),
Line::from("Tab2"),
Line::from("Tab3"),
Line::from("Tab4"),
],
selected: 0,
style: Style::default(),
highlight_style: Style::default(),
divider: Span::raw(symbols::line::VERTICAL),
}
);
}
fn render(tabs: Tabs, area: Rect) -> Buffer {
let mut buffer = Buffer::empty(area);
tabs.render(area, &mut buffer);
buffer
}
#[test]
fn render_default() {
let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"]);
assert_buffer_eq!(
render(tabs, Rect::new(0, 0, 30, 1)),
Buffer::with_lines(vec![" Tab1 │ Tab2 │ Tab3 │ Tab4 "])
);
}
#[test]
fn render_with_block() {
let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"])
.block(Block::default().title("Tabs").borders(Borders::ALL));
assert_buffer_eq!(
render(tabs, Rect::new(0, 0, 30, 3)),
Buffer::with_lines(vec![
"┌Tabs────────────────────────┐",
"│ Tab1 │ Tab2 │ Tab3 │ Tab4 │",
"└────────────────────────────┘",
])
);
}
#[test]
fn render_style() {
let tabs =
Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"]).style(Style::default().fg(Color::Red));
assert_buffer_eq!(
render(tabs, Rect::new(0, 0, 30, 1)),
Buffer::with_lines(vec![" Tab1 │ Tab2 │ Tab3 │ Tab4 ".red()])
);
}
#[test]
fn render_select() {
let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"])
.highlight_style(Style::new().reversed());
// first tab selected
assert_buffer_eq!(
render(tabs.clone().select(0), Rect::new(0, 0, 30, 1)),
Buffer::with_lines(vec![Line::from(vec![
" ".into(),
"Tab1".reversed(),
" │ Tab2 │ Tab3 │ Tab4 ".into(),
])])
);
// second tab selected
assert_buffer_eq!(
render(tabs.clone().select(1), Rect::new(0, 0, 30, 1)),
Buffer::with_lines(vec![Line::from(vec![
" Tab1 │ ".into(),
"Tab2".reversed(),
" │ Tab3 │ Tab4 ".into(),
])])
);
// last tab selected
assert_buffer_eq!(
render(tabs.clone().select(3), Rect::new(0, 0, 30, 1)),
Buffer::with_lines(vec![Line::from(vec![
" Tab1 │ Tab2 │ Tab3 │ ".into(),
"Tab4".reversed(),
" ".into(),
])])
);
// out of bounds selects no tab
assert_buffer_eq!(
render(tabs.clone().select(4), Rect::new(0, 0, 30, 1)),
Buffer::with_lines(vec![" Tab1 │ Tab2 │ Tab3 │ Tab4 "])
);
}
#[test]
fn render_style_and_selected() {
let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"])
.style(Style::new().red())
.highlight_style(Style::new().reversed())
.select(0);
assert_buffer_eq!(
render(tabs, Rect::new(0, 0, 30, 1)),
Buffer::with_lines(vec![Line::from(vec![
" ".red(),
"Tab1".red().reversed(),
" │ Tab2 │ Tab3 │ Tab4 ".red(),
])])
);
}
#[test]
fn render_divider() {
let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"]).divider("--");
assert_buffer_eq!(
render(tabs, Rect::new(0, 0, 30, 1)),
Buffer::with_lines(vec![" Tab1 -- Tab2 -- Tab3 -- Tab4 ",])
);
}
use crate::style::{Color, Modifier, Stylize};
#[test]
fn can_be_stylized() {

View File

@@ -89,7 +89,7 @@ fn widgets_barchart_group() {
"│ ▄▄▄▄ ████ ████ ████ ████│",
"│▆10▆ 20M█ █50█ █40█ █60█ █90█│",
"│ C1 C1 C2 C1 C2 │",
"Mar",
"Mar",
"└─────────────────────────────────┘",
]);

View File

@@ -4,7 +4,7 @@ use ratatui::{
layout::{Alignment, Rect},
style::{Color, Style},
symbols,
text::{self, Span},
text::Span,
widgets::{Axis, Block, Borders, Chart, Dataset, GraphType::Line},
Terminal,
};
@@ -13,9 +13,9 @@ fn create_labels<'a>(labels: &'a [&'a str]) -> Vec<Span<'a>> {
labels.iter().map(|l| Span::from(*l)).collect()
}
fn axis_test_case<'a, S>(width: u16, height: u16, x_axis: Axis, y_axis: Axis, lines: Vec<S>)
fn axis_test_case<S>(width: u16, height: u16, x_axis: Axis, y_axis: Axis, lines: Vec<S>)
where
S: Into<text::Line<'a>>,
S: AsRef<str>,
{
let backend = TestBackend::new(width, height);
let mut terminal = Terminal::new(backend).unwrap();
@@ -25,7 +25,6 @@ where
f.render_widget(chart, f.size());
})
.unwrap();
let lines = lines.into_iter().map(|l| l.into()).collect();
let expected = Buffer::with_lines(lines);
terminal.backend().assert_buffer(&expected);
}
@@ -619,41 +618,3 @@ fn widgets_chart_can_have_a_legend() {
terminal.backend().assert_buffer(&expected);
}
#[test]
fn widgets_chart_top_line_styling_is_correct() {
let backend = TestBackend::new(9, 5);
let mut terminal = Terminal::new(backend).unwrap();
let title_style = Style::default().fg(Color::Red).bg(Color::LightRed);
let data_style = Style::default().fg(Color::Blue);
terminal
.draw(|f| {
let data: [(f64, f64); 2] = [(0.0, 1.0), (1.0, 1.0)];
let widget = Chart::new(vec![Dataset::default()
.data(&data)
.graph_type(ratatui::widgets::GraphType::Line)
.style(data_style)])
.y_axis(
Axis::default()
.title(Span::styled("abc", title_style))
.bounds([0.0, 1.0])
.labels(create_labels(&["a", "b"])),
)
.x_axis(Axis::default().bounds([0.0, 1.0]));
f.render_widget(widget, f.size());
})
.unwrap();
let mut expected = Buffer::with_lines(vec![
"b│abc••••",
"",
"",
"",
"a│ ",
]);
expected.set_style(Rect::new(2, 0, 3, 1), title_style);
expected.set_style(Rect::new(5, 0, 4, 1), data_style);
terminal.backend().assert_buffer(&expected);
}

View File

@@ -7,7 +7,7 @@ use ratatui::{
style::{Color, Style},
symbols,
text::Line,
widgets::{Block, Borders, HighlightSpacing, List, ListItem, ListState},
widgets::{Block, Borders, List, ListItem, ListState},
Terminal,
};
@@ -243,128 +243,3 @@ fn widget_list_should_not_ignore_empty_string_items() {
terminal.backend().assert_buffer(&expected);
}
#[test]
fn widgets_list_enable_always_highlight_spacing() {
let test_case = |state: &mut ListState, space: HighlightSpacing, expected: Buffer| {
let backend = TestBackend::new(30, 8);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let size = f.size();
let table = List::new(vec![
ListItem::new(vec![Line::from("Item 1"), Line::from("Item 1a")]),
ListItem::new(vec![Line::from("Item 2"), Line::from("Item 2b")]),
ListItem::new(vec![Line::from("Item 3"), Line::from("Item 3c")]),
])
.block(Block::default().borders(Borders::ALL))
.highlight_symbol(">> ")
.highlight_spacing(space);
f.render_stateful_widget(table, size, state);
})
.unwrap();
terminal.backend().assert_buffer(&expected);
};
assert_eq!(HighlightSpacing::default(), HighlightSpacing::WhenSelected);
let mut state = ListState::default();
// no selection, "WhenSelected" should only allocate if selected
test_case(
&mut state,
HighlightSpacing::default(),
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│Item 1 │",
"│Item 1a │",
"│Item 2 │",
"│Item 2b │",
"│Item 3 │",
"│Item 3c │",
"└────────────────────────────┘",
]),
);
// no selection, "Always" should allocate regardless if selected or not
test_case(
&mut state,
HighlightSpacing::Always,
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│ Item 1 │",
"│ Item 1a │",
"│ Item 2 │",
"│ Item 2b │",
"│ Item 3 │",
"│ Item 3c │",
"└────────────────────────────┘",
]),
);
// no selection, "Never" should never allocate regadless if selected or not
test_case(
&mut state,
HighlightSpacing::Never,
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│Item 1 │",
"│Item 1a │",
"│Item 2 │",
"│Item 2b │",
"│Item 3 │",
"│Item 3c │",
"└────────────────────────────┘",
]),
);
// select first, "WhenSelected" should only allocate if selected
state.select(Some(0));
test_case(
&mut state,
HighlightSpacing::default(),
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│>> Item 1 │",
"│ Item 1a │",
"│ Item 2 │",
"│ Item 2b │",
"│ Item 3 │",
"│ Item 3c │",
"└────────────────────────────┘",
]),
);
// select first, "Always" should allocate regardless if selected or not
state.select(Some(0));
test_case(
&mut state,
HighlightSpacing::Always,
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│>> Item 1 │",
"│ Item 1a │",
"│ Item 2 │",
"│ Item 2b │",
"│ Item 3 │",
"│ Item 3c │",
"└────────────────────────────┘",
]),
);
// select first, "Never" should never allocate regadless if selected or not
state.select(Some(0));
test_case(
&mut state,
HighlightSpacing::Never,
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│Item 1 │",
"│Item 1a │",
"│Item 2 │",
"│Item 2b │",
"│Item 3 │",
"│Item 3c │",
"└────────────────────────────┘",
]),
);
}

Some files were not shown because too many files have changed in this diff Show More