Compare commits
2 Commits
v0.23.1-al
...
v0.20.0-al
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c70a84b3fe | ||
|
|
8ed244eeb9 |
34
.github/workflows/cd.yml
vendored
34
.github/workflows/cd.yml
vendored
@@ -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
|
||||
|
||||
83
.github/workflows/check-pr.yml
vendored
83
.github/workflows/check-pr.yml
vendored
@@ -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
|
||||
|
||||
27
.github/workflows/ci.yml
vendored
27
.github/workflows/ci.yml
vendored
@@ -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
|
||||
|
||||
@@ -7,6 +7,3 @@ no-inline-html:
|
||||
- summary
|
||||
line-length:
|
||||
line_length: 100
|
||||
|
||||
# to support repeated headers in the changelog
|
||||
no-duplicate-heading: false
|
||||
|
||||
881
CHANGELOG.md
881
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
111
Cargo.toml
111
Cargo.toml
@@ -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"]
|
||||
|
||||
146
Makefile.toml
146
Makefile.toml
@@ -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
|
||||
|
||||
34
README.md
34
README.md
@@ -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.
|
||||
|
||||
[](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)
|
||||
[](https://app.codecov.io/gh/ratatui-org/ratatui)
|
||||
[](https://discord.gg/pMCEU9hNEj)
|
||||
[](https://matrix.to/#/#ratatui:matrix.org)
|
||||
|
||||
<!-- See RELEASE.md for instructions on creating the demo gif --->
|
||||

|
||||

|
||||
|
||||
<details>
|
||||
<summary>Table of Contents</summary>
|
||||
@@ -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
|
||||
|
||||
17
RELEASE.md
17
RELEASE.md
@@ -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 |
BIN
assets/logo.png
BIN
assets/logo.png
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
107
cliff.toml
107
cliff.toml
@@ -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
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
ignore:
|
||||
- "examples"
|
||||
- "benches"
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(¶graph, Borders::ALL, frame, layout[0][0]);
|
||||
render_borders(¶graph, Borders::NONE, frame, layout[0][1]);
|
||||
render_borders(¶graph, Borders::LEFT, frame, layout[1][0]);
|
||||
render_borders(¶graph, Borders::RIGHT, frame, layout[1][1]);
|
||||
render_borders(¶graph, Borders::TOP, frame, layout[2][0]);
|
||||
render_borders(¶graph, Borders::BOTTOM, frame, layout[2][1]);
|
||||
|
||||
render_border_type(¶graph, BorderType::Plain, frame, layout[3][0]);
|
||||
render_border_type(¶graph, BorderType::Rounded, frame, layout[3][1]);
|
||||
render_border_type(¶graph, BorderType::Double, frame, layout[4][0]);
|
||||
render_border_type(¶graph, BorderType::Thick, frame, layout[4][1]);
|
||||
|
||||
render_styled_block(¶graph, frame, layout[5][0]);
|
||||
render_styled_borders(¶graph, frame, layout[5][1]);
|
||||
render_styled_title(¶graph, frame, layout[6][0]);
|
||||
render_styled_title_content(¶graph, frame, layout[6][1]);
|
||||
render_multiple_titles(¶graph, frame, layout[7][0]);
|
||||
render_multiple_title_positions(¶graph, frame, layout[7][1]);
|
||||
render_padding(¶graph, frame, layout[8][0]);
|
||||
render_nested_blocks(¶graph, 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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
[toolchain]
|
||||
channel = "stable"
|
||||
336
src/backend.rs
336
src/backend.rs
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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
117
src/backend/mod.rs
Normal 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>;
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
132
src/buffer.rs
132
src/buffer.rs
@@ -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()]));
|
||||
}
|
||||
}
|
||||
|
||||
1580
src/layout.rs
1580
src/layout.rs
File diff suppressed because it is too large
Load Diff
72
src/lib.rs
72
src/lib.rs
@@ -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.
|
||||
//!
|
||||
//! 
|
||||
//! 
|
||||
//!
|
||||
//! # 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;
|
||||
|
||||
@@ -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},
|
||||
};
|
||||
|
||||
384
src/style.rs
384
src/style.rs
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
421
src/terminal.rs
421
src/terminal.rs
@@ -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)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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 { .. } }
|
||||
261
src/text/span.rs
261
src/text/span.rs
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
/// ```
|
||||
|
||||
241
src/text/text.rs
241
src/text/text.rs
@@ -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"),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
100
src/title.rs
100
src/title.rs
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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![
|
||||
"• ",
|
||||
"• ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -89,7 +89,7 @@ fn widgets_barchart_group() {
|
||||
"│ ▄▄▄▄ ████ ████ ████ ████│",
|
||||
"│▆10▆ 20M█ █50█ █40█ █60█ █90█│",
|
||||
"│ C1 C1 C2 C1 C2 │",
|
||||
"│Mar │",
|
||||
"│ Mar │",
|
||||
"└─────────────────────────────────┘",
|
||||
]);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user