Compare commits

..

50 Commits

Author SHA1 Message Date
Dheepak Krishnamurthy
0696f484e8 Better ergonomics for ScrollbarState and improved documentation (#456)
* feat(scrollbar): Better ergonomics for ScrollbarState and improved documentation 

* feat(scrollbar)!: Use usize instead of u16 for scrollbar  💥
2023-09-01 06:47:14 +00:00
Valentin271
927a5d8251 docs: fix documentation lint warnings (#450) 2023-08-30 10:01:42 +00:00
a-kenji
28e7fd4bc5 docs(terminal): fix doc comment (#452) 2023-08-30 10:01:28 +00:00
Valentin271
28c61571e8 chore: add documentation guidelines (#447) 2023-08-29 20:57:46 +00:00
Benjamin Grosse
d0779034e7 feat(backend): backend provides window_size, add Size struct (#276)
For image (sixel, iTerm2, Kitty...) support that handles graphics in
terms of `Rect` so that the image area can be included in layouts.

For example: an image is loaded with a known pixel-size, and drawn, but
the image protocol has no mechanism of knowing the actual cell/character
area that been drawn on. It is then impossible to skip overdrawing the
area.

Returning the window size in pixel-width / pixel-height, together with
colums / rows, it can be possible to account the pixel size of each cell
/ character, and then known the `Rect` of a given image, and also resize
the image so that it fits exactly in a `Rect`.

Crossterm and termwiz also both return both sizes from one syscall,
while termion does two.

Add a `Size` struct for the cases where a `Rect`'s `x`/`y` is unused
(always zero).

`Size` is not "clipped" for `area < u16::max_value()` like `Rect`. This
is why there are `From` implementations between the two.
2023-08-29 04:46:02 +00:00
Valentin271
51fdcbe7e9 docs(title): add documentation to title (#443)
This adds documentation for Title and Position
2023-08-29 02:18:02 +00:00
Dheepak Krishnamurthy
eda2fb7077 docs: Use ratatui 📚 (#446) 2023-08-29 01:40:43 +00:00
Orhun Parmaksız
3f781cad0a chore(release): prepare for 0.23.0 (#444) 2023-08-28 11:46:03 +00:00
Hichem
fc727df7d2 refactor(barchart): reduce some calculations (#430)
Calculating the label_offset is unnecessary, if we just render the
group label after rendering the bars. We can just reuse bar_y.

Signed-off-by: Ben Fekih, Hichem <hichem.f@live.de>
2023-08-27 21:10:49 +00:00
Orhun Parmaksız
47fe4ad69f docs(project): make the project description cooler (#441)
* docs(project): make the project description cooler

* docs(lib): simplify description
2023-08-27 20:58:54 +00:00
Orhun Parmaksız
7a70602ec6 docs(examples): fix the instructions for generating demo GIF (#442) 2023-08-27 20:50:33 +00:00
Josh McKinney
14eb6b6979 test(tabs): add unit tests (#439) 2023-08-27 09:38:16 +00:00
Orhun Parmaksız
6009844e25 chore(changelog): ignore alpha tags (#440) 2023-08-27 09:01:17 +00:00
Orhun Parmaksız
8b36683571 docs(lib): extract feature documentation from Cargo.toml (#438)
* docs(lib): extract feature documentation from Cargo.toml

* chore(deps): make `document-features` optional dependency

* docs(lib): document the serde feature from features section
2023-08-27 09:00:35 +00:00
Josh McKinney
e9bd736b1a test(clear): test Clear rendering (#432) 2023-08-26 21:32:23 +00:00
Josh McKinney
a890f2ac00 test(block): test all block methods (#431) 2023-08-26 21:32:01 +00:00
Josh McKinney
b35f19ec44 test(test_backend): add tests for TestBackend coverage (#434)
These are mostly to catch any future bugs introduced in the test backend
2023-08-26 07:25:16 +00:00
Josh McKinney
ad3413eeec test(canvas): add unit tests for line (#437)
Also add constructor to simplify creating lines
2023-08-26 04:38:51 +00:00
Josh McKinney
f0716edbcf test(map): add unit tests (#436) 2023-08-26 01:09:55 +00:00
Josh McKinney
fc9f637fb0 test(text): add unit tests (#435) 2023-08-26 01:08:12 +00:00
Josh McKinney
292a11d81e test(styled_grapheme): test StyledGrapheme methods (#433) 2023-08-26 01:02:30 +00:00
Josh McKinney
ad4d6e7dec test(canvas): add tests for rectangle (#429) 2023-08-25 11:50:10 +00:00
Benjamin Grosse
e4bcf78afa feat(cell): add voluntary skipping capability for sixel (#215)
> Sixel is a bitmap graphics format supported by terminals.
> "Sixel mode" is entered by sending the sequence ESC+Pq.
> The "String Terminator" sequence ESC+\ exits the mode.

The graphics are then rendered with the top left positioned at the
cursor position.

It is actually possible to render sixels in ratatui with just
`buf.get_mut(x, y).set_symbol("^[Pq ... ^[\")`. But any buffer covering
the "image area" will overwrite the graphics. This is most likely the same
buffer, even though it consists of empty characters `' '`, except for
the top-left character that starts the sequence.

Thus, either the buffer or cells must be specialized to avoid drawing
over the graphics. This patch specializes the `Cell` with a
`set_skip(bool)` method, based on James' patch:
https://github.com/TurtleTheSeaHobo/tui-rs/tree/sixel-support
I unsuccessfully tried specializing the `Buffer`, but as far as I can tell
buffers get merged all the way "up" and thus skipping must be set on the
Cells. Otherwise some kind of "skipping area" state would be required,
which I think is too complicated.

Having access to the buffer now it is possible to skipp all cells but the
first one which can then `set_symbol(sixel)`. It is up to the user to
deal with the graphics size and buffer area size. It is possible to get
the terminal's font size in pixels with a syscall.

An image widget for ratatui that uses this `skip` flag is available at
https://github.com/benjajaja/ratatu-image.

Co-authored-by: James <james@rectangle.pizza>
2023-08-25 09:20:36 +00:00
Josh McKinney
d0ee04a69f docs(span): update docs and tests for Span (#427) 2023-08-25 09:08:05 +00:00
Josh McKinney
6d6eceeb88 docs(paragraph): add more docs (#428) 2023-08-25 04:43:19 +00:00
Hichem
0dca6a689a feat(barchart): Add direction attribute. (horizontal bars support) (#325)
* feat(barchart): Add direction attribute

Enable rendring the bars horizontally. In some cases this allow us to
make more efficient use of the available space.

Signed-off-by: Ben Fekih, Hichem <hichem.f@live.de>

* feat(barchart)!: render the group labels depending on the alignment

This is a breaking change, since the alignment by default is set to
Left and the group labels are always rendered in the center.

Signed-off-by: Ben Fekih, Hichem <hichem.f@live.de>

---------

Signed-off-by: Ben Fekih, Hichem <hichem.f@live.de>
2023-08-24 22:26:15 +00:00
Josh McKinney
a937500ae4 chore(changelog): show full commit message (#423)
This allows someone reading the changelog to search for information
about breaking changes or implementation of new functionality.

- refactored the commit template part to a macro instead of repeating it
- added a link to the commit and to the release
- updated the current changelog for the alpha and unreleased changes
- Automatically changed the existing * lists to - lists
2023-08-24 07:59:59 +00:00
Geert Stappers
80fd77e476 Matrix URL (#342)
* docs(readme): add links to matrix bridge

* Update README.md

Co-authored-by: Dheepak Krishnamurthy <me@kdheepak.com>

---------

Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
Co-authored-by: Dheepak Krishnamurthy <me@kdheepak.com>
2023-08-23 16:44:39 +00:00
Josh McKinney
98155dce25 chore(traits): add Display and FromStr traits (#425)
Use strum for most of these, with a couple of manual implementations,
and related tests
2023-08-23 04:21:13 +00:00
hasezoey
1ba2246d95 Document all features in one place (#391)
* chore(Cargo.toml): change "time" to "dep:time"

* docs(lib): document optional and default features
2023-08-23 03:54:42 +00:00
a-kenji
57ea871753 feat: expand serde attributes for TestBuffer (#389) 2023-08-22 12:51:06 +00:00
Dheepak Krishnamurthy
61533712be feat: Add weak constraints to make rects closer to each other in size (#395)
Also make `Max` and `Min` constraints MEDIUM strength for higher priority over equal chunks
2023-08-20 17:40:04 +00:00
Josh McKinney
dc552116cf fix(table): fix unit tests broken due to rounding (#419)
The merge of the table unit tests after the rounding layout fix was not
rebased correctly, this addresses the broken tests, makes them more
concise while adding comments to help clarify that the rounding behavior
is working as expected.
2023-08-20 00:15:20 +00:00
hasezoey
ab5e616635 style(paragraph): add documentation for "scroll"'s "offset" (#355)
* style(paragraph): add documentation for "scroll"'s "offset"

* style(paragraph): add more text to the scroll doc-comment
2023-08-19 21:17:46 +00:00
Orhun Parmaksız
b6b2da5eb7 fix(release): fix the last tag retrieval for alpha releases (#416) 2023-08-19 13:14:17 +00:00
Orhun Parmaksız
89ef0e29f5 chore(ci): update the name of the CI workflow (#417) 2023-08-19 13:14:01 +00:00
hasezoey
4cd843eda9 test(table): add test for consistent table-column-width (#404) 2023-08-19 12:47:23 +00:00
Dheepak Krishnamurthy
d2429bc3e4 chore: Create rust-toolchain.toml (#415) 2023-08-19 03:51:53 +00:00
Dheepak Krishnamurthy
b090101b23 feat: Simplify split function (#411) 2023-08-18 14:43:03 +00:00
Josh McKinney
56455e0fee fix(layout): don't leave gaps between chunks (#408)
Previously the layout used the floor of the calculated start and width
as the value to use for the split Rects. This resulted in gaps between
the split rects.

This change modifies the layout to round to the nearest column instead
of taking the floor of the start and width. This results in the start
and end of each rect being rounded the same way and being strictly
adjacent without gaps.

Because there is a required constraint that ensures that the last end is
equal to the area end, there is no longer the need to fixup the last
item width when the fill (as e.g. width = x.99 now rounds to x+1 not x).

The colors example has been updated to use Ratio(1, 8) instead of
Percentage(13), as this now renders without gaps for all possible sizes,
whereas previously it would have left odd gaps between columns.
2023-08-18 10:23:13 +00:00
Josh McKinney
f4ed3b7584 fix(layout): ensure left <= right (#410)
The recent refactor missed the positive width constraint
2023-08-17 20:15:25 +00:00
hasezoey
c86924b925 Span tests (#406)
* test(span): add some tests for "Span"

* feat(span): replace all "From<*>" with one "From<Into<Cow>>>"
2023-08-17 08:32:26 +00:00
Josh McKinney
de25de0a95 refactor(layout): simplify and doc split() (#405)
* test(layout): add tests for split()

* refactor(layout): simplify and doc split()

This is mainly a reduction in density of the code with a goal of
improving mainatainability so that the algorithm is clear.
2023-08-17 07:44:33 +00:00
hasezoey
ea48af1c9a chore(codecov): fix yaml syntax (#407)
a yaml file cannot contain tabs outside of strings
2023-08-16 10:21:21 +00:00
Josh McKinney
418ed20479 docs(layout): add doc comments (#403) 2023-08-16 00:10:28 +00:00
hasezoey
519509945b refactor(layout): simplify split() function (#396)
Removes some unnecessary code and makes the function more readable.
Instead of creating a temporary result and mutating it, we just create
the result directly from the list of changes.
2023-08-14 23:17:21 +00:00
Josh McKinney
8c55158822 chore: use vhs to create demo.gif (#390)
The bug that prevented braille rendering is fixed, so switch to VHS for
rendering the demo gif

![Demo of Ratatui](https://vhs.charm.sh/vhs-tF0QbuPbtHgUeG0sTVgFr.gif)
2023-08-13 16:21:00 +00:00
Jan Keith Darunday
7748720963 feat(table): add support for line alignment in the table widget (#392)
* feat(table): enforce line alignment in table render

* test(table): add table alignment render test
2023-08-13 08:32:27 +00:00
hasezoey
4d70169bef feat(list): add option to always allocate the "selection" column width (#394)
* feat(list): add option to always allocate the "selection" column width

Before this option was available, selecting a item in a list when nothing was selected
previously made the row layout change (the same applies to unselecting) by adding the width
of the "highlight symbol" in the front of the list, this option allows to configure this
behavior.

* style: change "highlight_spacing" doc comment to use inline code-block for reference
2023-08-13 08:24:51 +00:00
Josh McKinney
10dbd6f207 docs(examples): show layout constraints (#393)
Shows the way that layout constraints interact visually

![example](https://vhs.charm.sh/vhs-1ZNoNLNlLtkJXpgg9nCV5e.gif)
2023-08-13 07:38:43 +00:00
53 changed files with 5260 additions and 1113 deletions

View File

@@ -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

View File

@@ -1,4 +1,4 @@
name: CI
name: Continuous Integration
on:
# Allows you to run this workflow manually from the Actions tab

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -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"

View File

@@ -2,8 +2,8 @@
<img align="left" src="https://avatars.githubusercontent.com/u/125200832?s=128&v=4">
`ratatui` is a [Rust](https://www.rust-lang.org) library 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.
[![Crates.io](https://img.shields.io/crates/v/ratatui?logo=rust&style=flat-square)](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)
[![Codecov](https://img.shields.io/codecov/c/github/ratatui-org/ratatui?logo=codecov&style=flat-square&token=BAQ8SOKEST)](https://app.codecov.io/gh/ratatui-org/ratatui)
[![Discord](https://img.shields.io/discord/1070692720437383208?label=discord&logo=discord&style=flat-square)](https://discord.gg/pMCEU9hNEj)
[![Matrix](https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix)](https://matrix.to/#/#ratatui:matrix.org)
<!-- See RELEASE.md for instructions on creating the demo gif --->
![Demo of Ratatui](https://github.com/ratatui-org/ratatui/assets/24392180/93ab0e38-93e0-4ae0-a31b-91ae6c393185)
![Demo of Ratatui](https://vhs.charm.sh/vhs-tF0QbuPbtHgUeG0sTVgFr.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

View File

@@ -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).

View File

@@ -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

View File

@@ -1,2 +1,2 @@
ignore:
- "examples"
- "examples"

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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
View 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

View File

@@ -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}"),
}
}

View File

@@ -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

View File

@@ -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
View File

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

View File

@@ -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()
}

View File

@@ -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));
}
}

View File

@@ -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()
}

View File

@@ -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)
}

View File

@@ -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();
}
}

View File

@@ -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]);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,9 @@
#![forbid(unsafe_code)]
//! [ratatui](https://github.com/ratatui-org/ratatui) is a library 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).
//!
//! ![](https://raw.githubusercontent.com/ratatui-org/ratatui/master/assets/demo.gif)
//! ![Demo](https://vhs.charm.sh/vhs-tF0QbuPbtHgUeG0sTVgFr.gif)
//!
//! # 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))]

View File

@@ -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
///

View File

@@ -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));
}
}

View File

@@ -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)"
);
}
}

View File

@@ -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());
}
}

View File

@@ -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());
}
}

View File

@@ -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"),
]
);
}
}

View File

@@ -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));
}
}

View File

@@ -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,

View File

@@ -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> {

View File

@@ -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);
}
}

View File

@@ -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![
"┏━━━━━━━━━━━━━┓",
"┃ ┃",
"┗━━━━━━━━━━━━━┛"
])
);
}
}

View File

@@ -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

View File

@@ -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![
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
],
);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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));
}
}

View File

@@ -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",
])
);
}
}

View File

@@ -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"]);

View File

@@ -13,6 +13,7 @@
//! - [`BarChart`]
//! - [`Gauge`]
//! - [`Sparkline`]
//! - [`Scrollbar`]
//! - [`calendar::Monthly`]
//! - [`Clear`]

View File

@@ -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
}

View File

@@ -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));

View File

@@ -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 {

View File

@@ -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)
);
}
}

View File

@@ -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() {

View File

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

View File

@@ -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 │",
"└────────────────────────────┘",
]),
);
}

View File

@@ -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 │",
"│ │",
"│ │",
"└────────────────────────────┘",