Compare commits
50 Commits
v0.22.1-al
...
v0.23.1-al
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0696f484e8 | ||
|
|
927a5d8251 | ||
|
|
28e7fd4bc5 | ||
|
|
28c61571e8 | ||
|
|
d0779034e7 | ||
|
|
51fdcbe7e9 | ||
|
|
eda2fb7077 | ||
|
|
3f781cad0a | ||
|
|
fc727df7d2 | ||
|
|
47fe4ad69f | ||
|
|
7a70602ec6 | ||
|
|
14eb6b6979 | ||
|
|
6009844e25 | ||
|
|
8b36683571 | ||
|
|
e9bd736b1a | ||
|
|
a890f2ac00 | ||
|
|
b35f19ec44 | ||
|
|
ad3413eeec | ||
|
|
f0716edbcf | ||
|
|
fc9f637fb0 | ||
|
|
292a11d81e | ||
|
|
ad4d6e7dec | ||
|
|
e4bcf78afa | ||
|
|
d0ee04a69f | ||
|
|
6d6eceeb88 | ||
|
|
0dca6a689a | ||
|
|
a937500ae4 | ||
|
|
80fd77e476 | ||
|
|
98155dce25 | ||
|
|
1ba2246d95 | ||
|
|
57ea871753 | ||
|
|
61533712be | ||
|
|
dc552116cf | ||
|
|
ab5e616635 | ||
|
|
b6b2da5eb7 | ||
|
|
89ef0e29f5 | ||
|
|
4cd843eda9 | ||
|
|
d2429bc3e4 | ||
|
|
b090101b23 | ||
|
|
56455e0fee | ||
|
|
f4ed3b7584 | ||
|
|
c86924b925 | ||
|
|
de25de0a95 | ||
|
|
ea48af1c9a | ||
|
|
418ed20479 | ||
|
|
519509945b | ||
|
|
8c55158822 | ||
|
|
7748720963 | ||
|
|
4d70169bef | ||
|
|
10dbd6f207 |
2
.github/workflows/cd.yml
vendored
2
.github/workflows/cd.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
- name: Calculate the next release
|
||||
run: |
|
||||
suffix="alpha"
|
||||
last_tag="$(git describe --abbrev=0 --tags `git rev-list --tags --max-count=1`)"
|
||||
last_tag="$(git tag --sort=committerdate | tail -1)"
|
||||
if [[ "${last_tag}" = *"-${suffix}"* ]]; then
|
||||
# increment the alpha version
|
||||
# e.g. v0.22.1-alpha.12 -> v0.22.1-alpha.13
|
||||
|
||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: CI
|
||||
name: Continuous Integration
|
||||
|
||||
on:
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
|
||||
@@ -7,3 +7,6 @@ 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
@@ -113,6 +113,57 @@ 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
|
||||
|
||||
61
Cargo.toml
61
Cargo.toml
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "ratatui"
|
||||
version = "0.22.0" # crate version
|
||||
version = "0.23.0" # crate version
|
||||
authors = ["Florian Dehau <work@fdehau.com>", "The Ratatui Developers"]
|
||||
description = "A library to build rich terminal user interfaces or dashboards"
|
||||
description = "A library that's all about cooking up terminal user interfaces"
|
||||
documentation = "https://docs.rs/ratatui/latest/ratatui/"
|
||||
keywords = ["tui", "terminal", "dashboard"]
|
||||
repository = "https://github.com/ratatui-org/ratatui"
|
||||
@@ -22,31 +22,28 @@ rust-version = "1.67.0"
|
||||
|
||||
[badges]
|
||||
|
||||
[features]
|
||||
default = ["crossterm"]
|
||||
all-widgets = ["widget-calendar"]
|
||||
widget-calendar = ["time"]
|
||||
macros = []
|
||||
serde = ["dep:serde", "bitflags/serde"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
# see https://doc.rust-lang.org/nightly/rustdoc/scraped-examples.html
|
||||
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[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"
|
||||
crossterm = { version = "0.27", optional = true }
|
||||
indoc = "2.0"
|
||||
itertools = "0.11"
|
||||
paste = "1.0.2"
|
||||
serde = { version = "1", optional = true, features = ["derive"] }
|
||||
termion = { version = "2.0", optional = true }
|
||||
termwiz = { version = "0.20.0", optional = true }
|
||||
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 }
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = "1.0.71"
|
||||
@@ -56,8 +53,32 @@ cargo-husky = { version = "1.5.0", default-features = false, features = [
|
||||
] }
|
||||
criterion = { version = "0.5", features = ["html_reports"] }
|
||||
fakeit = "1.1"
|
||||
itertools = "0.10"
|
||||
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"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
# see https://doc.rust-lang.org/nightly/rustdoc/scraped-examples.html
|
||||
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[[bench]]
|
||||
name = "block"
|
||||
|
||||
32
README.md
32
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 to build rich terminal user interfaces and
|
||||
dashboards. 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 that is all about cooking up terminal user interfaces.
|
||||
It is a community fork of the original [tui-rs](https://github.com/fdehau/tui-rs)
|
||||
project.
|
||||
|
||||
[](https://crates.io/crates/ratatui)
|
||||
@@ -14,9 +14,10 @@ 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>
|
||||
@@ -51,16 +52,7 @@ Or modify your `Cargo.toml`
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
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"]}
|
||||
ratatui = { version = "0.23.0", features = ["all-widgets"]}
|
||||
```
|
||||
|
||||
## Introduction
|
||||
@@ -140,10 +132,12 @@ 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 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. 😄
|
||||
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.
|
||||
|
||||
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.
|
||||
@@ -214,9 +208,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
|
||||
`tui::text::Text`
|
||||
`ratatui::text::Text`
|
||||
* [color-to-tui](https://github.com/uttarayan21/color-to-tui) — Parse hex colors to
|
||||
`tui::style::Color`
|
||||
`ratatui::style::Color`
|
||||
* [rust-tui-template](https://github.com/ratatui-org/rust-tui-template) — A template for bootstrapping a
|
||||
Rust TUI application with Tui-rs & crossterm
|
||||
* [simple-tui-rs](https://github.com/pmsanford/simple-tui-rs) — A simple example tui-rs app
|
||||
|
||||
16
RELEASE.md
16
RELEASE.md
@@ -3,22 +3,16 @@
|
||||
[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. 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).
|
||||
1. Record a new demo gif if necessary. The preferred tool for this is
|
||||
[vhs](https://github.com/charmbracelet/vhs) (installation instructions in README).
|
||||
|
||||
```shell
|
||||
cargo build --example demo
|
||||
ttyrec -e 'cargo --quiet run --release --example demo -- --tick-rate 100' demo.rec
|
||||
ttygif demo.rec
|
||||
vhs examples/demo.tape --publish --quiet
|
||||
```
|
||||
|
||||
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.
|
||||
Then update the link in the [examples README](./examples/README) and the main README. Avoid
|
||||
adding the gif to the git repo as binary files tend to bloat repositories.
|
||||
|
||||
1. Bump the version in [Cargo.toml](Cargo.toml).
|
||||
1. Bump versions in the doc comments of [lib.rs](src/lib.rs).
|
||||
|
||||
107
cliff.toml
107
cliff.toml
@@ -3,35 +3,51 @@
|
||||
[changelog]
|
||||
# changelog header
|
||||
header = """
|
||||
# Changelog\n
|
||||
All notable changes to this project will be documented in this file.\n
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
"""
|
||||
# template for the changelog body
|
||||
# https://tera.netlify.app/docs/#introduction
|
||||
# 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.
|
||||
body = """
|
||||
{% if version %}\
|
||||
## {{ version }} - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||
{% else %}\
|
||||
## [unreleased]
|
||||
{% endif %}\
|
||||
{%- 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.scope | default(value = "uncategorized")}})* {{ commit.message | upper_first }}
|
||||
([{{ commit.id | truncate(length=7, end="") }}]({{ "https://github.com/ratatui-org/ratatui/commit/" ~ commit.id }}))
|
||||
{%- if commit.breaking %} [**breaking**]{% endif %}
|
||||
{%- if commit.body %}
|
||||
|
||||
````text {#- 4 backticks escape any backticks in body #}
|
||||
{{commit.body | indent(prefix=" ") }}
|
||||
````
|
||||
{%- endif %}
|
||||
{% endmacro -%}
|
||||
|
||||
{% for group, commits in commits | group_by(attribute="group") %}
|
||||
### {{ 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
|
||||
### {{ 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 %}
|
||||
"""
|
||||
|
||||
|
||||
# remove the leading and trailing whitespace from the template
|
||||
trim = true
|
||||
trim = false
|
||||
# changelog footer
|
||||
footer = """
|
||||
<!-- generated by git-cliff -->
|
||||
@@ -46,29 +62,32 @@ 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 = "^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" },
|
||||
{ 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" },
|
||||
]
|
||||
# protect breaking changes from being skipped due to matching a skipping commit_parser
|
||||
protect_breaking_commits = false
|
||||
@@ -79,7 +98,7 @@ tag_pattern = "v[0-9]*"
|
||||
# regex for skipping tags
|
||||
skip_tags = "v0.1.0-rc.1"
|
||||
# regex for ignoring tags
|
||||
ignore_tags = ""
|
||||
ignore_tags = "alpha"
|
||||
# sort the tags topologically
|
||||
topo_order = false
|
||||
# sort the commits inside sections by oldest/newest order
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
ignore:
|
||||
- "examples"
|
||||
- "examples"
|
||||
|
||||
@@ -6,6 +6,18 @@ 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 ([demo.rs](./demo/))
|
||||
|
||||
This is the demo example from the main README. It is available for each of the backends.
|
||||
|
||||
```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]
|
||||
|
||||
## Barchart ([barchart.rs](./barchart.rs)
|
||||
|
||||
```shell
|
||||
@@ -211,10 +223,11 @@ done
|
||||
[chart.gif]: https://vhs.charm.sh/vhs-zRzsE2AwRixQhcWMTAeF1.gif
|
||||
[colors.gif]: https://vhs.charm.sh/vhs-2ZCqYbTbXAaASncUeWkt1z.gif
|
||||
[custom_widget.gif]: https://vhs.charm.sh/vhs-32mW1TpkrovTcm79QXmBSu.gif
|
||||
[demo.gif]: https://vhs.charm.sh/vhs-tF0QbuPbtHgUeG0sTVgFr.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
|
||||
[layout.gif]: https://vhs.charm.sh/vhs-1ZNoNLNlLtkJXpgg9nCV5e.gif
|
||||
[list.gif]: https://vhs.charm.sh/vhs-4goo9reeUM9r0nYb54R7SP.gif
|
||||
[modifiers.gif]: https://vhs.charm.sh/vhs-2ovGBz5l3tfRGdZ7FCw0am.gif
|
||||
[panic.gif]: https://vhs.charm.sh/vhs-HrvKCHV4yeN69fb1EadTH.gif
|
||||
|
||||
@@ -62,7 +62,7 @@ impl<'a> App<'a> {
|
||||
},
|
||||
Company {
|
||||
label: "Comp.B",
|
||||
revenue: [1500, 2500, 3000, 4100],
|
||||
revenue: [1500, 2500, 3000, 500],
|
||||
bar_style: Style::default().fg(Color::Yellow),
|
||||
},
|
||||
Company {
|
||||
@@ -140,14 +140,7 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(2)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Ratio(1, 3),
|
||||
Constraint::Ratio(1, 3),
|
||||
Constraint::Ratio(1, 3),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.constraints([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)].as_ref())
|
||||
.split(f.size());
|
||||
|
||||
let barchart = BarChart::default()
|
||||
@@ -158,16 +151,17 @@ 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]);
|
||||
|
||||
draw_bar_with_group_labels(f, app, chunks[1], false);
|
||||
draw_bar_with_group_labels(f, app, chunks[2], true);
|
||||
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]);
|
||||
}
|
||||
|
||||
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
|
||||
fn create_groups<'a>(app: &'a App, combine_values_and_labels: bool) -> Vec<BarGroup<'a>> {
|
||||
app.months
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, &month)| {
|
||||
@@ -182,17 +176,34 @@ where
|
||||
Style::default()
|
||||
.bg(c.bar_style.fg.unwrap())
|
||||
.fg(Color::Black),
|
||||
)
|
||||
.text_value(format!("{:.1}", (c.revenue[i] as f64) / 1000.));
|
||||
if bar_labels {
|
||||
bar = bar.label(c.label.into());
|
||||
);
|
||||
|
||||
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());
|
||||
}
|
||||
bar
|
||||
})
|
||||
.collect();
|
||||
BarGroup::default().label(month.into()).bars(&bars)
|
||||
BarGroup::default()
|
||||
.label(Line::from(month).alignment(Alignment::Center))
|
||||
.bars(&bars)
|
||||
})
|
||||
.collect();
|
||||
.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);
|
||||
|
||||
let mut barchart = BarChart::default()
|
||||
.block(Block::default().title("Data1").borders(Borders::ALL))
|
||||
@@ -207,11 +218,44 @@ 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: TOTAL_REVENUE.len() as u16 + 2,
|
||||
width: legend_width,
|
||||
y: area.y,
|
||||
x: area.x,
|
||||
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,
|
||||
};
|
||||
draw_legend(f, legend_area);
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ fn render_fg_named_colors<B: Backend>(frame: &mut Frame<B>, bg: Color, area: Rec
|
||||
.flat_map(|area| {
|
||||
Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(vec![Constraint::Percentage(13); 8])
|
||||
.constraints(vec![Constraint::Ratio(1, 8); 8])
|
||||
.split(*area)
|
||||
.to_vec()
|
||||
})
|
||||
@@ -132,7 +132,7 @@ fn render_bg_named_colors<B: Backend>(frame: &mut Frame<B>, fg: Color, area: Rec
|
||||
.flat_map(|area| {
|
||||
Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(vec![Constraint::Percentage(13); 8])
|
||||
.constraints(vec![Constraint::Ratio(1, 8); 8])
|
||||
.split(*area)
|
||||
.to_vec()
|
||||
})
|
||||
|
||||
18
examples/demo.tape
Normal file
18
examples/demo.tape
Normal file
@@ -0,0 +1,18 @@
|
||||
# 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
|
||||
@@ -5,7 +5,8 @@ use crossterm::{
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
use itertools::Itertools;
|
||||
use ratatui::{layout::Constraint::*, prelude::*, widgets::*};
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
@@ -47,50 +48,176 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(frame: &mut Frame<B>) {
|
||||
let [top, mid, bottom] = *Layout::default()
|
||||
let main_layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.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;
|
||||
};
|
||||
.constraints(vec![
|
||||
Length(4), // text
|
||||
Length(50), // examples
|
||||
Min(0), // fills remaining space
|
||||
])
|
||||
.split(frame.size());
|
||||
|
||||
// title
|
||||
frame.render_widget(
|
||||
Paragraph::new("Constraint::Length(4)").block(Block::default().borders(Borders::ALL)),
|
||||
top,
|
||||
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],
|
||||
);
|
||||
|
||||
frame.render_widget(
|
||||
Paragraph::new("Constraint::Percentage(50)").block(Block::default().borders(Borders::ALL)),
|
||||
mid,
|
||||
);
|
||||
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::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,
|
||||
);
|
||||
// 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}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
# 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 600
|
||||
Set Height 1410
|
||||
Hide
|
||||
Type "cargo run --example=layout --features=crossterm"
|
||||
Enter
|
||||
Sleep 1s
|
||||
Show
|
||||
Sleep 5s
|
||||
Sleep 2s
|
||||
|
||||
@@ -66,27 +66,23 @@ 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 as u16);
|
||||
app.vertical_scroll_state =
|
||||
app.vertical_scroll_state.position(app.vertical_scroll);
|
||||
}
|
||||
KeyCode::Char('k') => {
|
||||
app.vertical_scroll = app.vertical_scroll.saturating_sub(1);
|
||||
app.vertical_scroll_state = app
|
||||
.vertical_scroll_state
|
||||
.position(app.vertical_scroll as u16);
|
||||
app.vertical_scroll_state =
|
||||
app.vertical_scroll_state.position(app.vertical_scroll);
|
||||
}
|
||||
KeyCode::Char('h') => {
|
||||
app.horizontal_scroll = app.horizontal_scroll.saturating_sub(1);
|
||||
app.horizontal_scroll_state = app
|
||||
.horizontal_scroll_state
|
||||
.position(app.horizontal_scroll as u16);
|
||||
app.horizontal_scroll_state =
|
||||
app.horizontal_scroll_state.position(app.horizontal_scroll);
|
||||
}
|
||||
KeyCode::Char('l') => {
|
||||
app.horizontal_scroll = app.horizontal_scroll.saturating_add(1);
|
||||
app.horizontal_scroll_state = app
|
||||
.horizontal_scroll_state
|
||||
.position(app.horizontal_scroll as u16);
|
||||
app.horizontal_scroll_state =
|
||||
app.horizontal_scroll_state.position(app.horizontal_scroll);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -151,10 +147,8 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
|
||||
),
|
||||
]),
|
||||
];
|
||||
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);
|
||||
app.vertical_scroll_state = app.vertical_scroll_state.content_length(text.len());
|
||||
app.horizontal_scroll_state = app.horizontal_scroll_state.content_length(long_line.len());
|
||||
|
||||
let create_block = |title| {
|
||||
Block::default()
|
||||
|
||||
2
rust-toolchain.toml
Normal file
2
rust-toolchain.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "stable"
|
||||
@@ -18,9 +18,10 @@ use crossterm::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
backend::{Backend, ClearType},
|
||||
backend::{Backend, ClearType, WindowSize},
|
||||
buffer::Cell,
|
||||
layout::Rect,
|
||||
layout::Size,
|
||||
prelude::Rect,
|
||||
style::{Color, Modifier},
|
||||
};
|
||||
|
||||
@@ -169,12 +170,26 @@ where
|
||||
}
|
||||
|
||||
fn size(&self) -> io::Result<Rect> {
|
||||
let (width, height) =
|
||||
terminal::size().map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
|
||||
|
||||
let (width, height) = terminal::size()?;
|
||||
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.buffer.flush()
|
||||
}
|
||||
|
||||
@@ -27,7 +27,9 @@
|
||||
|
||||
use std::io;
|
||||
|
||||
use crate::{buffer::Cell, layout::Rect};
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
use crate::{buffer::Cell, layout::Size, prelude::Rect};
|
||||
|
||||
#[cfg(feature = "termion")]
|
||||
mod termion;
|
||||
@@ -49,7 +51,7 @@ 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, Eq, PartialEq, Hash)]
|
||||
#[derive(Debug, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub enum ClearType {
|
||||
All,
|
||||
AfterCursor,
|
||||
@@ -58,6 +60,18 @@ pub enum ClearType {
|
||||
UntilNewLine,
|
||||
}
|
||||
|
||||
/// The window sizes in columns,rows and optionally pixel width,height.
|
||||
pub struct WindowSize {
|
||||
/// Size in character/cell columents,rows.
|
||||
pub columns_rows: Size,
|
||||
/// Size in pixel width,height.
|
||||
///
|
||||
/// 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.
|
||||
@@ -109,9 +123,54 @@ pub trait Backend {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the size of the terminal screen as a [`Rect`].
|
||||
/// Get the size of the terminal screen in columns/rows as a [`Rect`].
|
||||
fn size(&self) -> Result<Rect, io::Error>;
|
||||
|
||||
/// Get the size of the terminal screen in columns/rows and pixels as [`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,rows together with pixel size.
|
||||
fn window_size(&mut self) -> Result<WindowSize, io::Error>;
|
||||
|
||||
/// Flush any buffered content to the terminal screen.
|
||||
fn flush(&mut self) -> Result<(), io::Error>;
|
||||
}
|
||||
|
||||
#[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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,9 @@ use std::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
backend::{Backend, ClearType},
|
||||
backend::{Backend, ClearType, WindowSize},
|
||||
buffer::Cell,
|
||||
layout::Rect,
|
||||
prelude::Rect,
|
||||
style::{Color, Modifier},
|
||||
};
|
||||
|
||||
@@ -160,6 +160,13 @@ 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.stdout.flush()
|
||||
}
|
||||
|
||||
@@ -11,13 +11,14 @@ use termwiz::{
|
||||
cell::{AttributeChange, Blink, Intensity, Underline},
|
||||
color::{AnsiColor, ColorAttribute, SrgbaTuple},
|
||||
surface::{Change, CursorVisibility, Position},
|
||||
terminal::{buffered::BufferedTerminal, SystemTerminal, Terminal},
|
||||
terminal::{buffered::BufferedTerminal, ScreenSize, SystemTerminal, Terminal},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
backend::Backend,
|
||||
backend::{Backend, WindowSize},
|
||||
buffer::Cell,
|
||||
layout::Rect,
|
||||
layout::Size,
|
||||
prelude::Rect,
|
||||
style::{Color, Modifier},
|
||||
};
|
||||
|
||||
@@ -169,22 +170,31 @@ impl Backend for TermwizBackend {
|
||||
}
|
||||
|
||||
fn size(&self) -> Result<Rect, io::Error> {
|
||||
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
|
||||
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),
|
||||
},
|
||||
if term_height > usize::from(max) {
|
||||
max
|
||||
} else {
|
||||
term_height as u16
|
||||
pixels: Size {
|
||||
width: u16_max(xpixel),
|
||||
height: u16_max(ypixel),
|
||||
},
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> Result<(), io::Error> {
|
||||
@@ -221,3 +231,8 @@ impl From<Color> for ColorAttribute {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn u16_max(i: usize) -> u16 {
|
||||
u16::try_from(i).unwrap_or(u16::MAX)
|
||||
}
|
||||
|
||||
@@ -9,9 +9,9 @@ use std::{
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{
|
||||
backend::Backend,
|
||||
backend::{Backend, WindowSize},
|
||||
buffer::{Buffer, Cell},
|
||||
layout::Rect,
|
||||
layout::{Rect, Size},
|
||||
};
|
||||
|
||||
/// A backend used for the integration tests.
|
||||
@@ -29,6 +29,7 @@ use crate::{
|
||||
/// # }
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct TestBackend {
|
||||
width: u16,
|
||||
buffer: Buffer,
|
||||
@@ -93,6 +94,7 @@ 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);
|
||||
@@ -177,7 +179,151 @@ 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ use crate::{
|
||||
|
||||
/// A buffer cell
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Cell {
|
||||
pub symbol: String,
|
||||
pub fg: Color,
|
||||
@@ -22,6 +23,7 @@ pub struct Cell {
|
||||
#[cfg(feature = "crossterm")]
|
||||
pub underline_color: Color,
|
||||
pub modifier: Modifier,
|
||||
pub skip: bool,
|
||||
}
|
||||
|
||||
impl Cell {
|
||||
@@ -80,6 +82,15 @@ 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(' ');
|
||||
@@ -90,6 +101,7 @@ impl Cell {
|
||||
self.underline_color = Color::Reset;
|
||||
}
|
||||
self.modifier = Modifier::empty();
|
||||
self.skip = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +114,7 @@ impl Default for Cell {
|
||||
#[cfg(feature = "crossterm")]
|
||||
underline_color: Color::Reset,
|
||||
modifier: Modifier::empty(),
|
||||
skip: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -130,12 +143,14 @@ impl Default for Cell {
|
||||
/// bg: Color::White,
|
||||
/// #[cfg(feature = "crossterm")]
|
||||
/// underline_color: Color::Reset,
|
||||
/// modifier: Modifier::empty()
|
||||
/// modifier: Modifier::empty(),
|
||||
/// skip: false
|
||||
/// });
|
||||
/// 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))]
|
||||
pub struct Buffer {
|
||||
/// The area represented by this buffer
|
||||
pub area: Rect,
|
||||
@@ -486,10 +501,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):
|
||||
// their place (the skipped cells should be blank anyway), or due to per-cell-skipping:
|
||||
let mut to_skip: usize = 0;
|
||||
for (i, (current, previous)) in next_buffer.iter().zip(previous_buffer.iter()).enumerate() {
|
||||
if (current != previous || invalidated > 0) && to_skip == 0 {
|
||||
if !current.skip && (current != previous || invalidated > 0) && to_skip == 0 {
|
||||
let (x, y) = self.pos_of(i);
|
||||
updates.push((x, y, &next_buffer[i]));
|
||||
}
|
||||
@@ -914,6 +929,18 @@ 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(
|
||||
@@ -995,4 +1022,54 @@ 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]);
|
||||
}
|
||||
}
|
||||
|
||||
1496
src/layout.rs
1496
src/layout.rs
File diff suppressed because it is too large
Load Diff
44
src/lib.rs
44
src/lib.rs
@@ -1,9 +1,9 @@
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
//! [ratatui](https://github.com/ratatui-org/ratatui) is a library used to build rich
|
||||
//! terminal users interfaces and dashboards.
|
||||
//! [ratatui](https://github.com/ratatui-org/ratatui) is a library that is all about cooking up terminal user
|
||||
//! interfaces (TUIs).
|
||||
//!
|
||||
//! 
|
||||
//! 
|
||||
//!
|
||||
//! # Get started
|
||||
//!
|
||||
@@ -13,7 +13,7 @@
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! crossterm = "0.27"
|
||||
//! ratatui = "0.22"
|
||||
//! ratatui = "0.23"
|
||||
//! ```
|
||||
//!
|
||||
//! The crate is using the `crossterm` backend by default that works on most platforms. But if for
|
||||
@@ -23,19 +23,11 @@
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! termion = "2.0.1"
|
||||
//! ratatui = { version = "0.22", default-features = false, features = ['termion'] }
|
||||
//! ratatui = { version = "0.23", 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
|
||||
@@ -80,8 +72,8 @@
|
||||
//! 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:
|
||||
//!
|
||||
@@ -171,11 +163,25 @@
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! 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))]
|
||||
|
||||
|
||||
@@ -440,7 +440,7 @@ impl std::error::Error for ParseColorError {}
|
||||
/// `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.
|
||||
/// See the [`Color`] documentation for more information on the supported color names.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
pub mod block {
|
||||
pub const FULL: &str = "█";
|
||||
pub const SEVEN_EIGHTHS: &str = "▉";
|
||||
@@ -240,7 +242,7 @@ pub mod braille {
|
||||
}
|
||||
|
||||
/// Marker to use when plotting data points
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub enum Marker {
|
||||
/// One point per cell in shape of dot
|
||||
#[default]
|
||||
@@ -301,3 +303,27 @@ 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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::io;
|
||||
use std::{fmt, io};
|
||||
|
||||
use crate::{
|
||||
backend::{Backend, ClearType},
|
||||
@@ -15,6 +15,16 @@ pub enum Viewport {
|
||||
Fixed(Rect),
|
||||
}
|
||||
|
||||
impl fmt::Display for Viewport {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Viewport::Fullscreen => write!(f, "Fullscreen"),
|
||||
Viewport::Inline(height) => write!(f, "Inline({})", height),
|
||||
Viewport::Fixed(area) => write!(f, "Fixed({})", area),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Options to pass to [`Terminal::with_options`]
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct TerminalOptions {
|
||||
@@ -22,7 +32,7 @@ pub struct TerminalOptions {
|
||||
pub viewport: Viewport,
|
||||
}
|
||||
|
||||
/// Interface to the terminal backed by Termion
|
||||
/// Interface to the terminal backed by a [`Backend`].
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Terminal<B>
|
||||
where
|
||||
@@ -487,3 +497,18 @@ fn compute_inline_size<B: Backend>(
|
||||
pos,
|
||||
))
|
||||
}
|
||||
|
||||
#[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)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,3 +29,39 @@ 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());
|
||||
}
|
||||
}
|
||||
|
||||
251
src/text/span.rs
251
src/text/span.rs
@@ -6,22 +6,66 @@ use unicode_width::UnicodeWidthStr;
|
||||
use super::StyledGrapheme;
|
||||
use crate::style::{Style, Styled};
|
||||
|
||||
/// A string where all graphemes have the same style.
|
||||
/// 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)]
|
||||
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 no style.
|
||||
/// Create a span with the default style.
|
||||
///
|
||||
/// ## Examples
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::Span;
|
||||
/// Span::raw("My text");
|
||||
/// Span::raw(String::from("My text"));
|
||||
/// Span::raw("test content");
|
||||
/// Span::raw(String::from("test content"));
|
||||
/// ```
|
||||
pub fn raw<T>(content: T) -> Span<'a>
|
||||
where
|
||||
@@ -33,16 +77,15 @@ impl<'a> Span<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a span with a style.
|
||||
/// Create a span with the specified style.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # 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);
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let style = Style::new().yellow().on_green().italic();
|
||||
/// Span::styled("test content", style);
|
||||
/// Span::styled(String::from("test content"), style);
|
||||
/// ```
|
||||
pub fn styled<T>(content: T, style: Style) -> Span<'a>
|
||||
where
|
||||
@@ -54,31 +97,30 @@ impl<'a> Span<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the width of the content held by this span.
|
||||
/// Returns the unicode 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 each grapheme [`Style`] to get
|
||||
/// the resulting [`Style`].
|
||||
/// `base_style` is the [`Style`] that will be patched with the `Span`'s `style` to get the
|
||||
/// resulting [`Style`].
|
||||
///
|
||||
/// ## Examples
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::{Span, StyledGrapheme};
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// # use ratatui::{prelude::*, text::StyledGrapheme};
|
||||
/// # use std::iter::Iterator;
|
||||
/// let span = Span::styled("Text", Style::default().fg(Color::Yellow));
|
||||
/// let style = Style::default().fg(Color::Green).bg(Color::Black);
|
||||
/// let span = Span::styled("Test", Style::new().green().italic());
|
||||
/// let style = Style::new().red().on_yellow();
|
||||
/// assert_eq!(
|
||||
/// span.styled_graphemes(style).collect::<Vec<StyledGrapheme>>(),
|
||||
/// vec![
|
||||
/// 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)),
|
||||
/// 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()),
|
||||
/// ],
|
||||
/// );
|
||||
/// ```
|
||||
@@ -86,66 +128,59 @@ impl<'a> Span<'a> {
|
||||
&'a self,
|
||||
base_style: Style,
|
||||
) -> impl Iterator<Item = StyledGrapheme<'a>> {
|
||||
UnicodeSegmentation::graphemes(self.content.as_ref(), true)
|
||||
self.content
|
||||
.as_ref()
|
||||
.graphemes(true)
|
||||
.filter(|g| *g != "\n")
|
||||
.map(move |g| StyledGrapheme {
|
||||
symbol: g,
|
||||
style: base_style.patch(self.style),
|
||||
})
|
||||
.filter(|s| s.symbol != "\n")
|
||||
}
|
||||
|
||||
/// Patches the style an existing Span, adding modifiers from the given style.
|
||||
/// Patches the style of the Span, adding modifiers from the given style.
|
||||
///
|
||||
/// ## Examples
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # 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);
|
||||
/// # 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());
|
||||
/// ```
|
||||
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())`.
|
||||
///
|
||||
/// ## Examples
|
||||
/// This is Equivalent to calling `patch_style(Style::reset())`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # 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));
|
||||
///
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let mut span = Span::styled("Test Content", Style::new().green().on_yellow().italic());
|
||||
/// span.reset_style();
|
||||
/// assert_eq!(Style::reset(), span.style);
|
||||
/// assert_eq!(span.style, Style::reset());
|
||||
/// ```
|
||||
pub fn reset_style(&mut self) {
|
||||
self.patch_style(Style::reset());
|
||||
}
|
||||
}
|
||||
|
||||
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, T> From<T> for Span<'a>
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
{
|
||||
fn from(s: T) -> Self {
|
||||
Span::raw(s.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Styled for Span<'a> {
|
||||
type Item = Span<'a>;
|
||||
|
||||
fn style(&self) -> Style {
|
||||
self.style
|
||||
}
|
||||
@@ -155,3 +190,113 @@ 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());
|
||||
}
|
||||
}
|
||||
|
||||
220
src/text/text.rs
220
src/text/text.rs
@@ -223,3 +223,223 @@ 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"),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
98
src/title.rs
98
src/title.rs
@@ -1,23 +1,93 @@
|
||||
#![warn(missing_docs)]
|
||||
//! 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::widgets::block::Title;
|
||||
/// # use ratatui::style::Stylize;
|
||||
/// Title::from("Title".blue().on_white());
|
||||
/// ```
|
||||
///
|
||||
/// Title with multiple styles (see [`Line`] and [`Stylize`](crate::style::Stylize)).
|
||||
/// ```
|
||||
/// # use ratatui::widgets::block::Title;
|
||||
/// # use ratatui::style::Stylize;
|
||||
/// # use ratatui::text::Line;
|
||||
/// Title::from(
|
||||
/// Line::from(vec!["Q".white().underlined(), "uit".gray()])
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// Complete example
|
||||
/// ```
|
||||
/// # use ratatui::widgets::block::{Title, Position};
|
||||
/// # use ratatui::layout::Alignment;
|
||||
/// Title::from("Title")
|
||||
/// .position(Position::Top)
|
||||
/// .alignment(Alignment::Right);
|
||||
/// ```
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Title<'a> {
|
||||
/// Title content
|
||||
pub content: Line<'a>,
|
||||
/// Defaults to Left if unset
|
||||
/// 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).
|
||||
pub alignment: Option<Alignment>,
|
||||
|
||||
/// Defaults to Top if unset
|
||||
/// 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).
|
||||
pub position: Option<Position>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
/// 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::{Title, Position}};
|
||||
/// Block::new().title(
|
||||
/// Title::from("title").position(Position::Bottom)
|
||||
/// );
|
||||
/// ```
|
||||
#[derive(Debug, Default, Display, EnumString, 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>>,
|
||||
@@ -26,11 +96,13 @@ 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
|
||||
@@ -45,3 +117,23 @@ 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,4 +1,4 @@
|
||||
use crate::{buffer::Buffer, style::Style, text::Line};
|
||||
use crate::{buffer::Buffer, prelude::Rect, style::Style, text::Line};
|
||||
|
||||
/// represent a bar to be shown by the Barchart
|
||||
///
|
||||
@@ -56,6 +56,45 @@ impl<'a> Bar<'a> {
|
||||
self
|
||||
}
|
||||
|
||||
/// Render the value of the bar. value_text 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,
|
||||
@@ -79,14 +118,18 @@ impl<'a> Bar<'a> {
|
||||
x + (max_width.saturating_sub(value_label.len() as u16) >> 1),
|
||||
y,
|
||||
value_label,
|
||||
self.value_style.patch(default_value_style),
|
||||
default_value_style.patch(self.value_style),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// render the label
|
||||
if let Some(mut label) = self.label {
|
||||
label.patch_style(default_label_style);
|
||||
// patch label styles
|
||||
for span in &mut label.spans {
|
||||
span.style = default_label_style.patch(span.style);
|
||||
}
|
||||
|
||||
buf.set_line(
|
||||
x + (max_width.saturating_sub(label.width() as u16) >> 1),
|
||||
y + 1,
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
use super::Bar;
|
||||
use crate::text::Line;
|
||||
use crate::{
|
||||
prelude::{Alignment, Buffer, Rect},
|
||||
style::Style,
|
||||
text::Line,
|
||||
};
|
||||
|
||||
/// represent a group of bars to be shown by the Barchart
|
||||
///
|
||||
@@ -35,6 +39,23 @@ impl<'a> BarGroup<'a> {
|
||||
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> {
|
||||
|
||||
@@ -53,6 +53,8 @@ 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> {
|
||||
@@ -69,6 +71,7 @@ impl<'a> Default for BarChart<'a> {
|
||||
group_gap: 0,
|
||||
bar_set: symbols::bar::NINE_LEVELS,
|
||||
style: Style::default(),
|
||||
direction: Direction::Vertical,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,6 +107,9 @@ impl<'a> BarChart<'a> {
|
||||
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
|
||||
@@ -124,11 +130,17 @@ impl<'a> BarChart<'a> {
|
||||
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
|
||||
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 Group.
|
||||
/// In this case the default label style will be patched by the individual label style
|
||||
pub fn label_style(mut self, style: Style) -> BarChart<'a> {
|
||||
self.label_style = style;
|
||||
self
|
||||
@@ -143,6 +155,12 @@ impl<'a> BarChart<'a> {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the direction of the bars
|
||||
pub fn direction(mut self, direction: Direction) -> BarChart<'a> {
|
||||
self.direction = direction;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> BarChart<'a> {
|
||||
@@ -196,7 +214,72 @@ impl<'a> BarChart<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_bars(&self, buf: &mut Buffer, bars_area: Rect, max: u64) {
|
||||
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) {
|
||||
// convert the bar values to ratatui::symbols::bar::Set
|
||||
let mut groups: Vec<Vec<u64>> = self
|
||||
.data
|
||||
@@ -227,7 +310,7 @@ impl<'a> BarChart<'a> {
|
||||
_ => self.bar_set.full,
|
||||
};
|
||||
|
||||
let bar_style = bar.style.patch(self.bar_style);
|
||||
let bar_style = self.bar_style.patch(bar.style);
|
||||
|
||||
for x in 0..self.bar_width {
|
||||
buf.get_mut(bar_x + x, bars_area.top() + j)
|
||||
@@ -264,23 +347,24 @@ 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 group in self.data.into_iter() {
|
||||
// print group labels under the bars or the previous labels
|
||||
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,
|
||||
);
|
||||
for mut group in self.data.into_iter() {
|
||||
if group.bars.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let bars = std::mem::take(&mut group.bars);
|
||||
// print group labels under the bars or the previous labels
|
||||
let label_max_width =
|
||||
bars.len() as u16 * (self.bar_width + self.bar_gap) - self.bar_gap;
|
||||
let group_area = Rect {
|
||||
x: bar_x,
|
||||
y: area.bottom() - 1,
|
||||
width: label_max_width,
|
||||
height: 1,
|
||||
};
|
||||
group.render_label(buf, group_area, self.label_style);
|
||||
|
||||
// print the bar values and numbers
|
||||
for bar in group.bars.into_iter() {
|
||||
for bar in bars.into_iter() {
|
||||
bar.render_label_and_value(
|
||||
buf,
|
||||
self.bar_width,
|
||||
@@ -314,16 +398,23 @@ impl<'a> Widget for BarChart<'a> {
|
||||
|
||||
let max = self.maximum_data_value();
|
||||
|
||||
// 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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -614,7 +705,7 @@ mod tests {
|
||||
let expected = Buffer::with_lines(vec![
|
||||
"█ █ █ █ ",
|
||||
"█ █ █ █ █ █ █ █ █",
|
||||
" G1 G2 G3 ",
|
||||
"G1 G2 G3 ",
|
||||
]);
|
||||
|
||||
assert_buffer_eq!(buffer, expected);
|
||||
@@ -637,7 +728,7 @@ mod tests {
|
||||
let expected = Buffer::with_lines(vec![
|
||||
"█ █ █ ",
|
||||
"█ █ █ █ █ █ █",
|
||||
" G1 G2 G",
|
||||
"G1 G2 G",
|
||||
]);
|
||||
assert_buffer_eq!(buffer, expected);
|
||||
}
|
||||
@@ -659,7 +750,7 @@ mod tests {
|
||||
let expected = Buffer::with_lines(vec![
|
||||
"█ █ █ ",
|
||||
"█ █ █ █ █ █ ",
|
||||
" G1 G2 ",
|
||||
"G1 G2 ",
|
||||
]);
|
||||
assert_buffer_eq!(buffer, expected);
|
||||
}
|
||||
@@ -753,7 +844,189 @@ mod tests {
|
||||
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 3, 3));
|
||||
chart.render(buffer.area, &mut buffer);
|
||||
let expected = Buffer::with_lines(vec![" █", "█ █", " G "]);
|
||||
let expected = Buffer::with_lines(vec![" █", "█ █", "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"]);
|
||||
assert_buffer_eq!(buffer, expected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#[path = "../title.rs"]
|
||||
pub mod title;
|
||||
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
pub use self::title::{Position, Title};
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
@@ -10,7 +12,7 @@ use crate::{
|
||||
widgets::{Borders, Widget},
|
||||
};
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub enum BorderType {
|
||||
#[default]
|
||||
Plain,
|
||||
@@ -526,6 +528,8 @@ impl<'a> Styled for Block<'a> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use strum::ParseError;
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
assert_buffer_eq,
|
||||
@@ -538,215 +542,104 @@ mod tests {
|
||||
// No borders
|
||||
assert_eq!(
|
||||
Block::default().inner(Rect::default()),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0
|
||||
},
|
||||
Rect::new(0, 0, 0, 0),
|
||||
"no borders, width=0, height=0"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1
|
||||
},
|
||||
Block::default().inner(Rect::new(0, 0, 1, 1)),
|
||||
Rect::new(0, 0, 1, 1),
|
||||
"no borders, width=1, height=1"
|
||||
);
|
||||
|
||||
// Left border
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::LEFT).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 1
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 1
|
||||
},
|
||||
Block::default()
|
||||
.borders(Borders::LEFT)
|
||||
.inner(Rect::new(0, 0, 0, 1)),
|
||||
Rect::new(0, 0, 0, 1),
|
||||
"left, width=0"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::LEFT).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1
|
||||
}),
|
||||
Rect {
|
||||
x: 1,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 1
|
||||
},
|
||||
Block::default()
|
||||
.borders(Borders::LEFT)
|
||||
.inner(Rect::new(0, 0, 1, 1)),
|
||||
Rect::new(1, 0, 0, 1),
|
||||
"left, width=1"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::LEFT).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 2,
|
||||
height: 1
|
||||
}),
|
||||
Rect {
|
||||
x: 1,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1
|
||||
},
|
||||
Block::default()
|
||||
.borders(Borders::LEFT)
|
||||
.inner(Rect::new(0, 0, 2, 1)),
|
||||
Rect::new(1, 0, 1, 1),
|
||||
"left, width=2"
|
||||
);
|
||||
|
||||
// Top border
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::TOP).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 0
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 0
|
||||
},
|
||||
Block::default()
|
||||
.borders(Borders::TOP)
|
||||
.inner(Rect::new(0, 0, 1, 0)),
|
||||
Rect::new(0, 0, 1, 0),
|
||||
"top, height=0"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::TOP).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 1,
|
||||
width: 1,
|
||||
height: 0
|
||||
},
|
||||
Block::default()
|
||||
.borders(Borders::TOP)
|
||||
.inner(Rect::new(0, 0, 1, 1)),
|
||||
Rect::new(0, 1, 1, 0),
|
||||
"top, height=1"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::TOP).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 2
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 1,
|
||||
width: 1,
|
||||
height: 1
|
||||
},
|
||||
Block::default()
|
||||
.borders(Borders::TOP)
|
||||
.inner(Rect::new(0, 0, 1, 2)),
|
||||
Rect::new(0, 1, 1, 1),
|
||||
"top, height=2"
|
||||
);
|
||||
|
||||
// Right border
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::RIGHT).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 1
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 1
|
||||
},
|
||||
Block::default()
|
||||
.borders(Borders::RIGHT)
|
||||
.inner(Rect::new(0, 0, 0, 1)),
|
||||
Rect::new(0, 0, 0, 1),
|
||||
"right, width=0"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::RIGHT).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 1
|
||||
},
|
||||
Block::default()
|
||||
.borders(Borders::RIGHT)
|
||||
.inner(Rect::new(0, 0, 1, 1)),
|
||||
Rect::new(0, 0, 0, 1),
|
||||
"right, width=1"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::RIGHT).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 2,
|
||||
height: 1
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1
|
||||
},
|
||||
Block::default()
|
||||
.borders(Borders::RIGHT)
|
||||
.inner(Rect::new(0, 0, 2, 1)),
|
||||
Rect::new(0, 0, 1, 1),
|
||||
"right, width=2"
|
||||
);
|
||||
|
||||
// Bottom border
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::BOTTOM).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 0
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 0
|
||||
},
|
||||
Block::default()
|
||||
.borders(Borders::BOTTOM)
|
||||
.inner(Rect::new(0, 0, 1, 0)),
|
||||
Rect::new(0, 0, 1, 0),
|
||||
"bottom, height=0"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::BOTTOM).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 0
|
||||
},
|
||||
Block::default()
|
||||
.borders(Borders::BOTTOM)
|
||||
.inner(Rect::new(0, 0, 1, 1)),
|
||||
Rect::new(0, 0, 1, 0),
|
||||
"bottom, height=1"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::BOTTOM).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 2
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1
|
||||
},
|
||||
Block::default()
|
||||
.borders(Borders::BOTTOM)
|
||||
.inner(Rect::new(0, 0, 1, 2)),
|
||||
Rect::new(0, 0, 1, 1),
|
||||
"bottom, height=2"
|
||||
);
|
||||
|
||||
@@ -755,57 +648,28 @@ mod tests {
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.inner(Rect::default()),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0
|
||||
},
|
||||
Rect::new(0, 0, 0, 0),
|
||||
"all borders, width=0, height=0"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::ALL).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1
|
||||
}),
|
||||
Rect {
|
||||
x: 1,
|
||||
y: 1,
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.inner(Rect::new(0, 0, 1, 1)),
|
||||
Rect::new(1, 1, 0, 0),
|
||||
"all borders, width=1, height=1"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::ALL).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 2,
|
||||
height: 2,
|
||||
}),
|
||||
Rect {
|
||||
x: 1,
|
||||
y: 1,
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.inner(Rect::new(0, 0, 2, 2)),
|
||||
Rect::new(1, 1, 0, 0),
|
||||
"all borders, width=2, height=2"
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default().borders(Borders::ALL).inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 3,
|
||||
height: 3,
|
||||
}),
|
||||
Rect {
|
||||
x: 1,
|
||||
y: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.inner(Rect::new(0, 0, 3, 3)),
|
||||
Rect::new(1, 1, 1, 1),
|
||||
"all borders, width=3, height=3"
|
||||
);
|
||||
}
|
||||
@@ -813,50 +677,20 @@ mod tests {
|
||||
#[test]
|
||||
fn inner_takes_into_account_the_title() {
|
||||
assert_eq!(
|
||||
Block::default().title("Test").inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 1,
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 1,
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
Block::default().title("Test").inner(Rect::new(0, 0, 0, 1)),
|
||||
Rect::new(0, 1, 0, 0),
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default()
|
||||
.title(Title::from("Test").alignment(Alignment::Center))
|
||||
.inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 1,
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 1,
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
.inner(Rect::new(0, 0, 0, 1)),
|
||||
Rect::new(0, 1, 0, 0),
|
||||
);
|
||||
assert_eq!(
|
||||
Block::default()
|
||||
.title(Title::from("Test").alignment(Alignment::Right))
|
||||
.inner(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 1,
|
||||
}),
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 1,
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
.inner(Rect::new(0, 0, 0, 1)),
|
||||
Rect::new(0, 1, 0, 0),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -866,16 +700,53 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn padding_can_be_const() {
|
||||
const PADDING: Padding = Padding::new(1, 1, 1, 1);
|
||||
const UNI_PADDING: Padding = Padding::uniform(1);
|
||||
assert_eq!(PADDING, UNI_PADDING);
|
||||
fn padding_new() {
|
||||
assert_eq!(
|
||||
Padding::new(1, 2, 3, 4),
|
||||
Padding {
|
||||
left: 1,
|
||||
right: 2,
|
||||
top: 3,
|
||||
bottom: 4
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn padding_constructors() {
|
||||
assert_eq!(Padding::zero(), Padding::new(0, 0, 0, 0));
|
||||
assert_eq!(Padding::horizontal(1), Padding::new(1, 1, 0, 0));
|
||||
assert_eq!(Padding::vertical(1), Padding::new(0, 0, 1, 1));
|
||||
assert_eq!(Padding::uniform(1), Padding::new(1, 1, 1, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn padding_can_be_const() {
|
||||
const _PADDING: Padding = Padding::new(1, 1, 1, 1);
|
||||
const _UNI_PADDING: Padding = Padding::uniform(1);
|
||||
const _NO_PADDING: Padding = Padding::zero();
|
||||
const _HORIZONTAL: Padding = Padding::horizontal(1);
|
||||
const _VERTICAL: Padding = Padding::vertical(1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_new() {
|
||||
assert_eq!(
|
||||
Block::new(),
|
||||
Block {
|
||||
titles: Vec::new(),
|
||||
titles_style: Style::new(),
|
||||
titles_alignment: Alignment::Left,
|
||||
titles_position: Position::Top,
|
||||
borders: Borders::NONE,
|
||||
border_style: Style::new(),
|
||||
border_type: BorderType::Plain,
|
||||
style: Style::new(),
|
||||
padding: Padding::zero(),
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_can_be_const() {
|
||||
const _DEFAULT_STYLE: Style = Style::new();
|
||||
@@ -892,8 +763,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn can_be_stylized() {
|
||||
let block = Block::default().black().on_white().bold().not_dim();
|
||||
assert_eq!(
|
||||
Block::default().black().on_white().bold().not_dim().style,
|
||||
block.style,
|
||||
Style::default()
|
||||
.fg(Color::Black)
|
||||
.bg(Color::White)
|
||||
@@ -937,7 +809,28 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_title_content_style() {
|
||||
fn title_on_bottom() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 2));
|
||||
#[allow(deprecated)]
|
||||
Block::default()
|
||||
.title("test")
|
||||
.title_on_bottom()
|
||||
.render(buffer.area, &mut buffer);
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" ", "test"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn title_position() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 2));
|
||||
Block::default()
|
||||
.title("test")
|
||||
.title_position(Position::Bottom)
|
||||
.render(buffer.area, &mut buffer);
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" ", "test"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn title_content_style() {
|
||||
for alignment in [Alignment::Left, Alignment::Center, Alignment::Right] {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 1));
|
||||
Block::default()
|
||||
@@ -953,7 +846,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_block_title_style() {
|
||||
fn block_title_style() {
|
||||
for alignment in [Alignment::Left, Alignment::Center, Alignment::Right] {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 1));
|
||||
Block::default()
|
||||
@@ -985,4 +878,107 @@ mod tests {
|
||||
assert_buffer_eq!(buffer, expected_buffer);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn title_border_style() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
|
||||
Block::default()
|
||||
.title("test")
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::new().yellow())
|
||||
.render(buffer.area, &mut buffer);
|
||||
|
||||
let mut expected_buffer = Buffer::with_lines(vec![
|
||||
"┌test─────────┐",
|
||||
"│ │",
|
||||
"└─────────────┘",
|
||||
]);
|
||||
expected_buffer.set_style(Rect::new(0, 0, 15, 3), Style::new().yellow());
|
||||
expected_buffer.set_style(Rect::new(1, 1, 13, 1), Style::reset());
|
||||
|
||||
assert_buffer_eq!(buffer, expected_buffer);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn border_type_to_string() {
|
||||
assert_eq!(format!("{}", BorderType::Plain), "Plain");
|
||||
assert_eq!(format!("{}", BorderType::Rounded), "Rounded");
|
||||
assert_eq!(format!("{}", BorderType::Double), "Double");
|
||||
assert_eq!(format!("{}", BorderType::Thick), "Thick");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn border_type_from_str() {
|
||||
assert_eq!("Plain".parse(), Ok(BorderType::Plain));
|
||||
assert_eq!("Rounded".parse(), Ok(BorderType::Rounded));
|
||||
assert_eq!("Double".parse(), Ok(BorderType::Double));
|
||||
assert_eq!("Thick".parse(), Ok(BorderType::Thick));
|
||||
assert_eq!("".parse::<BorderType>(), Err(ParseError::VariantNotFound));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_plain_border() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Plain)
|
||||
.render(buffer.area, &mut buffer);
|
||||
assert_buffer_eq!(
|
||||
buffer,
|
||||
Buffer::with_lines(vec![
|
||||
"┌─────────────┐",
|
||||
"│ │",
|
||||
"└─────────────┘"
|
||||
])
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn render_rounded_border() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.render(buffer.area, &mut buffer);
|
||||
assert_buffer_eq!(
|
||||
buffer,
|
||||
Buffer::with_lines(vec![
|
||||
"╭─────────────╮",
|
||||
"│ │",
|
||||
"╰─────────────╯"
|
||||
])
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn render_double_border() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Double)
|
||||
.render(buffer.area, &mut buffer);
|
||||
assert_buffer_eq!(
|
||||
buffer,
|
||||
Buffer::with_lines(vec![
|
||||
"╔═════════════╗",
|
||||
"║ ║",
|
||||
"╚═════════════╝"
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_solid_border() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Thick)
|
||||
.render(buffer.area, &mut buffer);
|
||||
assert_buffer_eq!(
|
||||
buffer,
|
||||
Buffer::with_lines(vec![
|
||||
"┏━━━━━━━━━━━━━┓",
|
||||
"┃ ┃",
|
||||
"┗━━━━━━━━━━━━━┛"
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ impl<'a, S: DateStyler> Monthly<'a, S> {
|
||||
self
|
||||
}
|
||||
|
||||
/// Render the calendar within a [Block](crate::widgets::Block)
|
||||
/// Render the calendar within a [Block]
|
||||
pub fn block(mut self, b: Block<'a>) -> Self {
|
||||
self.block = Some(b);
|
||||
self
|
||||
|
||||
@@ -13,6 +13,19 @@ 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 {
|
||||
@@ -91,3 +104,160 @@ 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,3 +1,5 @@
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
use crate::{
|
||||
style::Color,
|
||||
widgets::canvas::{
|
||||
@@ -6,7 +8,7 @@ use crate::{
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub enum MapResolution {
|
||||
#[default]
|
||||
Low,
|
||||
@@ -38,3 +40,153 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,3 +50,91 @@ 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,5 +1,6 @@
|
||||
use std::{borrow::Cow, cmp::max};
|
||||
|
||||
use strum::{Display, EnumString};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{
|
||||
@@ -76,7 +77,7 @@ impl<'a> Axis<'a> {
|
||||
}
|
||||
|
||||
/// Used to determine which style of graphing to use
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub enum GraphType {
|
||||
/// Draw each point
|
||||
#[default]
|
||||
@@ -627,6 +628,8 @@ impl<'a> Styled for Chart<'a> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use strum::ParseError;
|
||||
|
||||
use super::*;
|
||||
use crate::style::{Modifier, Stylize};
|
||||
|
||||
@@ -702,4 +705,17 @@ 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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,3 +35,28 @@ 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",
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::{
|
||||
layout::{Corner, Rect},
|
||||
style::{Style, Styled},
|
||||
text::Text,
|
||||
widgets::{Block, StatefulWidget, Widget},
|
||||
widgets::{Block, HighlightSpacing, StatefulWidget, Widget},
|
||||
};
|
||||
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
@@ -103,6 +103,8 @@ 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> {
|
||||
@@ -118,6 +120,7 @@ impl<'a> List<'a> {
|
||||
highlight_style: Style::default(),
|
||||
highlight_symbol: None,
|
||||
repeat_highlight_symbol: false,
|
||||
highlight_spacing: HighlightSpacing::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,6 +149,14 @@ 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
|
||||
@@ -228,7 +239,7 @@ impl<'a> StatefulWidget for List<'a> {
|
||||
let blank_symbol = " ".repeat(highlight_symbol.width());
|
||||
|
||||
let mut current_height = 0;
|
||||
let has_selection = state.selected.is_some();
|
||||
let selection_spacing = self.highlight_spacing.should_add(state.selected.is_some());
|
||||
for (i, item) in self
|
||||
.items
|
||||
.iter_mut()
|
||||
@@ -263,7 +274,7 @@ impl<'a> StatefulWidget for List<'a> {
|
||||
} else {
|
||||
&blank_symbol
|
||||
};
|
||||
let (elem_x, max_element_width) = if has_selection {
|
||||
let (elem_x, max_element_width) = if selection_spacing {
|
||||
let (elem_x, _) = buf.set_stringn(
|
||||
x,
|
||||
y + j as u16,
|
||||
@@ -774,6 +785,134 @@ mod tests {
|
||||
assert_buffer_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_repeat_highlight_symbol() {
|
||||
let items = list_items(vec!["Item 0\nLine 2", "Item 1", "Item 2"]);
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
//! - [`BarChart`]
|
||||
//! - [`Gauge`]
|
||||
//! - [`Sparkline`]
|
||||
//! - [`Scrollbar`]
|
||||
//! - [`calendar::Monthly`]
|
||||
//! - [`Clear`]
|
||||
|
||||
|
||||
@@ -21,24 +21,25 @@ fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Alignment)
|
||||
|
||||
/// A widget to display some text.
|
||||
///
|
||||
/// # Examples
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::text::{Text, Line, Span};
|
||||
/// # use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
|
||||
/// # use ratatui::style::{Style, Color, Modifier};
|
||||
/// # use ratatui::layout::{Alignment};
|
||||
/// # use ratatui::prelude::*;
|
||||
/// # use ratatui::widgets::*;
|
||||
/// let text = vec![
|
||||
/// Line::from(vec![
|
||||
/// Span::raw("First"),
|
||||
/// Span::styled("line",Style::default().add_modifier(Modifier::ITALIC)),
|
||||
/// Span::raw("."),
|
||||
/// Span::styled("line",Style::new().green().italic()),
|
||||
/// ".".into(),
|
||||
/// ]),
|
||||
/// Line::from(Span::styled("Second line", Style::default().fg(Color::Red))),
|
||||
/// Line::from("Second line".red()),
|
||||
/// "Third line".into(),
|
||||
/// ];
|
||||
/// Paragraph::new(text)
|
||||
/// .block(Block::default().title("Paragraph").borders(Borders::ALL))
|
||||
/// .style(Style::default().fg(Color::White).bg(Color::Black))
|
||||
/// .block(Block::new()
|
||||
/// .title("Paragraph")
|
||||
/// .borders(Borders::ALL))
|
||||
/// .style(Style::new().white().on_black())
|
||||
/// .alignment(Alignment::Center)
|
||||
/// .wrap(Wrap { trim: true });
|
||||
/// ```
|
||||
@@ -91,7 +92,28 @@ pub struct Wrap {
|
||||
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::*;
|
||||
/// # use ratatui::widgets::Paragraph;
|
||||
/// 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>>,
|
||||
@@ -106,22 +128,70 @@ impl<'a> Paragraph<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Surrounds the [`Paragraph`] widget with a [`Block`].
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// # use ratatui::widgets::{Block, Borders, Paragraph};
|
||||
/// 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::*;
|
||||
/// # use ratatui::widgets::Paragraph;
|
||||
/// 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::*;
|
||||
/// # use ratatui::widgets::{Paragraph, Wrap};
|
||||
/// let paragraph = Paragraph::new("Hello, world!")
|
||||
/// .wrap(Wrap { trim: true });
|
||||
/// ```
|
||||
pub fn wrap(mut self, wrap: Wrap) -> Paragraph<'a> {
|
||||
self.wrap = Some(wrap);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn scroll(mut self, offset: (u16, u16)) -> Paragraph<'a> {
|
||||
/// 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> {
|
||||
self.scroll = offset;
|
||||
self
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
use super::StatefulWidget;
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
@@ -7,7 +9,7 @@ use crate::{
|
||||
};
|
||||
|
||||
/// An enum representing the direction of scrolling in a Scrollbar widget.
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub enum ScrollDirection {
|
||||
/// Forward scroll direction, usually corresponds to scrolling downwards or rightwards.
|
||||
#[default]
|
||||
@@ -18,10 +20,16 @@ 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 `position` is 0
|
||||
/// - the `content_length` is 4
|
||||
/// - the `position` is 0
|
||||
/// - the `viewport_content_length` is 2
|
||||
///
|
||||
/// ```text
|
||||
@@ -37,29 +45,36 @@ pub enum ScrollDirection {
|
||||
/// default of 0 and it'll use the track size as a `viewport_content_length`.
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub struct ScrollbarState {
|
||||
// The current position within the scrollable content.
|
||||
position: u16,
|
||||
// The total length of the scrollable content.
|
||||
content_length: u16,
|
||||
content_length: usize,
|
||||
// The current position within the scrollable content.
|
||||
position: usize,
|
||||
// The length of content in current viewport.
|
||||
viewport_content_length: u16,
|
||||
viewport_content_length: usize,
|
||||
}
|
||||
|
||||
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: u16) -> Self {
|
||||
pub fn position(mut self, position: usize) -> 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: u16) -> Self {
|
||||
pub fn content_length(mut self, content_length: usize) -> 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: u16) -> Self {
|
||||
pub fn viewport_content_length(mut self, viewport_content_length: usize) -> Self {
|
||||
self.viewport_content_length = viewport_content_length;
|
||||
self
|
||||
}
|
||||
@@ -101,7 +116,7 @@ impl ScrollbarState {
|
||||
}
|
||||
|
||||
/// Scrollbar Orientation
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
#[derive(Debug, Default, Display, EnumString, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum ScrollbarOrientation {
|
||||
#[default]
|
||||
VerticalRight,
|
||||
@@ -122,6 +137,37 @@ pub enum ScrollbarOrientation {
|
||||
/// │ └──────── thumb
|
||||
/// └─────────── begin
|
||||
/// ```
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// # use ratatui::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)]
|
||||
pub struct Scrollbar<'a> {
|
||||
orientation: ScrollbarOrientation,
|
||||
@@ -310,7 +356,7 @@ impl<'a> Scrollbar<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn should_not_render(&self, track_start: u16, track_end: u16, content_length: u16) -> bool {
|
||||
fn should_not_render(&self, track_start: u16, track_end: u16, content_length: usize) -> bool {
|
||||
if track_end - track_start == 0 || content_length == 0 {
|
||||
return true;
|
||||
}
|
||||
@@ -361,7 +407,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
|
||||
(track_end - track_start) as usize
|
||||
} else {
|
||||
state.viewport_content_length
|
||||
};
|
||||
@@ -458,12 +504,105 @@ 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));
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use std::cmp::min;
|
||||
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
@@ -38,7 +40,7 @@ pub struct Sparkline<'a> {
|
||||
direction: RenderDirection,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub enum RenderDirection {
|
||||
#[default]
|
||||
LeftToRight,
|
||||
@@ -167,6 +169,8 @@ impl<'a> Widget for Sparkline<'a> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use strum::ParseError;
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
assert_buffer_eq,
|
||||
@@ -174,6 +178,28 @@ 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,8 +1,9 @@
|
||||
use strum::{Display, EnumString};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
layout::{Alignment, Constraint, Direction, Layout, Rect, SegmentSize},
|
||||
style::{Style, Styled},
|
||||
text::Text,
|
||||
widgets::{Block, StatefulWidget, Widget},
|
||||
@@ -161,7 +162,7 @@ impl<'a> Styled for Row<'a> {
|
||||
}
|
||||
|
||||
/// This option allows the user to configure the "highlight symbol" column width spacing
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Default, Hash)]
|
||||
#[derive(Debug, Display, EnumString, PartialEq, Eq, Clone, Default, Hash)]
|
||||
pub enum HighlightSpacing {
|
||||
/// Always add spacing for the selection symbol column
|
||||
///
|
||||
@@ -324,7 +325,7 @@ impl<'a> Table<'a> {
|
||||
|
||||
/// Set when to show the highlight spacing
|
||||
///
|
||||
/// See [HighlightSpacing] about which variant affects spacing in which way
|
||||
/// See [`HighlightSpacing`] about which variant affects spacing in which way
|
||||
pub fn highlight_spacing(mut self, value: HighlightSpacing) -> Self {
|
||||
self.highlight_spacing = value;
|
||||
self
|
||||
@@ -350,7 +351,7 @@ impl<'a> Table<'a> {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(constraints)
|
||||
.expand_to_fill(false)
|
||||
.segment_size(SegmentSize::None)
|
||||
.split(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
@@ -569,7 +570,14 @@ fn render_cell(buf: &mut Buffer, cell: &Cell, area: Rect) {
|
||||
if i as u16 >= area.height {
|
||||
break;
|
||||
}
|
||||
buf.set_line(area.x, area.y + i as u16, line, area.width);
|
||||
|
||||
let x_offset = match line.alignment {
|
||||
Some(Alignment::Center) => (area.width / 2).saturating_sub(line.width() as u16 / 2),
|
||||
Some(Alignment::Right) => area.width.saturating_sub(line.width() as u16),
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
buf.set_line(area.x + x_offset, area.y + i as u16, line, area.width);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -585,13 +593,145 @@ mod tests {
|
||||
use std::vec;
|
||||
|
||||
use super::*;
|
||||
use crate::style::{Color, Modifier, Style, Stylize};
|
||||
use crate::{
|
||||
layout::Constraint::*,
|
||||
style::{Color, Modifier, Style, Stylize},
|
||||
text::Line,
|
||||
};
|
||||
#[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!(
|
||||
@@ -637,4 +777,34 @@ 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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,7 +146,125 @@ impl<'a> Widget for Tabs<'a> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::style::{Color, Modifier, Stylize};
|
||||
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));
|
||||
let mut expected = Buffer::with_lines(vec![" Tab1 │ Tab2 │ Tab3 │ Tab4 "]);
|
||||
expected.set_style(Rect::new(0, 0, 30, 1), Style::default().fg(Color::Red));
|
||||
assert_buffer_eq!(render(tabs, Rect::new(0, 0, 30, 1)), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_select() {
|
||||
let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"])
|
||||
.highlight_style(Style::new().reversed());
|
||||
|
||||
// first tab selected
|
||||
let mut expected = Buffer::with_lines(vec![" Tab1 │ Tab2 │ Tab3 │ Tab4 "]);
|
||||
expected.set_style(Rect::new(1, 0, 4, 1), Style::new().reversed());
|
||||
assert_buffer_eq!(
|
||||
render(tabs.clone().select(0), Rect::new(0, 0, 30, 1)),
|
||||
expected
|
||||
);
|
||||
|
||||
// second tab selected
|
||||
let mut expected = Buffer::with_lines(vec![" Tab1 │ Tab2 │ Tab3 │ Tab4 "]);
|
||||
expected.set_style(Rect::new(8, 0, 4, 1), Style::new().reversed());
|
||||
assert_buffer_eq!(
|
||||
render(tabs.clone().select(1), Rect::new(0, 0, 30, 1)),
|
||||
expected
|
||||
);
|
||||
|
||||
// last tab selected
|
||||
let mut expected = Buffer::with_lines(vec![" Tab1 │ Tab2 │ Tab3 │ Tab4 "]);
|
||||
expected.set_style(Rect::new(22, 0, 4, 1), Style::new().reversed());
|
||||
assert_buffer_eq!(
|
||||
render(tabs.clone().select(3), Rect::new(0, 0, 30, 1)),
|
||||
expected
|
||||
);
|
||||
|
||||
// out of bounds selects no tab
|
||||
let expected = Buffer::with_lines(vec![" Tab1 │ Tab2 │ Tab3 │ Tab4 "]);
|
||||
assert_buffer_eq!(
|
||||
render(tabs.clone().select(4), Rect::new(0, 0, 30, 1)),
|
||||
expected
|
||||
);
|
||||
}
|
||||
|
||||
#[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);
|
||||
let mut expected = Buffer::with_lines(vec![" Tab1 │ Tab2 │ Tab3 │ Tab4 "]);
|
||||
expected.set_style(Rect::new(0, 0, 30, 1), Style::new().red());
|
||||
expected.set_style(Rect::new(1, 0, 4, 1), Style::new().reversed());
|
||||
assert_buffer_eq!(render(tabs, Rect::new(0, 0, 30, 1)), expected);
|
||||
}
|
||||
|
||||
#[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 ",])
|
||||
);
|
||||
}
|
||||
|
||||
#[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 │",
|
||||
"└─────────────────────────────────┘",
|
||||
]);
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ use ratatui::{
|
||||
style::{Color, Style},
|
||||
symbols,
|
||||
text::Line,
|
||||
widgets::{Block, Borders, List, ListItem, ListState},
|
||||
widgets::{Block, Borders, HighlightSpacing, List, ListItem, ListState},
|
||||
Terminal,
|
||||
};
|
||||
|
||||
@@ -243,3 +243,128 @@ 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 │",
|
||||
"└────────────────────────────┘",
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -304,7 +304,8 @@ fn widgets_table_columns_widths_can_use_percentage_constraints() {
|
||||
|
||||
#[test]
|
||||
fn widgets_table_columns_widths_can_use_mixed_constraints() {
|
||||
let test_case = |widths, expected| {
|
||||
#[track_caller]
|
||||
fn test_case(widths: &[Constraint], expected: Buffer) {
|
||||
let backend = TestBackend::new(30, 10);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
@@ -324,7 +325,7 @@ fn widgets_table_columns_widths_can_use_mixed_constraints() {
|
||||
})
|
||||
.unwrap();
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
};
|
||||
}
|
||||
|
||||
// columns of zero width show nothing
|
||||
test_case(
|
||||
@@ -356,12 +357,12 @@ fn widgets_table_columns_widths_can_use_mixed_constraints() {
|
||||
],
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│Hea Head2 He │",
|
||||
"│Hea Head2 Hea│",
|
||||
"│ │",
|
||||
"│Row Row12 Ro │",
|
||||
"│Row Row22 Ro │",
|
||||
"│Row Row32 Ro │",
|
||||
"│Row Row42 Ro │",
|
||||
"│Row Row12 Row│",
|
||||
"│Row Row22 Row│",
|
||||
"│Row Row32 Row│",
|
||||
"│Row Row42 Row│",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"└────────────────────────────┘",
|
||||
@@ -398,12 +399,12 @@ fn widgets_table_columns_widths_can_use_mixed_constraints() {
|
||||
],
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│Head1 Head2 │",
|
||||
"│Head1 Head2 │",
|
||||
"│ │",
|
||||
"│Row11 Row12 │",
|
||||
"│Row21 Row22 │",
|
||||
"│Row31 Row32 │",
|
||||
"│Row41 Row42 │",
|
||||
"│Row11 Row12 │",
|
||||
"│Row21 Row22 │",
|
||||
"│Row31 Row32 │",
|
||||
"│Row41 Row42 │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"└────────────────────────────┘",
|
||||
@@ -413,7 +414,8 @@ fn widgets_table_columns_widths_can_use_mixed_constraints() {
|
||||
|
||||
#[test]
|
||||
fn widgets_table_columns_widths_can_use_ratio_constraints() {
|
||||
let test_case = |widths, expected| {
|
||||
#[track_caller]
|
||||
fn test_case(widths: &[Constraint], expected: Buffer) {
|
||||
let backend = TestBackend::new(30, 10);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
@@ -434,7 +436,7 @@ fn widgets_table_columns_widths_can_use_ratio_constraints() {
|
||||
})
|
||||
.unwrap();
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
};
|
||||
}
|
||||
|
||||
// columns of zero width show nothing
|
||||
test_case(
|
||||
@@ -487,12 +489,12 @@ fn widgets_table_columns_widths_can_use_ratio_constraints() {
|
||||
],
|
||||
Buffer::with_lines(vec![
|
||||
"┌────────────────────────────┐",
|
||||
"│Head1 Head2 Head3 │",
|
||||
"│Head1 Head2 Head3 │",
|
||||
"│ │",
|
||||
"│Row11 Row12 Row13 │",
|
||||
"│Row21 Row22 Row23 │",
|
||||
"│Row31 Row32 Row33 │",
|
||||
"│Row41 Row42 Row43 │",
|
||||
"│Row11 Row12 Row13 │",
|
||||
"│Row21 Row22 Row23 │",
|
||||
"│Row31 Row32 Row33 │",
|
||||
"│Row41 Row42 Row43 │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"└────────────────────────────┘",
|
||||
|
||||
Reference in New Issue
Block a user