Compare commits

..

82 Commits

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

```rust
let [top, main] =
    Layout::vertical([Constraint::Length(1), Constraint::Min(0)])
    .split_array(area);
```
2024-01-03 20:27:36 -08:00
Orhun Parmaksız
50374b2456 docs(backend): fix broken book link (#733) 2024-01-03 07:23:59 -05:00
Akiomi Kamakura
49df5d4626 docs(example): fix markdown syntax for note (#730) 2024-01-02 17:20:50 -08:00
Josh McKinney
7ab12ed8ce feat(layout): add horizontal and vertical constructors (#728)
* feat(layout): add vertical and horizontal constructors

This commit adds two new constructors to the `Layout` struct, which
allow the user to create a vertical or horizontal layout with default
values.

```rust
let layout = Layout::vertical([
    Constraint::Length(10),
    Constraint::Min(5),
    Constraint::Length(10),
]);

let layout = Layout::horizontal([
    Constraint::Length(10),
    Constraint::Min(5),
    Constraint::Length(10),
]);
```
2024-01-02 15:59:33 -08:00
Valentin271
b459228e26 feat(termwiz): add From termwiz style impls (#726)
Important note: this also fixes a wrong mapping between ratatui's gray
and termwiz's grey. `ratatui::Color::Gray` now maps to
`termwiz::color::AnsiColor::Silver`
2024-01-02 13:19:14 -08:00
Josh McKinney
8f56fabcdd feat: accept Color and Modifier for all Styles (#720)
* feat: accept Color and Modifier for all Styles

All style related methods now accept `S: Into<Style>` instead of
`Style`.
`Color` and `Modifier` implement `Into<Style>` so this is allows for
more ergonomic usage. E.g.:

```rust
Line::styled("hello", Style::new().red());
Line::styled("world", Style::new().bold());

// can now be simplified to

Line::styled("hello", Color::Red);
Line::styled("world", Modifier::BOLD);
```

Fixes https://github.com/ratatui-org/ratatui/issues/694

BREAKING CHANGE: All style related methods now accept `S: Into<Style>`
instead of `Style`. This means that if you are already passing an
ambiguous type that implements `Into<Style>` you will need to remove
the `.into()` call.

`Block` style methods can no longer be called from a const context as
trait functions cannot (yet) be const.

* feat: add tuple conversions to Style

Adds conversions for various Color and Modifier combinations

* chore: add unit tests
2023-12-31 10:01:06 -08:00
Josh McKinney
a62632a947 refactor(buffer): split buffer module into files (#721) 2023-12-29 10:00:50 -08:00
Antonio Yang
f025d2bfa2 feat(table): Add Table::footer and Row::top_margin methods (#722)
* feat(table): Add a Table::footer method

Signed-off-by: Antonio Yang <yanganto@gmail.com>

* feat(table): Add a Row::top_margin method

- add Row::top_margin
- update table example

Signed-off-by: Antonio Yang <yanganto@gmail.com>

---------

Signed-off-by: Antonio Yang <yanganto@gmail.com>
2023-12-29 07:44:41 -08:00
Josh McKinney
63645333d6 refactor(table): split table into multiple files (#718)
At close to 2000 lines of code, the table widget was getting a bit
unwieldy. This commit splits it into multiple files, one for each
struct, and one for the table itself.

Also refactors the table rendering code to be easier to maintain.
2023-12-27 20:43:01 -08:00
Josh McKinney
5d410c6895 feat(line): implement Widget for Line (#715)
This allows us to use Line as a child of other widgets, and to use
Line::render() to render it rather than calling buffer.set_line().

```rust
frame.render_widget(Line::raw("Hello, world!"), area);
// or
Line::raw("Hello, world!").render(frame, area);
```
2023-12-27 20:30:47 +01:00
Orhun Parmaksız
8d77b734bb chore(ci): use cargo-nextest for running tests (#717)
* chore(ci): use cargo-nextest for running tests

* refactor(make): run library tests before doc tests
2023-12-27 10:50:56 -08:00
Josh McKinney
9574198958 refactor(line): reorder methods for natural reading order (#713) 2023-12-27 05:10:21 -08:00
Josh McKinney
ee54493163 fix(buffer): don't panic in set_style (#714)
This fixes a panic in set_style when the area to be styled is
outside the buffer's bounds.
2023-12-27 10:19:30 +01:00
Josh McKinney
c977293f14 feat(line)!: add style field, setters and docs (#708)
- The `Line` struct now stores the style of the line rather than each
  `Span` storing it.
- Adds two new setters for style and spans
- Adds missing docs

BREAKING CHANGE: `Line::style` is now a field of `Line` instead of being
stored in each `Span`.
2023-12-27 10:10:41 +01:00
Josh McKinney
b0ed658970 fix(table): render missing widths as equal (#710)
Previously, if `.widths` was not called before rendering a `Table`, no
content would render in the area of the table. This commit changes that
behaviour to default to equal widths for each column.

Fixes #510.

Co-authored-by: joshcbrown <80245312+joshcbrown@users.noreply.github.com>
2023-12-26 15:06:44 +01:00
Josh McKinney
37c183636b feat(span): implement Widget on Span (#709)
This allows us to use Span as a child of other widgets, and to use
Span::render() to render it rather than calling buffer.set_span().

```rust
frame.render_widget(Span::raw("Hello, world!"), area);
// or
Span::raw("Hello, world!").render(frame, area);
// or even
"Hello, world!".green().render(frame, area);
```
2023-12-26 15:05:57 +01:00
a-kenji
e67d3c64e0 docs(table): fix typo (#707) 2023-12-25 15:19:29 +01:00
Orhun Parmaksız
4f2db82a77 feat(color): use the FromStr implementation for deserialization (#705)
The deserialize implementation for Color used to support only the enum
names (e.g. Color, LightRed, etc.) With this change, you can use any of
the strings supported by the FromStr implementation (e.g. black,
light-red, #00ff00, etc.)
2023-12-23 19:38:53 +01:00
Valentin271
d6b851301e docs(examples): refactor chart example to showcase scatter (#703) 2023-12-22 16:57:26 -08:00
Josh McKinney
b7a479392e chore(ci): bump alpha release for breaking changes (#495)
Automatically detect breaking changes based on commit messages
and bump the alpha release number accordingly.

E.g. v0.23.1-alpha.1 will be bumped to v0.24.0-alpha.0 if any commit
since v0.23.0 has a breaking change.
2023-12-21 13:30:25 +01:00
a-kenji
e1cc849554 docs(breaking): fix typo (#702) 2023-12-18 16:06:49 +01:00
Orhun Parmaksız
7f5884829c chore(release): prepare for 0.25.0 (#699) 2023-12-18 13:06:28 +01:00
Orhun Parmaksız
a15c3b2660 docs: remove deprecated table constructor from breaking changes (#698) 2023-12-17 15:29:19 +01:00
Josh McKinney
41c44a4af6 docs(frame): add docs about resize events (#697) 2023-12-17 01:36:25 -08:00
Josh McKinney
1b8b6261e2 docs(examples): add animation and FPS counter to colors_rgb (#583) 2023-12-17 01:34:59 -08:00
Valentin271
5bf4f52119 feat: implement From trait for termion to Style related structs (#692)
* feat(termion): implement from termion color

* feat(termion): implement from termion style

* feat(termion): implement from termion `Bg` and `Fg`
2023-12-16 20:48:44 +01:00
Valentin271
f4c8de041d docs(chart): document chart module (#696) 2023-12-16 11:41:12 -08:00
Valentin271
910ad00059 chore(rustfmt): enable format_code_in_doc_comments (#695)
This enables more consistently formatted code in doc comments,
especially since ratatui heavily uses fluent setters.

See https://rust-lang.github.io/rustfmt/?version=v1.6.0#format_code_in_doc_comments
2023-12-16 13:01:07 +01:00
Valentin271
b282a06932 refactor!: remove items deprecated since 0.10 (#691)
Remove `Axis::title_style` and `Buffer::set_background` which are deprecated since 0.10
2023-12-15 16:22:34 +01:00
Lee Wonjoon
b8f71c0d6e feat(widgets/chart): add option to set the position of legend (#378) 2023-12-15 04:31:58 -08:00
Dheepak Krishnamurthy
113b4b7a4e docs: Rename template links to remove ratatui from name 📚 (#690) 2023-12-15 07:44:44 +01:00
Valentin271
b82451fb33 refactor(examples): add vim binding (#688) 2023-12-14 19:11:48 -08:00
Valentin271
4be18aba8b refactor(readme): reference awesome-ratatui instead of wiki (#689)
* refactor(readme): link awesome-ratatui instead of wiki

The apps wiki moved to awesome-ratatui

* Update README.md

Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>

* Update README.md

Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>

* Update README.md

Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>

---------

Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>
2023-12-14 20:45:36 +01:00
Valentin271
ebf1f42942 feat(style): implement From trait for crossterm to Style related structs (#686) 2023-12-14 13:25:36 +01:00
Josh McKinney
2169a0da01 docs(examples): Add example of half block rendering (#687)
This is a fun example of how to render big text using half blocks
2023-12-13 18:25:21 -08:00
Josh McKinney
d118565ef6 chore(table): cleanup docs and builder methods (#638)
- Refactor the `table` module for better top to bottom readability by
putting types first and arranging them in a logical order (Table, Row,
Cell, other).

- Adds new methods for:
  - `Table::rows`
  - `Row::cells`
  - `Cell::new`
  - `Cell::content`
  - `TableState::new`
  - `TableState::selected_mut`

- Makes `HighlightSpacing::should_add` pub(crate) since it's an internal
  detail.

- Adds tests for all the new methods and simple property tests for all
  the other setter methods.
2023-12-13 15:53:01 +01:00
YeungKC
aaeba2709c fix: truncate table when overflow (#685)
This prevents a panic when rendering an empty right aligned and rightmost table cell
2023-12-12 18:29:07 +01:00
Josh McKinney
d19b266e0e feat: add Constraint helpers (e.g. from_lengths) (#641)
Adds helper methods that convert from iterators of u16 values to the
specific Constraint type. This makes it easy to create constraints like:

```rust
// a fixed layout
let constraints = Constraint::from_lengths([10, 20, 10]);

// a centered layout
let constraints = Constraint::from_ratios([(1, 4), (1, 2), (1, 4)]);
let constraints = Constraint::from_percentages([25, 50, 25]);

// a centered layout with a minimum size
let constraints = Constraint::from_mins([0, 100, 0]);

// a sidebar / main layout with maximum sizes
let constraints = Constraint::from_maxes([30, 200]);
```
2023-12-10 18:01:55 -08:00
Valentin271
f767ea7d37 refactor(List): start_corner is now direction (#673)
The previous name `start_corner` did not communicate clearly the intent of the method.
A new method `direction` and a new enum `ListDirection` were added.

`start_corner` is now deprecated
2023-12-10 16:50:13 -08:00
Josh McKinney
0576a8aa32 refactor(layout): to natural reading order (#681)
Structs and enums at the top of the file helps show the interaction
between the types without having to find each type in between longer
impl sections.

Also moved the try_split function into the Layout impl as an associated
function and inlined the `layout::split()` which just called try_split.
This makes the code a bit more contained.
2023-12-09 13:45:41 -08:00
Josh McKinney
03401cd46e ci: fix untrusted input in pr check workflow (#680) 2023-12-09 06:49:26 -08:00
Valentin271
f69d57c3b5 fix(Rect): fix underflow in the Rect::intersection method (#678) 2023-12-09 06:27:41 -08:00
Josh McKinney
2a87251152 docs(security): add security policy (#676)
* docs: Create SECURITY.md

* Update SECURITY.md

Co-authored-by: Orhun Parmaksız <orhun@archlinux.org>

---------

Co-authored-by: Orhun Parmaksız <orhun@archlinux.org>
2023-12-09 11:39:57 +01:00
Valentin271
aef495604c feat(List)!: List::new now accepts IntoIterator<Item = Into<ListItem>> (#672)
This allows to build list like

```
List::new(["Item 1", "Item 2"])
```

BREAKING CHANGE: `List::new` parameter type changed from `Into<Vec<ListItem<'a>>>`
to `IntoIterator<Item = Into<ListItem<'a>>>`
2023-12-08 14:20:49 -08:00
Tyler Bloom
8bfd6661e2 feat(Paragraph): add line_count and line_width unstable helper methods
This is an unstable feature that may be removed in the future
2023-12-07 18:14:56 +01:00
Valentin271
3ec4e24d00 docs(list): add documentation to the List widget (#669)
Adds documentation to the List widget and all its sub components like `ListState` and `ListItem`
2023-12-07 11:44:50 +01:00
Val Lorentz
7ced7c0aa3 refactor: define struct WrappedLine instead of anonymous tuple (#608)
It makes the type easier to document, and more obvious for users
2023-12-06 22:18:02 +01:00
tieway59
dd22e721e3 chore: Correct "builder methods" in docs and add must_use on widgets setters (#655)
Fixes #650

This PR corrects the "builder methods" expressing to simple `setters`
(see #650 #655), and gives a clearer diagnostic notice on setters `must_use`.

`#[must_use = "method moves the value of self and returns the modified value"]`

Details:

    docs: Correct wording in docs from builder methods

    Add `must_use` on layout setters

    chore: add `must_use` on widgets fluent methods

        This commit ignored `table.rs` because it is included in other PRs.

    test(gauge): fix test
2023-12-06 15:39:52 +01:00
Josh McKinney
4424637af2 feat(span): add setters for content and style (#647) 2023-12-05 01:17:53 -08:00
Josh McKinney
37c70dbb8e fix(table)!: Add widths parameter to new() (#664)
This prevents creating a table that doesn't actually render anything.

Fixes: https://github.com/ratatui-org/ratatui/issues/537

BREAKING CHANGE: Table::new() now takes an additional widths parameter.
2023-12-04 15:22:52 -08:00
Orhun Parmaksız
91c67eb100 docs(github): update code owners (#666)
onboard @Valentin271 as maintainer
2023-12-04 13:53:26 -08:00
Alan Somers
e49385b78c feat(table): Add a Table::segment_size method (#660)
It controls how to distribute extra space to an underconstrained table.
The default, legacy behavior is to leave the extra space unused.  The
new options are LastTakesRemainder which gets all space to the rightmost
column that can used it, and EvenDistribution which divides it amongst
all columns.

Fixes #370
2023-12-04 13:51:02 -08:00
Josh McKinney
6b2efd0f6c feat(layout): Accept IntoIterator for constraints (#663)
Layout and Table now accept IntoIterator for constraints with an Item
that is AsRef<Constraint>. This allows pretty much any collection of
constraints to be passed to the layout functions including arrays,
vectors, slices, and iterators (without having to call collect() on
them).
2023-12-04 10:46:21 -08:00
Josh McKinney
34d099c99a fix(tabs): fixup tests broken by semantic merge conflict (#665)
Two changes without any line overlap caused the tabs tests to break
2023-12-04 10:45:31 -08:00
Josh McKinney
987f7eed4c docs(website): rename book to website (#661) 2023-12-04 11:49:54 +01:00
Josh McKinney
e4579f0db2 fix(tabs)!: set the default highlight_style (#635)
Previously the default highlight_style was set to `Style::default()`,
which meant that the highlight style was the same as the normal style.
This change sets the default highlight_style to reversed text.

BREAKING CHANGE: The `Tab` widget now renders the highlight style as
reversed text by default. This can be changed by setting the
`highlight_style` field of the `Tab` widget.
2023-12-04 01:39:46 -08:00
Josh McKinney
6a6e9dde9d style(tabs): fix doc formatting (#662) 2023-12-04 01:38:57 -08:00
Rhaskia
28ac55bc62 fix(tabs): Tab widget now supports custom padding (#629)
The Tab widget now contains padding_left and and padding_right
properties. Those values can be set with functions `padding_left()`,
`padding_right()`, and `padding()` whic all accept `Into<Line>`.

Fixes issue https://github.com/ratatui-org/ratatui/issues/502
2023-12-03 16:38:43 -08:00
Orhun Parmaksız
458fa90362 docs(lib): tweak the crate documentation (#659) 2023-12-03 15:59:53 -08:00
Jan Ferdinand Sauer
56fc410105 fix(block): make inner aware of title positions (#657)
Previously, when computing the inner rendering area of a block, all
titles were assumed to be positioned at the top, which caused the
height of the inner area to be miscalculated.
2023-12-03 13:57:59 +01:00
Josh McKinney
753e246531 feat(layout): allow configuring layout fill (#633)
The layout split will generally fill the remaining area when `split()`
is called. This change allows the caller to configure how any extra
space is allocated to the `Rect`s. This is useful for cases where the
caller wants to have a fixed size for one of the `Rect`s, and have the
other `Rect`s fill the remaining space.

For now, the method and enum are marked as unstable because the exact
name is still being bikeshedded. To enable this functionality, add the
`unstable-segment-size` feature flag in your `Cargo.toml`.

To configure the layout to fill the remaining space evenly, use
`Layout::segment_size(SegmentSize::EvenDistribution)`. The default
behavior is `SegmentSize::LastTakesRemainder`, which gives the last
segment the remaining space. `SegmentSize::None` will disable this
behavior. See the docs for `Layout::segment_size()` and
`layout::SegmentSize` for more information.

Fixes https://github.com/ratatui-org/ratatui/issues/536
2023-12-02 12:21:13 -08:00
Josh McKinney
211160ca16 docs: remove simple-tui-rs (#651)
This has not been recently and doesn't lead to good code
2023-12-02 16:17:50 +01:00
Valentin271
1229b96e42 feat(Rect): add offset method (#533)
The offset method creates a new Rect that is moved by the amount
specified in the x and y direction. These values can be positive or
negative. This is useful for manual layout tasks.

```rust
let rect = area.offset(Offset { x: 10, y -10 });
```
2023-11-27 08:03:18 -08:00
Valentin271
fe632d70cb docs(Sparkline): add documentation (#648) 2023-11-26 15:46:20 -08:00
Orhun Parmaksız
c862aa5e9e feat(list): support line alignment (#599)
The `List` widget now respects the alignment of `Line`s and renders them as expected.
2023-11-23 11:52:12 -08:00
Josh McKinney
18e19f6ce6 chore: fix breaking changes doc versions (#639)
Moves the layout::new change to unreleasedd section and adds the table change
2023-11-22 23:59:06 +01:00
Linda_pp
7ef0afcb62 refactor(widgets): remove unnecessary dynamic dispatch and heap allocation (#597)
Signed-off-by: rhysd <lin90162@yahoo.co.jp>
2023-11-21 22:24:54 -08:00
Josh McKinney
1e2f0be75a feat(layout)!: add parameters to Layout::new() (#557)
Adds a convenience function to create a layout with a direction and a
list of constraints which are the most common parameters that would be
generally configured using the builder pattern. The constraints can be
passed in as any iterator of constraints.

```rust
let layout = Layout::new(Direction::Horizontal, [
    Constraint::Percentage(50),
    Constraint::Percentage(50),
]);
```

BREAKING CHANGE:
Layout::new() now takes a direction and a list of constraints instead of
no arguments. This is a breaking change because it changes the signature
of the function. Layout::new() is also no longer const because it takes
an iterator of constraints.
2023-11-21 14:34:08 -08:00
Leon Sautour
a58cce2dba chore: disable default benchmarking (#598)
Disables the default benchmarking behaviour for the lib target to fix unrecognized
criterion benchmark arguments.

See https://bheisler.github.io/criterion.rs/book/faq.html#cargo-bench-gives-unrecognized-option-errors-for-valid-command-line-options for details
2023-11-21 14:23:21 -08:00
Jonathan Chan Kwan Yin
ffa78aa67c fix: add #[must_use] to Style-moving methods (#600) 2023-11-21 14:21:28 -08:00
dependabot[bot]
7cbb1060ac chore(deps): update itertools requirement from 0.11 to 0.12 (#636)
Updates the requirements on [itertools](https://github.com/rust-itertools/itertools) to permit the latest version.
- [Changelog](https://github.com/rust-itertools/itertools/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-itertools/itertools/compare/v0.11.0...v0.12.0)

---
updated-dependencies:
- dependency-name: itertools
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-20 14:47:59 -08:00
dependabot[bot]
a05541358e chore(deps): bump actions/github-script from 6 to 7 (#637)
Bumps [actions/github-script](https://github.com/actions/github-script) from 6 to 7.
- [Release notes](https://github.com/actions/github-script/releases)
- [Commits](https://github.com/actions/github-script/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/github-script
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-20 14:43:06 -08:00
Josh McKinney
1f88da7538 fix(table): fix new clippy lint which triggers on table widths tests (#630)
* fix(table): new clippy lint in 1.74.0 triggers on table widths tests

https://rust-lang.github.io/rust-clippy/master/index.html\#/needless_borrows_for_generic_args

* fix(clippy): fix beta lint for .get(0) -> .first()

https://rust-lang.github.io/rust-clippy/master/index.html\#/get_first
2023-11-16 18:52:32 +01:00
Josh McKinney
36d8c53645 fix(table): widths() now accepts AsRef<[Constraint]> (#628)
This allows passing an array, slice or Vec of constraints, which is more
ergonomic than requiring this to always be a slice.

The following calls now all succeed:

```rust
Table::new(rows).widths([Constraint::Length(5), Constraint::Length(5)]);
Table::new(rows).widths(&[Constraint::Length(5), Constraint::Length(5)]);

// widths could also be computed at runtime
let widths = vec![Constraint::Length(5), Constraint::Length(5)];
Table::new(rows).widths(widths.clone());
Table::new(rows).widths(&widths);
```
2023-11-15 12:34:02 -08:00
Linda_pp
ec7b3872b4 fix(doc): do not access deprecated Cell::symbol field in doc example (#626) 2023-11-13 06:29:01 -08:00
Linda_pp
edacaf7ff4 feat(buffer): deprecate Cell::symbol field (#624)
The Cell::symbol field is now accessible via a getter method (`symbol()`). This will
allow us to make future changes to the Cell internals such as replacing `String` with
`compact_str`.
2023-11-11 21:43:51 -08:00
Danny Burrows
df0eb1f8e9 fix(terminal): insert_before() now accepts lines > terminal height and doesn't add an extra blank line (#596)
Fixes issue with inserting content with height>viewport_area.height and adds
the ability to insert content of height>terminal_height

- Adds TestBackend::append_lines() and TestBackend::clear_region() methods to
  support testing the changes
2023-11-08 10:04:35 -08:00
Josh McKinney
59b9c32fbc ci(codecov): adjust threshold and noise settings (#615)
Fixes https://github.com/ratatui-org/ratatui/issues/612
2023-11-05 10:21:11 +01:00
isinstance
9f37100096 Update README.md and fix the bug that demo2 cannot run (#595)
Fixes https://github.com/ratatui-org/ratatui/issues/594
2023-10-25 14:01:04 -07:00
a-kenji
a2f2bd5df5 fix: MSRV is now 1.70.0 (#593) 2023-10-25 02:44:36 -07:00
80 changed files with 11217 additions and 3432 deletions

2
.github/CODEOWNERS vendored
View File

@@ -5,4 +5,4 @@
# https://git-scm.com/docs/gitignore#_pattern_format
# Maintainers
* @orhun @mindoodoo @sayanarijit @joshka @kdheepak
* @orhun @mindoodoo @sayanarijit @joshka @kdheepak @Valentin271

View File

@@ -0,0 +1,52 @@
#!/bin/bash
# Exit on error. Append "|| true" if you expect an error.
set -o errexit
# Exit on error inside any functions or subshells.
set -o errtrace
# Do not allow use of undefined vars. Use ${VAR:-} to use an undefined VAR
set -o nounset
# Catch the error in case mysqldump fails (but gzip succeeds) in `mysqldump |gzip`
set -o pipefail
# Turn on traces, useful while debugging but commented out by default
# set -o xtrace
last_release="$(git tag --sort=committerdate | grep -E "v0\.\d+\.\d+$" | tail -1)"
echo "🐭 Last release: ${last_release}"
# detect breaking changes
if git log --oneline ${last_release}..HEAD | grep -q '!:' || true; then
echo "🐭 Breaking changes detected since ${last_release}"
git log --oneline ${last_release}..HEAD | grep '!:'
# increment the minor version
minor="${last_release##v0.}"
minor="${minor%.*}"
next_minor="$((minor + 1))"
next_release="v0.${next_minor}.0"
else
# increment the patch version
patch="${last_release##*.}"
next_patch="$((patch + 1))"
next_release="${last_release/%${patch}/${next_patch}}"
fi
echo "🐭 Next release: ${next_release}"
suffix="alpha"
last_tag="$(git tag --sort=committerdate | tail -1)"
if [[ "${last_tag}" = "${next-release}-${suffix}"* ]]; then
echo "🐭 Last alpha release: ${last_tag}"
# increment the alpha version
# e.g. v0.22.1-alpha.12 -> v0.22.1-alpha.13
alpha="${last_tag##*-${suffix}.}"
next_alpha="$((alpha + 1))"
next_tag="${last_tag/%${alpha}/${next_alpha}}"
else
# increment the patch and start the alpha version from 0
# e.g. v0.22.0 -> v0.22.1-alpha.0
next_tag="${next_release}-${suffix}.0"
fi
# update the crate version
msg="# crate version"
sed -E -i "s/^version = .* ${msg}$/version = \"${next_tag#v}\" ${msg}/" Cargo.toml
echo "NEXT_TAG=${next_tag}" >> $GITHUB_ENV
echo "🐭 Next alpha release: ${next_tag}"

View File

@@ -28,27 +28,7 @@ jobs:
fetch-depth: 0
- name: Calculate the next release
run: |
suffix="alpha"
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
alpha="${last_tag##*-${suffix}.}"
next_alpha="$((alpha + 1))"
next_tag="${last_tag/%${alpha}/${next_alpha}}"
else
# increment the patch and start the alpha version from 0
# e.g. v0.22.0 -> v0.22.1-alpha.0
patch="${last_tag##*.}"
next_patch="$((patch + 1))"
next_tag="${last_tag/%${patch}/${next_patch}}-${suffix}.0"
fi
# update the crate version
msg="# crate version"
sed -E -i "s/^version = .* ${msg}$/version = \"${next_tag#v}\" ${msg}/" Cargo.toml
echo "NEXT_TAG=${next_tag}" >> $GITHUB_ENV
echo "Next alpha release: ${next_tag} 🐭"
run: .github/workflows/calculate-alpha-release.bash
- name: Publish on crates.io
uses: actions-rs/cargo@v1

View File

@@ -46,20 +46,24 @@ jobs:
check-breaking-change-label:
runs-on: ubuntu-latest
env:
# use an environment variable to pass untrusted input to the script
# see https://securitylab.github.com/research/github-actions-untrusted-input/
PR_TITLE: ${{ github.event.pull_request.title }}
steps:
- name: Check breaking change label
id: check_breaking_change
run: |
pattern='^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\(\w+\))?!:'
# Check if pattern matches
if echo "${{ github.event.pull_request.title }}" | grep -qE "$pattern"; then
if echo "${PR_TITLE}" | grep -qE "$pattern"; then
echo "breaking_change=true" >> $GITHUB_OUTPUT
else
echo "breaking_change=false" >> $GITHUB_OUTPUT
fi
- name: Add label
if: steps.check_breaking_change.outputs.breaking_change == 'true'
uses: actions/github-script@v6
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |

View File

@@ -96,7 +96,7 @@ jobs:
fail-fast: false
matrix:
os: [ ubuntu-latest, windows-latest, macos-latest ]
toolchain: [ "1.67.0", "stable" ]
toolchain: [ "1.70.0", "stable" ]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
@@ -135,7 +135,7 @@ jobs:
fail-fast: false
matrix:
os: [ ubuntu-latest, windows-latest, macos-latest ]
toolchain: [ "1.67.0", "stable" ]
toolchain: [ "1.70.0", "stable" ]
backend: [ crossterm, termion, termwiz ]
exclude:
# termion is not supported on windows
@@ -151,6 +151,8 @@ jobs:
toolchain: ${{ matrix.toolchain }}
- name: Install cargo-make
uses: taiki-e/install-action@cargo-make
- name: Install cargo-make
uses: taiki-e/install-action@nextest
- name: Test ${{ matrix.backend }}
run: cargo make test-backend ${{ matrix.backend }}
env:

View File

@@ -1,7 +1,7 @@
# Breaking Changes
This document contains a list of breaking changes in each version and some notes to help migrate
between versions. It is compile manually from the commit history and changelog. We also tag PRs on
between versions. It is compiled manually from the commit history and changelog. We also tag PRs on
github with a [breaking change] label.
[breaking change]: (https://github.com/ratatui-org/ratatui/issues?q=label%3A%22breaking+change%22)
@@ -10,7 +10,18 @@ github with a [breaking change] label.
This is a quick summary of the sections below:
- [v0.26.0 (unreleased)](#v0260-unreleased)
- `Line` now has an extra `style` field which applies the style to the entire line
- `Block` style methods cannot be created in a const context
- [v0.25.0](#v0250)
- Removed `Axis::title_style` and `Buffer::set_background`
- `List::new()` now accepts `IntoIterator<Item = Into<ListItem<'a>>>`
- `Table::new()` now requires specifying the widths
- `Table::widths()` now accepts `IntoIterator<Item = AsRef<Constraint>>`
- Layout::new() now accepts direction and constraint parameters
- The default `Tabs::highlight_style` is now `Style::new().reversed()`
- [v0.24.0](#v0240)
- MSRV is now 1.70.0
- `ScrollbarState`: `position`, `content_length`, and `viewport_content_length` are now `usize`
- `BorderType`: `line_symbols` is now `border_symbols` and returns `symbols::border::set`
- `Frame<'a, B: Backend>` is now `Frame<'a>`
@@ -31,6 +42,146 @@ This is a quick summary of the sections below:
- MSRV is now 1.63.0
- `List` no longer ignores empty strings
## v0.26.0 (unreleased)
### `Block` style methods cannot be used in a const context ([#720])
[#720]: https://github.com/ratatui-org/ratatui/pull/720
Previously the `style()`, `border_style()` and `title_style()` methods could be used to create a
`Block` in a constant context. These now accept `Into<Style>` instead of `Style`. These methods no
longer can be called from a constant context.
### `Line` now has a `style` field that applies to the entire line ([#708])
[#708]: https://github.com/ratatui-org/ratatui/pull/708
Previously the style of a `Line` was stored in the `Span`s that make up the line. Now the `Line`
itself has a `style` field, which can be set with the `Line::style` method. Any code that creates
`Line`s using the struct initializer instead of constructors will fail to compile due to the added
field. This can be easily fixed by adding `..Default::default()` to the field list or by using a
constructor method (`Line::styled()`, `Line::raw()`) or conversion method (`Line::from()`).
Each `Span` contained within the line will no longer have the style that is applied to the line in
the `Span::style` field.
```diff
let line = Line {
spans: vec!["".into()],
alignment: Alignment::Left,
+ ..Default::default()
};
// or
let line = Line::raw(vec!["".into()])
.alignment(Alignment::Left);
```
## [v0.25.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.25.0)
### Removed `Axis::title_style` and `Buffer::set_background` ([#691])
[#691]: https://github.com/ratatui-org/ratatui/pull/691
These items were deprecated since 0.10.
- You should use styling capabilities of [`text::Line`] given as argument of [`Axis::title`]
instead of `Axis::title_style`
- You should use styling capabilities of [`Buffer::set_style`] instead of `Buffer::set_background`
[`text::Line`]: https://docs.rs/ratatui/latest/ratatui/text/struct.Line.html
[`Axis::title`]: https://docs.rs/ratatui/latest/ratatui/widgets/struct.Axis.html#method.title
[`Buffer::set_style`]: https://docs.rs/ratatui/latest/ratatui/buffer/struct.Buffer.html#method.set_style
### `List::new()` now accepts `IntoIterator<Item = Into<ListItem<'a>>>` ([#672])
[#672]: https://github.com/ratatui-org/ratatui/pull/672
Previously `List::new()` took `Into<Vec<ListItem<'a>>>`. This change will throw a compilation
error for `IntoIterator`s with an indeterminate item (e.g. empty vecs).
E.g.
```diff
- let list = List::new(vec![]);
// becomes
+ let list = List::default();
```
### The default `Tabs::highlight_style` is now `Style::new().reversed()` ([#635])
[#635]: https://github.com/ratatui-org/ratatui/pull/635
Previously the default highlight style for tabs was `Style::default()`, which meant that a `Tabs`
widget in the default configuration would not show any indication of the selected tab.
### The default `Tabs::highlight_style` is now `Style::new().reversed()` ([#635])
Previously the default highlight style for tabs was `Style::default()`, which meant that a `Tabs`
widget in the default configuration would not show any indication of the selected tab.
### `Table::new()` now requires specifying the widths of the columns ([#664])
[#664]: https://github.com/ratatui-org/ratatui/pull/664
Previously `Table`s could be constructed without widths. In almost all cases this is an error.
A new widths parameter is now mandatory on `Table::new()`. Existing code of the form:
```diff
- Table::new(rows).widths(widths)
```
Should be updated to:
```diff
+ Table::new(rows, widths)
```
For ease of automated replacement in cases where the amount of code broken by this change is large
or complex, it may be convenient to replace `Table::new` with `Table::default().rows`.
```diff
- Table::new(rows).block(block).widths(widths);
// becomes
+ Table::default().rows(rows).widths(widths)
```
### `Table::widths()` now accepts `IntoIterator<Item = AsRef<Constraint>>` ([#663])
[#663]: https://github.com/ratatui-org/ratatui/pull/663
Previously `Table::widths()` took a slice (`&'a [Constraint]`). This change will introduce clippy
`needless_borrow` warnings for places where slices are passed to this method. To fix these, remove
the `&`.
E.g.
```diff
- let table = Table::new(rows).widths(&[Constraint::Length(1)]);
// becomes
+ let table = Table::new(rows, [Constraint::Length(1)]);
```
### Layout::new() now accepts direction and constraint parameters ([#557])
[#557]: https://github.com/ratatui-org/ratatui/pull/557
Previously layout new took no parameters. Existing code should either use `Layout::default()` or
the new constructor.
```rust
let layout = layout::new()
.direction(Direction::Vertical)
.constraints([Constraint::Min(1), Constraint::Max(2)]);
// becomes either
let layout = layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(1), Constraint::Max(2)]);
// or
let layout = layout::new(Direction::Vertical, [Constraint::Min(1), Constraint::Max(2)]);
```
## [v0.24.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.24.0)
### ScrollbarState field type changed from `u16` to `usize` ([#456])
@@ -48,10 +199,10 @@ Applications can now set custom borders on a `Block` by calling `border_set()`.
`BorderType::line_symbols()` is renamed to `border_symbols()` and now returns a new struct
`symbols::border::Set`. E.g.:
```rust
let line_set: symbols::line::Set = BorderType::line_symbols(BorderType::Plain);
```diff
- let line_set: symbols::line::Set = BorderType::line_symbols(BorderType::Plain);
// becomes
let border_set: symbols::border::Set = BorderType::border_symbols(BorderType::Plain);
+ let border_set: symbols::border::Set = BorderType::border_symbols(BorderType::Plain);
```
### Generic `Backend` parameter removed from `Frame` ([#530])
@@ -62,10 +213,10 @@ let border_set: symbols::border::Set = BorderType::border_symbols(BorderType::Pl
accept `Frame`. To migrate existing code, remove any generic parameters from code that uses an
instance of a Frame. E.g.:
```rust
fn ui<B: Backend>(frame: &mut Frame<B>) { ... }
```diff
- fn ui<B: Backend>(frame: &mut Frame<B>) { ... }
// becomes
fn ui(frame: Frame) { ... }
+ fn ui(frame: Frame) { ... }
```
### `Stylize` shorthands now consume rather than borrow `String` ([#466])
@@ -77,13 +228,13 @@ new implementation of `Stylize` was added that returns a `Span<'static>`. This c
be consumed rather than borrowed. Existing code that expects to use the string after a call will no
longer compile. E.g.
```rust
let s = String::new("foo");
let span1 = s.red();
let span2 = s.blue(); // will no longer compile as s is consumed by the previous line
```diff
- let s = String::new("foo");
- let span1 = s.red();
- let span2 = s.blue(); // will no longer compile as s is consumed by the previous line
// becomes
let span1 = s.clone().red();
let span2 = s.blue();
+ let span1 = s.clone().red();
+ let span2 = s.blue();
```
### Deprecated `Spans` type removed (replaced with `Line`) ([#426])
@@ -93,12 +244,12 @@ let span2 = s.blue();
`Spans` was replaced with `Line` in 0.21.0. `Buffer::set_spans` was replaced with
`Buffer::set_line`.
```rust
let spans = Spans::from(some_string_str_span_or_vec_span);
buffer.set_spans(0, 0, spans, 10);
```diff
- let spans = Spans::from(some_string_str_span_or_vec_span);
- buffer.set_spans(0, 0, spans, 10);
// becomes
let line - Line::from(some_string_str_span_or_vec_span);
buffer.set_line(0, 0, line, 10);
+ let line - Line::from(some_string_str_span_or_vec_span);
+ buffer.set_line(0, 0, line, 10);
```
## [v0.23.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.23.0)
@@ -109,10 +260,10 @@ buffer.set_line(0, 0, line, 10);
The track symbol of `Scrollbar` is now optional, this method now takes an optional value.
```rust
let scrollbar = Scrollbar::default().track_symbol("|");
```diff
- let scrollbar = Scrollbar::default().track_symbol("|");
// becomes
let scrollbar = Scrollbar::default().track_symbol(Some("|"));
+ let scrollbar = Scrollbar::default().track_symbol(Some("|"));
```
### `Scrollbar` symbols moved to `symbols::scrollbar` and `widgets::scrollbar` module is private ([#330])
@@ -123,10 +274,10 @@ The symbols for defining scrollbars have been moved to the `symbols` module from
`widgets::scrollbar` module which is no longer public. To update your code update any imports to the
new module locations. E.g.:
```rust
use ratatui::{widgets::scrollbar::{Scrollbar, Set}};
```diff
- use ratatui::{widgets::scrollbar::{Scrollbar, Set}};
// becomes
use ratatui::{widgets::Scrollbar, symbols::scrollbar::Set}
+ use ratatui::{widgets::Scrollbar, symbols::scrollbar::Set}
```
### MSRV updated to 1.67 ([#361])
@@ -160,13 +311,13 @@ The minimum supported rust version is now 1.65.0.
In order to support inline viewports, the unstable method `Terminal::with_options()` was stabilized
and `ViewPort` was changed from a struct to an enum.
```rust
```diff
let terminal = Terminal::with_options(backend, TerminalOptions {
viewport: Viewport::fixed(area),
- viewport: Viewport::fixed(area),
});
// becomes
let terminal = Terminal::with_options(backend, TerminalOptions {
viewport: Viewport::Fixed(area),
+ viewport: Viewport::Fixed(area),
});
```
@@ -178,10 +329,10 @@ A new type `Masked` was introduced that implements `From<Text<'a>>`. This causes
previously did not need to use type annotations to fail to compile. To fix this, annotate or call
to_string() / to_owned() / as_str() on the value. E.g.:
```rust
let paragraph = Paragraph::new("".as_ref());
```diff
- let paragraph = Paragraph::new("".as_ref());
// becomes
let paragraph = Paragraph::new("".as_str());
+ let paragraph = Paragraph::new("".as_str());
```
### `Marker::Block` now renders as a block rather than a bar character ([#133])
@@ -192,10 +343,10 @@ Code using the `Block` marker that previously rendered using a half block charac
renders using the full block character (`'█'`). A new marker variant`Bar` is introduced to replace
the existing code.
```rust
let canvas = Canvas::default().marker(Marker::Block);
```diff
- let canvas = Canvas::default().marker(Marker::Block);
// becomes
let canvas = Canvas::default().marker(Marker::Bar);
+ let canvas = Canvas::default().marker(Marker::Bar);
```
## [v0.20.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.20.0)

View File

@@ -2,6 +2,491 @@
All notable changes to this project will be documented in this file.
## [0.25.0](https://github.com/ratatui-org/ratatui/releases/tag/0.25.0) - 2023-12-18
We are thrilled to announce the new version of `ratatui` - a Rust library that's all about cooking up TUIs 🐭
In this version, we made improvements on widgets such as List, Table and Layout and changed some of the defaults for a better user experience.
Also, we renewed our website and updated our documentation/tutorials to get started with `ratatui`: <https://ratatui.rs> 🚀
**Release highlights**: <https://ratatui.rs/highlights/v025/>
⚠️ List of breaking changes can be found [here](https://github.com/ratatui-org/ratatui/blob/main/BREAKING-CHANGES.md).
💖 We also enabled GitHub Sponsors for our organization, consider sponsoring us if you like `ratatui`: <https://github.com/sponsors/ratatui-org>
### Features
- [aef4956](https://github.com/ratatui-org/ratatui/commit/aef495604c52e563fbacfb1a6e730cd441a99129)
*(list)* `List::new` now accepts `IntoIterator<Item = Into<ListItem>>` ([#672](https://github.com/ratatui-org/ratatui/issues/672)) [**breaking**]
````text
This allows to build list like
```
List::new(["Item 1", "Item 2"])
```
````
- [8bfd666](https://github.com/ratatui-org/ratatui/commit/8bfd6661e251b6943f74bda626e4708b2e9f4b51)
*(paragraph)* Add `line_count` and `line_width` unstable helper methods
````text
This is an unstable feature that may be removed in the future
````
- [1229b96](https://github.com/ratatui-org/ratatui/commit/1229b96e428df880a951ef57f53ca73e74ef1ea2)
*(rect)* Add `offset` method ([#533](https://github.com/ratatui-org/ratatui/issues/533))
````text
The offset method creates a new Rect that is moved by the amount
specified in the x and y direction. These values can be positive or
negative. This is useful for manual layout tasks.
```rust
let rect = area.offset(Offset { x: 10, y -10 });
```
````
- [edacaf7](https://github.com/ratatui-org/ratatui/commit/edacaf7ff4e4b14702f6361af5a6da713b7dc564)
*(buffer)* Deprecate `Cell::symbol` field ([#624](https://github.com/ratatui-org/ratatui/issues/624))
````text
The Cell::symbol field is now accessible via a getter method (`symbol()`). This will
allow us to make future changes to the Cell internals such as replacing `String` with
`compact_str`.
````
- [6b2efd0](https://github.com/ratatui-org/ratatui/commit/6b2efd0f6c3bf56dc06bbf042db40c0c66de577e)
*(layout)* Accept IntoIterator for constraints ([#663](https://github.com/ratatui-org/ratatui/issues/663))
````text
Layout and Table now accept IntoIterator for constraints with an Item
that is AsRef<Constraint>. This allows pretty much any collection of
constraints to be passed to the layout functions including arrays,
vectors, slices, and iterators (without having to call collect() on
them).
````
- [753e246](https://github.com/ratatui-org/ratatui/commit/753e246531e1e9e2ea558911f8d03e738901d85f)
*(layout)* Allow configuring layout fill ([#633](https://github.com/ratatui-org/ratatui/issues/633))
````text
The layout split will generally fill the remaining area when `split()`
is called. This change allows the caller to configure how any extra
space is allocated to the `Rect`s. This is useful for cases where the
caller wants to have a fixed size for one of the `Rect`s, and have the
other `Rect`s fill the remaining space.
For now, the method and enum are marked as unstable because the exact
name is still being bikeshedded. To enable this functionality, add the
`unstable-segment-size` feature flag in your `Cargo.toml`.
To configure the layout to fill the remaining space evenly, use
`Layout::segment_size(SegmentSize::EvenDistribution)`. The default
behavior is `SegmentSize::LastTakesRemainder`, which gives the last
segment the remaining space. `SegmentSize::None` will disable this
behavior. See the docs for `Layout::segment_size()` and
`layout::SegmentSize` for more information.
Fixes https://github.com/ratatui-org/ratatui/issues/536
````
- [1e2f0be](https://github.com/ratatui-org/ratatui/commit/1e2f0be75ac3fb3d6500c1de291bd49972b808e4)
*(layout)* Add parameters to Layout::new() ([#557](https://github.com/ratatui-org/ratatui/issues/557)) [**breaking**]
````text
Adds a convenience function to create a layout with a direction and a
list of constraints which are the most common parameters that would be
generally configured using the builder pattern. The constraints can be
passed in as any iterator of constraints.
```rust
let layout = Layout::new(Direction::Horizontal, [
Constraint::Percentage(50),
Constraint::Percentage(50),
]);
```
````
- [c862aa5](https://github.com/ratatui-org/ratatui/commit/c862aa5e9ef4dbf494b5151214ac87f5c71e76d4)
*(list)* Support line alignment ([#599](https://github.com/ratatui-org/ratatui/issues/599))
````text
The `List` widget now respects the alignment of `Line`s and renders them as expected.
````
- [4424637](https://github.com/ratatui-org/ratatui/commit/4424637af252dc2f227fe4956eac71135e60fb02)
*(span)* Add setters for content and style ([#647](https://github.com/ratatui-org/ratatui/issues/647))
- [ebf1f42](https://github.com/ratatui-org/ratatui/commit/ebf1f4294211d478b8633a06576ec269a50db588)
*(style)* Implement `From` trait for crossterm to `Style` related structs ([#686](https://github.com/ratatui-org/ratatui/issues/686))
- [e49385b](https://github.com/ratatui-org/ratatui/commit/e49385b78c8e01fe6381b19d15137346bc6eb8a1)
*(table)* Add a Table::segment_size method ([#660](https://github.com/ratatui-org/ratatui/issues/660))
````text
It controls how to distribute extra space to an underconstrained table.
The default, legacy behavior is to leave the extra space unused. The
new options are LastTakesRemainder which gets all space to the rightmost
column that can used it, and EvenDistribution which divides it amongst
all columns.
````
- [b8f71c0](https://github.com/ratatui-org/ratatui/commit/b8f71c0d6eda3da272d29c7a9b3c47181049f76a)
*(widgets/chart)* Add option to set the position of legend ([#378](https://github.com/ratatui-org/ratatui/issues/378))
- [5bf4f52](https://github.com/ratatui-org/ratatui/commit/5bf4f52119ab3e0e3a266af196058179dc1d18c3)
*(uncategorized)* Implement `From` trait for termion to `Style` related structs ([#692](https://github.com/ratatui-org/ratatui/issues/692))
````text
* feat(termion): implement from termion color
* feat(termion): implement from termion style
* feat(termion): implement from termion `Bg` and `Fg`
````
- [d19b266](https://github.com/ratatui-org/ratatui/commit/d19b266e0eabdb0fb00660439a1818239c94024b)
*(uncategorized)* Add Constraint helpers (e.g. from_lengths) ([#641](https://github.com/ratatui-org/ratatui/issues/641))
````text
Adds helper methods that convert from iterators of u16 values to the
specific Constraint type. This makes it easy to create constraints like:
```rust
// a fixed layout
let constraints = Constraint::from_lengths([10, 20, 10]);
// a centered layout
let constraints = Constraint::from_ratios([(1, 4), (1, 2), (1, 4)]);
let constraints = Constraint::from_percentages([25, 50, 25]);
// a centered layout with a minimum size
let constraints = Constraint::from_mins([0, 100, 0]);
// a sidebar / main layout with maximum sizes
let constraints = Constraint::from_maxes([30, 200]);
```
````
### Bug Fixes
- [f69d57c](https://github.com/ratatui-org/ratatui/commit/f69d57c3b59e27b517a5ca1a002af808fee47970)
*(rect)* Fix underflow in the `Rect::intersection` method ([#678](https://github.com/ratatui-org/ratatui/issues/678))
- [56fc410](https://github.com/ratatui-org/ratatui/commit/56fc4101056e0f631f563f8f2c07646063e650d3)
*(block)* Make `inner` aware of title positions ([#657](https://github.com/ratatui-org/ratatui/issues/657))
````text
Previously, when computing the inner rendering area of a block, all
titles were assumed to be positioned at the top, which caused the
height of the inner area to be miscalculated.
````
- [ec7b387](https://github.com/ratatui-org/ratatui/commit/ec7b3872b46c6828c88ce7f72308dc67731fca25)
*(doc)* Do not access deprecated `Cell::symbol` field in doc example ([#626](https://github.com/ratatui-org/ratatui/issues/626))
- [37c70db](https://github.com/ratatui-org/ratatui/commit/37c70dbb8e19c0fb35ced16b29751933514a441e)
*(table)* Add widths parameter to new() ([#664](https://github.com/ratatui-org/ratatui/issues/664)) [**breaking**]
````text
This prevents creating a table that doesn't actually render anything.
````
- [1f88da7](https://github.com/ratatui-org/ratatui/commit/1f88da75383f6de76e64e9258fbf38d02ec77af9)
*(table)* Fix new clippy lint which triggers on table widths tests ([#630](https://github.com/ratatui-org/ratatui/issues/630))
````text
* fix(table): new clippy lint in 1.74.0 triggers on table widths tests
````
- [36d8c53](https://github.com/ratatui-org/ratatui/commit/36d8c5364590a559913c40ee5f021b5d8e3466e6)
*(table)* Widths() now accepts AsRef<[Constraint]> ([#628](https://github.com/ratatui-org/ratatui/issues/628))
````text
This allows passing an array, slice or Vec of constraints, which is more
ergonomic than requiring this to always be a slice.
The following calls now all succeed:
```rust
Table::new(rows).widths([Constraint::Length(5), Constraint::Length(5)]);
Table::new(rows).widths(&[Constraint::Length(5), Constraint::Length(5)]);
// widths could also be computed at runtime
let widths = vec![Constraint::Length(5), Constraint::Length(5)];
Table::new(rows).widths(widths.clone());
Table::new(rows).widths(&widths);
```
````
- [34d099c](https://github.com/ratatui-org/ratatui/commit/34d099c99af27eacfdde71f9ced255c29e1e001a)
*(tabs)* Fixup tests broken by semantic merge conflict ([#665](https://github.com/ratatui-org/ratatui/issues/665))
````text
Two changes without any line overlap caused the tabs tests to break
````
- [e4579f0](https://github.com/ratatui-org/ratatui/commit/e4579f0db2b70b59590cae02e994e3736b19a1b3)
*(tabs)* Set the default highlight_style ([#635](https://github.com/ratatui-org/ratatui/issues/635)) [**breaking**]
````text
Previously the default highlight_style was set to `Style::default()`,
which meant that the highlight style was the same as the normal style.
This change sets the default highlight_style to reversed text.
````
- [28ac55b](https://github.com/ratatui-org/ratatui/commit/28ac55bc62e4e14e3ace300633d56791a1d3dea0)
*(tabs)* Tab widget now supports custom padding ([#629](https://github.com/ratatui-org/ratatui/issues/629))
````text
The Tab widget now contains padding_left and and padding_right
properties. Those values can be set with functions `padding_left()`,
`padding_right()`, and `padding()` which all accept `Into<Line>`.
Fixes issue https://github.com/ratatui-org/ratatui/issues/502
````
- [df0eb1f](https://github.com/ratatui-org/ratatui/commit/df0eb1f8e94752db542ff58e1453f4f8beab17e2)
*(terminal)* Insert_before() now accepts lines > terminal height and doesn't add an extra blank line ([#596](https://github.com/ratatui-org/ratatui/issues/596))
````text
Fixes issue with inserting content with height>viewport_area.height and adds
the ability to insert content of height>terminal_height
- Adds TestBackend::append_lines() and TestBackend::clear_region() methods to
support testing the changes
````
- [aaeba27](https://github.com/ratatui-org/ratatui/commit/aaeba2709c09b7373f3781ecd4b0a96b22fc2764)
*(uncategorized)* Truncate table when overflow ([#685](https://github.com/ratatui-org/ratatui/issues/685))
````text
This prevents a panic when rendering an empty right aligned and rightmost table cell
````
- [ffa78aa](https://github.com/ratatui-org/ratatui/commit/ffa78aa67ccd79b9aa1af0d7ccf56a2059d0f519)
*(uncategorized)* Add #[must_use] to Style-moving methods ([#600](https://github.com/ratatui-org/ratatui/issues/600))
- [a2f2bd5](https://github.com/ratatui-org/ratatui/commit/a2f2bd5df53a796c0f2a57bb1b22151e52b5ef03)
*(uncategorized)* MSRV is now `1.70.0` ([#593](https://github.com/ratatui-org/ratatui/issues/593))
### Refactor
- [f767ea7](https://github.com/ratatui-org/ratatui/commit/f767ea7d3766887cb79145103b5aa92e0eabf8f6)
*(list)* `start_corner` is now `direction` ([#673](https://github.com/ratatui-org/ratatui/issues/673))
````text
The previous name `start_corner` did not communicate clearly the intent of the method.
A new method `direction` and a new enum `ListDirection` were added.
`start_corner` is now deprecated
````
- [b82451f](https://github.com/ratatui-org/ratatui/commit/b82451fb33f35ae0323a56bb6f962404b076a262)
*(examples)* Add vim binding ([#688](https://github.com/ratatui-org/ratatui/issues/688))
- [0576a8a](https://github.com/ratatui-org/ratatui/commit/0576a8aa3212c57d288c67592337a3870ae6dafc)
*(layout)* To natural reading order ([#681](https://github.com/ratatui-org/ratatui/issues/681))
````text
Structs and enums at the top of the file helps show the interaction
between the types without having to find each type in between longer
impl sections.
Also moved the try_split function into the Layout impl as an associated
function and inlined the `layout::split()` which just called try_split.
This makes the code a bit more contained.
````
- [4be18ab](https://github.com/ratatui-org/ratatui/commit/4be18aba8b535165f03d15450276b2e95a7970eb)
*(readme)* Reference awesome-ratatui instead of wiki ([#689](https://github.com/ratatui-org/ratatui/issues/689))
````text
* refactor(readme): link awesome-ratatui instead of wiki
The apps wiki moved to awesome-ratatui
* docs(readme): Update README.md
````
- [7ef0afc](https://github.com/ratatui-org/ratatui/commit/7ef0afcb62198f76321e84d9bb19a8a590a3b649)
*(widgets)* Remove unnecessary dynamic dispatch and heap allocation ([#597](https://github.com/ratatui-org/ratatui/issues/597))
- [b282a06](https://github.com/ratatui-org/ratatui/commit/b282a0693289d9d2602b54b639d3701d8c8cc8a8)
*(uncategorized)* Remove items deprecated since 0.10 ([#691](https://github.com/ratatui-org/ratatui/issues/691)) [**breaking**]
````text
Remove `Axis::title_style` and `Buffer::set_background` which are deprecated since 0.10
````
- [7ced7c0](https://github.com/ratatui-org/ratatui/commit/7ced7c0aa3acdaa63ed6add59711614993210ba3)
*(uncategorized)* Define struct WrappedLine instead of anonymous tuple ([#608](https://github.com/ratatui-org/ratatui/issues/608))
````text
It makes the type easier to document, and more obvious for users
````
### Documentation
- [fe632d7](https://github.com/ratatui-org/ratatui/commit/fe632d70cb150264d9af2f79145a1d14a3637f3e)
*(sparkline)* Add documentation ([#648](https://github.com/ratatui-org/ratatui/issues/648))
- [f4c8de0](https://github.com/ratatui-org/ratatui/commit/f4c8de041d48cec5ea9b3e1f540f57af5a09d7a4)
*(chart)* Document chart module ([#696](https://github.com/ratatui-org/ratatui/issues/696))
- [1b8b626](https://github.com/ratatui-org/ratatui/commit/1b8b6261e2de29a37b2cd7d6ee8659fb46d3beff)
*(examples)* Add animation and FPS counter to colors_rgb ([#583](https://github.com/ratatui-org/ratatui/issues/583))
- [2169a0d](https://github.com/ratatui-org/ratatui/commit/2169a0da01e3bd6272e33b9de26a033fcb5f55f2)
*(examples)* Add example of half block rendering ([#687](https://github.com/ratatui-org/ratatui/issues/687))
````text
This is a fun example of how to render big text using half blocks
````
- [41c44a4](https://github.com/ratatui-org/ratatui/commit/41c44a4af66ba791959f3a298d1b544330b9a164)
*(frame)* Add docs about resize events ([#697](https://github.com/ratatui-org/ratatui/issues/697))
- [91c67eb](https://github.com/ratatui-org/ratatui/commit/91c67eb1009449e0dfdd29e6ef0132c5254cfbde)
*(github)* Update code owners ([#666](https://github.com/ratatui-org/ratatui/issues/666))
````text
onboard @Valentin271 as maintainer
````
- [458fa90](https://github.com/ratatui-org/ratatui/commit/458fa9036281e0e6e88bd2ec90c633e499ce547c)
*(lib)* Tweak the crate documentation ([#659](https://github.com/ratatui-org/ratatui/issues/659))
- [3ec4e24](https://github.com/ratatui-org/ratatui/commit/3ec4e24d00e118a12c8fea888e16ce19b75cf45f)
*(list)* Add documentation to the List widget ([#669](https://github.com/ratatui-org/ratatui/issues/669))
````text
Adds documentation to the List widget and all its sub components like `ListState` and `ListItem`
````
- [9f37100](https://github.com/ratatui-org/ratatui/commit/9f371000968044e09545d66068c4ed4ea4b35d8a)
*(readme)* Update README.md and fix the bug that demo2 cannot run ([#595](https://github.com/ratatui-org/ratatui/issues/595))
````text
Fixes https://github.com/ratatui-org/ratatui/issues/594
````
- [2a87251](https://github.com/ratatui-org/ratatui/commit/2a87251152432fd99c18864f32874fed2cab2f99)
*(security)* Add security policy ([#676](https://github.com/ratatui-org/ratatui/issues/676))
````text
* docs: Create SECURITY.md
* Update SECURITY.md
````
- [987f7ee](https://github.com/ratatui-org/ratatui/commit/987f7eed4c8bd09e319b504e587eb1f3667ee64b)
*(website)* Rename book to website ([#661](https://github.com/ratatui-org/ratatui/issues/661))
- [a15c3b2](https://github.com/ratatui-org/ratatui/commit/a15c3b2660bf4102bc881a5bc11959bc136f4a17)
*(uncategorized)* Remove deprecated table constructor from breaking changes ([#698](https://github.com/ratatui-org/ratatui/issues/698))
- [113b4b7](https://github.com/ratatui-org/ratatui/commit/113b4b7a4ea841fe2ca7b1c153243fec781c3cc0)
*(uncategorized)* Rename template links to remove ratatui from name 📚 ([#690](https://github.com/ratatui-org/ratatui/issues/690))
- [211160c](https://github.com/ratatui-org/ratatui/commit/211160ca165e2ad23b3d4cd9382c6e4869644a9c)
*(uncategorized)* Remove simple-tui-rs ([#651](https://github.com/ratatui-org/ratatui/issues/651))
````text
This has not been recently and doesn't lead to good code
````
### Styling
- [6a6e9dd](https://github.com/ratatui-org/ratatui/commit/6a6e9dde9dc66ecb6f47f858fd0a67d7dc9eb7d1)
*(tabs)* Fix doc formatting ([#662](https://github.com/ratatui-org/ratatui/issues/662))
### Miscellaneous Tasks
- [910ad00](https://github.com/ratatui-org/ratatui/commit/910ad00059c3603ba6b1751c95783f974fde88a1)
*(rustfmt)* Enable format_code_in_doc_comments ([#695](https://github.com/ratatui-org/ratatui/issues/695))
````text
This enables more consistently formatted code in doc comments,
especially since ratatui heavily uses fluent setters.
See https://rust-lang.github.io/rustfmt/?version=v1.6.0#format_code_in_doc_comments
````
- [d118565](https://github.com/ratatui-org/ratatui/commit/d118565ef60480fba8f2906ede81f875a562cb61)
*(table)* Cleanup docs and builder methods ([#638](https://github.com/ratatui-org/ratatui/issues/638))
````text
- Refactor the `table` module for better top to bottom readability by
putting types first and arranging them in a logical order (Table, Row,
Cell, other).
- Adds new methods for:
- `Table::rows`
- `Row::cells`
- `Cell::new`
- `Cell::content`
- `TableState::new`
- `TableState::selected_mut`
- Makes `HighlightSpacing::should_add` pub(crate) since it's an internal
detail.
- Adds tests for all the new methods and simple property tests for all
the other setter methods.
````
- [dd22e72](https://github.com/ratatui-org/ratatui/commit/dd22e721e3aed24538eb08e46e40339cec636bcb)
*(uncategorized)* Correct "builder methods" in docs and add `must_use` on widgets setters ([#655](https://github.com/ratatui-org/ratatui/issues/655))
- [18e19f6](https://github.com/ratatui-org/ratatui/commit/18e19f6ce6ae3ce9bd52110ab6cbd4ed4bcca5e6)
*(uncategorized)* Fix breaking changes doc versions ([#639](https://github.com/ratatui-org/ratatui/issues/639))
````text
Moves the layout::new change to unreleasedd section and adds the table change
````
- [a58cce2](https://github.com/ratatui-org/ratatui/commit/a58cce2dba404fe394bbb298645bf3c40518fe1f)
*(uncategorized)* Disable default benchmarking ([#598](https://github.com/ratatui-org/ratatui/issues/598))
````text
Disables the default benchmarking behaviour for the lib target to fix unrecognized
criterion benchmark arguments.
See https://bheisler.github.io/criterion.rs/book/faq.html#cargo-bench-gives-unrecognized-option-errors-for-valid-command-line-options for details
````
### Continuous Integration
- [59b9c32](https://github.com/ratatui-org/ratatui/commit/59b9c32fbc2bc6725bdec42e63216024fab71493)
*(codecov)* Adjust threshold and noise settings ([#615](https://github.com/ratatui-org/ratatui/issues/615))
````text
Fixes https://github.com/ratatui-org/ratatui/issues/612
````
- [03401cd](https://github.com/ratatui-org/ratatui/commit/03401cd46e6566af4d063bac11efc30f28b5358a)
*(uncategorized)* Fix untrusted input in pr check workflow ([#680](https://github.com/ratatui-org/ratatui/issues/680))
### Contributors
Thank you so much to everyone that contributed to this release!
Here is the list of contributors who have contributed to `ratatui` for the first time!
* @rikonaka
* @danny-burrows
* @SOF3
* @jan-ferdinand
* @rhaskia
* @asomers
* @progval
* @TylerBloom
* @YeungKC
* @lyuha
## [0.24.0](https://github.com/ratatui-org/ratatui/releases/tag/0.24.0) - 2023-10-23
We are excited to announce the new version of `ratatui` - a Rust library that's all about cooking up TUIs 🐭
@@ -10,7 +495,7 @@ In this version, we've introduced features like window size API, enhanced chart
The list of \*breaking changes\* can be found [here](https://github.com/ratatui-org/ratatui/blob/main/BREAKING-CHANGES.md) ⚠️.
Also, we created various tutorials and walkthroughs in [Ratatui Book](https://github.com/ratatui-org/ratatui-book) which is available at <https://ratatui.rs> 🚀
**Release highlights**: <https://ratatui.rs/highlights/v0.24.html>
✨ **Release highlights**: <https://ratatui.rs/highlights/v024>
### Features

View File

@@ -1,6 +1,6 @@
[package]
name = "ratatui"
version = "0.24.0" # crate version
version = "0.25.0" # crate version
authors = ["Florian Dehau <work@fdehau.com>", "The Ratatui Developers"]
description = "A library that's all about cooking up terminal user interfaces"
documentation = "https://docs.rs/ratatui/latest/ratatui/"
@@ -18,7 +18,7 @@ exclude = [
]
autoexamples = true
edition = "2021"
rust-version = "1.67.0"
rust-version = "1.70.0"
[badges]
@@ -31,7 +31,7 @@ serde = { version = "1", optional = true, features = ["derive"] }
bitflags = "2.3"
cassowary = "0.3"
indoc = "2.0"
itertools = "0.11"
itertools = "0.12"
paste = "1.0.2"
strum = { version = "0.25", features = ["derive"] }
time = { version = "0.3.11", optional = true, features = ["local-offset"] }
@@ -39,6 +39,7 @@ unicode-segmentation = "1.10"
unicode-width = "0.1"
document-features = { version = "0.2.7", optional = true }
lru = "0.12.0"
stability = "0.1.1"
[dev-dependencies]
anyhow = "1.0.71"
@@ -47,11 +48,12 @@ better-panic = "0.3.0"
cargo-husky = { version = "1.5.0", default-features = false, features = [
"user-hooks",
] }
color-eyre = "0.6.2"
criterion = { version = "0.5.1", features = ["html_reports"] }
fakeit = "1.1"
rand = "0.8.5"
palette = "0.7.3"
pretty_assertions = "1.4.0"
rand = "0.8.5"
[features]
#! The crate provides a set of optional features that can be enabled in your `cargo.toml` file.
@@ -89,6 +91,21 @@ widget-calendar = ["dep:time"]
## enables the backend code that sets the underline color.
underline-color = ["dep:crossterm"]
#! The following features are unstable and may change in the future:
## Enable all unstable features.
unstable = ["unstable-segment-size", "unstable-rendered-line-info"]
## Enables the [`Layout::segment_size`](crate::layout::Layout::segment_size) method which is experimental and may change in the
## future. See [Issue #536](https://github.com/ratatui-org/ratatui/issues/536) for more details.
unstable-segment-size = []
## Enables the [`Paragraph::line_count`](crate::widgets::Paragraph::line_count)
## [`Paragraph::line_width`](crate::widgets::Paragraph::line_width) methods
## which are experimental and may change in the future.
## See [Issue 293](https://github.com/ratatui-org/ratatui/issues/293) for more details.
unstable-rendered-line-info = []
[package.metadata.docs.rs]
all-features = true
# see https://doc.rust-lang.org/nightly/rustdoc/scraped-examples.html
@@ -107,6 +124,9 @@ harness = false
name = "list"
harness = false
[lib]
bench = false
[[bench]]
name = "paragraph"
harness = false
@@ -213,6 +233,11 @@ name = "popup"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "ratatui-logo"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "scrollbar"
required-features = ["crossterm"]

View File

@@ -11,7 +11,7 @@ ALL_FEATURES = "all-widgets,macros,serde"
# sets of flags, one for Windows and one for other platforms.
# Windows: --features=all-widgets,macros,serde,crossterm,termwiz,underline-color
# Other: --features=all-widgets,macros,serde,crossterm,termion,termwiz,underline-color
ALL_FEATURES_FLAG = { source = "${CARGO_MAKE_RUST_TARGET_OS}", default_value = "--features=all-widgets,macros,serde,crossterm,termion,termwiz", mapping = { "windows" = "--features=all-widgets,macros,serde,crossterm,termwiz" } }
ALL_FEATURES_FLAG = { source = "${CARGO_MAKE_RUST_TARGET_OS}", default_value = "--features=all-widgets,macros,serde,crossterm,termion,termwiz,unstable", mapping = { "windows" = "--features=all-widgets,macros,serde,crossterm,termwiz,unstable" } }
[tasks.default]
alias = "ci"
@@ -90,12 +90,21 @@ args = [
"warnings",
]
[tasks.install-nextest]
description = "Install cargo-nextest"
install_crate = { crate_name = "cargo-nextest", binary = "cargo-nextest", test_arg = "--help" }
[tasks.test]
description = "Run tests"
dependencies = ["test-doc"]
run_task = { name = ["test-lib", "test-doc"] }
[tasks.test-lib]
description = "Run default tests"
dependencies = ["install-nextest"]
command = "cargo"
args = [
"test",
"nextest",
"run",
"--all-targets",
"--no-default-features",
"${ALL_FEATURES_FLAG}",
@@ -109,9 +118,11 @@ args = ["test", "--doc", "--no-default-features", "${ALL_FEATURES_FLAG}"]
[tasks.test-backend]
# takes a command line parameter to specify the backend to test (e.g. "crossterm")
description = "Run backend-specific tests"
dependencies = ["install-nextest"]
command = "cargo"
args = [
"test",
"nextest",
"run",
"--all-targets",
"--no-default-features",
"--features",

120
README.md
View File

@@ -25,27 +25,29 @@
<div align="center">
[![Crate Badge]](https://crates.io/crates/ratatui) [![License Badge]](./LICENSE) [![CI
Badge]](https://github.com/ratatui-org/ratatui/actions?query=workflow%3ACI+) [![Docs
Badge]](https://docs.rs/crate/ratatui/)<br>
[![Dependencies Badge]](https://deps.rs/repo/github/ratatui-org/ratatui) [![Codecov
Badge]](https://app.codecov.io/gh/ratatui-org/ratatui) [![Discord
Badge]](https://discord.gg/pMCEU9hNEj) [![Matrix
Badge]](https://matrix.to/#/#ratatui:matrix.org)<br>
[Documentation](https://docs.rs/ratatui) · [Ratatui Book](https://ratatui.rs) ·
[Examples](https://github.com/ratatui-org/ratatui/tree/main/examples) · [Report a
bug](https://github.com/ratatui-org/ratatui/issues/new?labels=bug&projects=&template=bug_report.md)
· [Request a
Feature](https://github.com/ratatui-org/ratatui/issues/new?labels=enhancement&projects=&template=feature_request.md)
[![Crate Badge]](https://crates.io/crates/ratatui)
[![License Badge]](./LICENSE)
[![CI Badge]](https://github.com/ratatui-org/ratatui/actions?query=workflow%3ACI+)
[![Docs Badge]](https://docs.rs/crate/ratatui/)<br>
[![Dependencies Badge]](https://deps.rs/repo/github/ratatui-org/ratatui)
[![Codecov Badge]](https://app.codecov.io/gh/ratatui-org/ratatui)
[![Discord Badge]](https://discord.gg/pMCEU9hNEj)
[![Matrix Badge]](https://matrix.to/#/#ratatui:matrix.org)<br>
[Documentation](https://docs.rs/ratatui)
· [Ratatui Website](https://ratatui.rs)
· [Examples](https://github.com/ratatui-org/ratatui/tree/main/examples)
· [Report a bug](https://github.com/ratatui-org/ratatui/issues/new?labels=bug&projects=&template=bug_report.md)
· [Request a Feature](https://github.com/ratatui-org/ratatui/issues/new?labels=enhancement&projects=&template=feature_request.md)
· [Send a Pull Request](https://github.com/ratatui-org/ratatui/compare)
</div>
# Ratatui
[Ratatui] is a crate for cooking up terminal user interfaces in rust. It is a lightweight
library that provides a set of widgets and utilities to build complex rust TUIs. Ratatui was
forked from the [Tui-rs crate] in 2023 in order to continue its development.
[Ratatui] is a crate for cooking up terminal user interfaces in Rust. It is a lightweight
library that provides a set of widgets and utilities to build complex Rust TUIs. Ratatui was
forked from the [tui-rs] crate in 2023 in order to continue its development.
## Installation
@@ -56,7 +58,7 @@ cargo add ratatui crossterm
```
Ratatui uses [Crossterm] by default as it works on most platforms. See the [Installation]
section of the [Ratatui Book] for more details on how to use other backends ([Termion] /
section of the [Ratatui Website] for more details on how to use other backends ([Termion] /
[Termwiz]).
## Introduction
@@ -64,12 +66,12 @@ section of the [Ratatui Book] for more details on how to use other backends ([Te
Ratatui is based on the principle of immediate rendering with intermediate buffers. This means
that for each frame, your app must render all widgets that are supposed to be part of the UI.
This is in contrast to the retained mode style of rendering where widgets are updated and then
automatically redrawn on the next frame. See the [Rendering] section of the [Ratatui Book] for
automatically redrawn on the next frame. See the [Rendering] section of the [Ratatui Website] for
more info.
## Other documentation
- [Ratatui Book] - explains the library's concepts and provides step-by-step tutorials
- [Ratatui Website] - explains the library's concepts and provides step-by-step tutorials
- [Examples] - a collection of examples that demonstrate how to use the library.
- [API Documentation] - the full API documentation for the library on docs.rs.
- [Changelog] - generated by [git-cliff] utilizing [Conventional Commits].
@@ -81,12 +83,11 @@ more info.
The following example demonstrates the minimal amount of code necessary to setup a terminal and
render "Hello World!". The full code for this example which contains a little more detail is in
[hello_world.rs]. For more guidance on different ways to structure your application see the
[Application Patterns] and [Hello World tutorial] sections in the [Ratatui Book] and the various
[Application Patterns] and [Hello World tutorial] sections in the [Ratatui Website] and the various
[Examples]. There are also several starter templates available:
- [rust-tui-template]
- [ratatui-async-template] (book and template)
- [simple-tui-rs]
- [template]
- [async-template] (book and template)
Every application built with `ratatui` needs to implement the following steps:
@@ -110,20 +111,20 @@ implements the [`Backend`] trait which has implementations for [Crossterm], [Ter
Most applications should enter the Alternate Screen when starting and leave it when exiting and
also enable raw mode to disable line buffering and enable reading key events. See the [`backend`
module] and the [Backends] section of the [Ratatui Book] for more info.
module] and the [Backends] section of the [Ratatui Website] for more info.
### Drawing the UI
The drawing logic is delegated to a closure that takes a [`Frame`] instance as argument. The
[`Frame`] provides the size of the area to draw to and allows the app to render any [`Widget`]
using the provided [`render_widget`] method. See the [Widgets] section of the [Ratatui Book] for
using the provided [`render_widget`] method. See the [Widgets] section of the [Ratatui Website] for
more info.
### Handling events
Ratatui does not include any input handling. Instead event handling can be implemented by
calling backend library methods directly. See the [Handling Events] section of the [Ratatui
Book] for more info. For example, if you are using [Crossterm], you can use the
Website] for more info. For example, if you are using [Crossterm], you can use the
[`crossterm::event`] module to handle events.
### Example
@@ -182,20 +183,21 @@ Running this example produces the following output:
The library comes with a basic yet useful layout management object called [`Layout`] which
allows you to split the available space into multiple areas and then render widgets in each
area. This lets you describe a responsive terminal UI by nesting layouts. See the [Layout]
section of the [Ratatui Book] for more info.
section of the [Ratatui Website] for more info.
```rust
use ratatui::{prelude::*, widgets::*};
fn ui(frame: &mut Frame) {
let main_layout = Layout::default()
.direction(Direction::Vertical)
.constraints([
let main_layout = Layout::new(
Direction::Vertical,
[
Constraint::Length(1),
Constraint::Min(0),
Constraint::Length(1),
])
.split(frame.size());
]
)
.split(frame.size());
frame.render_widget(
Block::new().borders(Borders::TOP).title("Title Bar"),
main_layout[0],
@@ -205,10 +207,11 @@ fn ui(frame: &mut Frame) {
main_layout[2],
);
let inner_layout = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(main_layout[1]);
let inner_layout = Layout::new(
Direction::Horizontal,
[Constraint::Percentage(50), Constraint::Percentage(50)]
)
.split(main_layout[1]);
frame.render_widget(
Block::default().borders(Borders::ALL).title("Left"),
inner_layout[0],
@@ -234,22 +237,23 @@ The [`style` module] provides types that represent the various styling options.
important one is [`Style`] which represents the foreground and background colors and the text
attributes of a [`Span`]. The [`style` module] also provides a [`Stylize`] trait that allows
short-hand syntax to apply a style to widgets and text. See the [Styling Text] section of the
[Ratatui Book] for more info.
[Ratatui Website] for more info.
```rust
use ratatui::{prelude::*, widgets::*};
fn ui(frame: &mut Frame) {
let areas = Layout::default()
.direction(Direction::Vertical)
.constraints([
let areas = Layout::new(
Direction::Vertical,
[
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
Constraint::Min(0),
])
.split(frame.size());
]
)
.split(frame.size());
let span1 = Span::raw("Hello ");
let span2 = Span::styled(
@@ -285,21 +289,20 @@ Running this example produces the following output:
![docsrs-styling]
[Ratatui Book]: https://ratatui.rs
[Installation]: https://ratatui.rs/installation.html
[Rendering]: https://ratatui.rs/concepts/rendering/index.html
[Application Patterns]: https://ratatui.rs/concepts/application_patterns/index.html
[Hello World tutorial]: https://ratatui.rs/tutorial/hello_world.html
[Backends]: https://ratatui.rs/concepts/backends/index.html
[Widgets]: https://ratatui.rs/how-to/widgets/index.html
[Handling Events]: https://ratatui.rs/concepts/event_handling.html
[Layout]: https://ratatui.rs/how-to/layout/index.html
[Styling Text]: https://ratatui.rs/how-to/render/style-text.html
[rust-tui-template]: https://github.com/ratatui-org/rust-tui-template
[ratatui-async-template]: https://ratatui-org.github.io/ratatui-async-template/
[simple-tui-rs]: https://github.com/pmsanford/simple-tui-rs
[Ratatui Website]: https://ratatui.rs/
[Installation]: https://ratatui.rs/installation/
[Rendering]: https://ratatui.rs/concepts/rendering/
[Application Patterns]: https://ratatui.rs/concepts/application-patterns/
[Hello World tutorial]: https://ratatui.rs/tutorials/hello-world/
[Backends]: https://ratatui.rs/concepts/backends/
[Widgets]: https://ratatui.rs/how-to/widgets/
[Handling Events]: https://ratatui.rs/concepts/event-handling/
[Layout]: https://ratatui.rs/how-to/layout/
[Styling Text]: https://ratatui.rs/how-to/render/style-text/
[template]: https://github.com/ratatui-org/template
[async-template]: https://ratatui-org.github.io/async-template
[Examples]: https://github.com/ratatui-org/ratatui/tree/main/examples
[git-cliff]: https://github.com/orhun/git-cliff
[git-cliff]: https://git-cliff.org
[Conventional Commits]: https://www.conventionalcommits.org
[API Documentation]: https://docs.rs/ratatui
[Changelog]: https://github.com/ratatui-org/ratatui/blob/main/CHANGELOG.md
@@ -325,7 +328,7 @@ Running this example produces the following output:
[Crossterm]: https://crates.io/crates/crossterm
[Termion]: https://crates.io/crates/termion
[Termwiz]: https://crates.io/crates/termwiz
[Tui-rs crate]: https://crates.io/crates/tui
[tui-rs]: https://crates.io/crates/tui
[hello_world.rs]: https://github.com/ratatui-org/ratatui/blob/main/examples/hello_world.rs
[Crate Badge]: https://img.shields.io/crates/v/ratatui?logo=rust&style=flat-square
[CI Badge]:
@@ -402,7 +405,6 @@ be installed with `cargo install cargo-make`).
`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
- [tui-builder](https://github.com/jkelleyrtp/tui-builder) — Batteries-included MVC framework for
Tui-rs + Crossterm apps
- [tui-clap](https://github.com/kegesch/tui-clap-rs) — Use clap-rs together with Tui-rs
@@ -425,8 +427,8 @@ be installed with `cargo install cargo-make`).
## Apps
Check out the list of more than 50 [Apps using
`Ratatui`](https://github.com/ratatui-org/ratatui/wiki/Apps-using-Ratatui)!
Check out [awesome-ratatui](https://github.com/ratatui-org/awesome-ratatui) for a curated list of
awesome apps/libraries built with `ratatui`!
## Alternatives

9
SECURITY.md Normal file
View File

@@ -0,0 +1,9 @@
# Security Policy
## Supported Versions
We only support the latest version of this crate.
## Reporting a Vulnerability
To report secuirity vulnerability, please use the form at https://github.com/ratatui-org/ratatui/security/advisories/new

View File

@@ -1,3 +1,14 @@
coverage: # https://docs.codecov.com/docs/codecovyml-reference#coverage
precision: 1 # e.g. 89.1%
round: down
range: 85..100 # https://docs.codecov.com/docs/coverage-configuration#section-range
status: # https://docs.codecov.com/docs/commit-status
project:
default:
threshold: 1% # Avoid false negatives
ignore:
- "examples"
- "benches"
comment: # https://docs.codecov.com/docs/pull-request-comments
# make the comments less noisy
require_changes: true

View File

@@ -9,7 +9,7 @@ images themselves are stored in a separate git branch to avoid bloating the main
This is the demo example from the main README and crate page. Source: [demo2](./demo2/).
```shell
cargo run --example=demo2 --features=crossterm
cargo run --example=demo2 --features="crossterm widget-calendar"
```
![Demo2][demo2.gif]
@@ -117,7 +117,10 @@ two square-ish pixels in the space of a single rectangular terminal cell.
cargo run --example=colors_rgb --features=crossterm
```
![Colors RGB][colors_rgb.png]
Note: VHs renders full screen animations poorly, so this is a screen capture rather than the output
of the VHS tape.
<https://github.com/ratatui-org/ratatui/assets/381361/485e775a-e0b5-4133-899b-1e8aeb56e774>
## Custom Widget
@@ -223,6 +226,18 @@ cargo run --example=popup --features=crossterm
![Popup][popup.gif]
## Ratatui-logo
A fun example of using half blocks to render graphics Source:
[ratatui-logo.rs](./ratatui-logo.rs).
>
```shell
cargo run --example=ratatui-logo --features=crossterm
```
![Ratatui Logo][ratatui-logo.gif]
## Scrollbar
Demonstrates the [`Scrollbar`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Scrollbar.html)
@@ -271,7 +286,8 @@ cargo run --example=tabs --features=crossterm
Demonstrates one approach to accepting user input. Source [user_input.rs](./user_input.rs).
> [!NOTE] Consider using [`tui-textarea`](https://crates.io/crates/tui-textarea) or
> [!NOTE]
> Consider using [`tui-textarea`](https://crates.io/crates/tui-textarea) or
> [`tui-input`](https://crates.io/crates/tui-input) crates for more functional text entry UIs.
```shell
@@ -296,7 +312,6 @@ examples/generate.bash
[canvas.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/canvas.gif?raw=true
[chart.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/chart.gif?raw=true
[colors.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/colors.gif?raw=true
[colors_rgb.png]: https://github.com/ratatui-org/ratatui/blob/images/examples/colors_rgb.png?raw=true
[custom_widget.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/custom_widget.gif?raw=true
[demo.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/demo.gif?raw=true
[demo2.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/demo2.gif?raw=true
@@ -309,6 +324,7 @@ examples/generate.bash
[panic.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/panic.gif?raw=true
[paragraph.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/paragraph.gif?raw=true
[popup.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/popup.gif?raw=true
[ratatui-logo.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/ratatui-logo.gif?raw=true
[scrollbar.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/scrollbar.gif?raw=true
[sparkline.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/sparkline.gif?raw=true
[table.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/table.gif?raw=true

View File

@@ -175,7 +175,7 @@ fn make_dates(current_year: i32) -> CalendarEventStore {
mod cals {
use super::*;
pub(super) fn get_cal<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
pub(super) fn get_cal<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
use Month::*;
match m {
May => example1(m, y, es),
@@ -188,7 +188,7 @@ mod cals {
}
}
fn default<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
fn default<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
let default_style = Style::default()
.add_modifier(Modifier::BOLD)
.bg(Color::Rgb(50, 50, 50));
@@ -198,7 +198,7 @@ mod cals {
.default_style(default_style)
}
fn example1<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
fn example1<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
let default_style = Style::default()
.add_modifier(Modifier::BOLD)
.bg(Color::Rgb(50, 50, 50));
@@ -209,7 +209,7 @@ mod cals {
.show_month_header(Style::default())
}
fn example2<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
fn example2<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
let header_style = Style::default()
.add_modifier(Modifier::BOLD)
.add_modifier(Modifier::DIM)
@@ -225,7 +225,7 @@ mod cals {
.show_month_header(Style::default())
}
fn example3<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
fn example3<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
let header_style = Style::default()
.add_modifier(Modifier::BOLD)
.fg(Color::Green);
@@ -241,7 +241,7 @@ mod cals {
.show_month_header(Style::default())
}
fn example4<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
fn example4<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
let header_style = Style::default()
.add_modifier(Modifier::BOLD)
.fg(Color::Green);
@@ -255,7 +255,7 @@ mod cals {
.default_style(default_style)
}
fn example5<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
fn example5<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
let header_style = Style::default()
.add_modifier(Modifier::BOLD)
.fg(Color::Green);

View File

@@ -59,10 +59,10 @@ impl App {
if let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Char('q') => break,
KeyCode::Down => app.y += 1.0,
KeyCode::Up => app.y -= 1.0,
KeyCode::Right => app.x += 1.0,
KeyCode::Left => app.x -= 1.0,
KeyCode::Down | KeyCode::Char('j') => app.y += 1.0,
KeyCode::Up | KeyCode::Char('k') => app.y -= 1.0,
KeyCode::Right | KeyCode::Char('l') => app.x += 1.0,
KeyCode::Left | KeyCode::Char('h') => app.x -= 1.0,
_ => {}
}
}

View File

@@ -9,18 +9,10 @@ use crossterm::{
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{prelude::*, widgets::*};
const DATA: [(f64, f64); 5] = [(0.0, 0.0), (1.0, 1.0), (2.0, 2.0), (3.0, 3.0), (4.0, 4.0)];
const DATA2: [(f64, f64); 7] = [
(0.0, 0.0),
(10.0, 1.0),
(20.0, 0.5),
(30.0, 1.5),
(40.0, 1.0),
(50.0, 2.5),
(60.0, 3.0),
];
use ratatui::{
prelude::*,
widgets::{block::Title, *},
};
#[derive(Clone)]
pub struct SinSignal {
@@ -142,14 +134,28 @@ fn run_app<B: Backend>(
fn ui(f: &mut Frame, app: &App) {
let size = f.size();
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
])
.split(size);
let vertical_chunks = Layout::new(
Direction::Vertical,
[Constraint::Percentage(40), Constraint::Percentage(60)],
)
.split(size);
// top chart
render_chart1(f, vertical_chunks[0], app);
let horizontal_chunks = Layout::new(
Direction::Horizontal,
[Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)],
)
.split(vertical_chunks[1]);
// bottom left
render_line_chart(f, horizontal_chunks[0]);
// bottom right
render_scatter(f, horizontal_chunks[1]);
}
fn render_chart1(f: &mut Frame, area: Rect, app: &App) {
let x_labels = vec![
Span::styled(
format!("{}", app.window[0]),
@@ -194,61 +200,164 @@ fn ui(f: &mut Frame, app: &App) {
.labels(vec!["-20".bold(), "0".into(), "20".bold()])
.bounds([-20.0, 20.0]),
);
f.render_widget(chart, chunks[0]);
let datasets = vec![Dataset::default()
.name("data")
.marker(symbols::Marker::Braille)
.style(Style::default().fg(Color::Yellow))
.graph_type(GraphType::Line)
.data(&DATA)];
let chart = Chart::new(datasets)
.block(
Block::default()
.title("Chart 2".cyan().bold())
.borders(Borders::ALL),
)
.x_axis(
Axis::default()
.title("X Axis")
.style(Style::default().fg(Color::Gray))
.bounds([0.0, 5.0])
.labels(vec!["0".bold(), "2.5".into(), "5.0".bold()]),
)
.y_axis(
Axis::default()
.title("Y Axis")
.style(Style::default().fg(Color::Gray))
.bounds([0.0, 5.0])
.labels(vec!["0".bold(), "2.5".into(), "5.0".bold()]),
);
f.render_widget(chart, chunks[1]);
let datasets = vec![Dataset::default()
.name("data")
.marker(symbols::Marker::Braille)
.style(Style::default().fg(Color::Yellow))
.graph_type(GraphType::Line)
.data(&DATA2)];
let chart = Chart::new(datasets)
.block(
Block::default()
.title("Chart 3".cyan().bold())
.borders(Borders::ALL),
)
.x_axis(
Axis::default()
.title("X Axis")
.style(Style::default().fg(Color::Gray))
.bounds([0.0, 50.0])
.labels(vec!["0".bold(), "25".into(), "50".bold()]),
)
.y_axis(
Axis::default()
.title("Y Axis")
.style(Style::default().fg(Color::Gray))
.bounds([0.0, 5.0])
.labels(vec!["0".bold(), "2.5".into(), "5".bold()]),
);
f.render_widget(chart, chunks[2]);
f.render_widget(chart, area);
}
fn render_line_chart(f: &mut Frame, area: Rect) {
let datasets = vec![Dataset::default()
.name("Line from only 2 points")
.marker(symbols::Marker::Braille)
.style(Style::default().fg(Color::Yellow))
.graph_type(GraphType::Line)
.data(&[(1., 1.), (4., 4.)])];
let chart = Chart::new(datasets)
.block(
Block::default()
.title(
Title::default()
.content("Line chart".cyan().bold())
.alignment(Alignment::Center),
)
.borders(Borders::ALL),
)
.x_axis(
Axis::default()
.title("X Axis")
.style(Style::default().gray())
.bounds([0.0, 5.0])
.labels(vec!["0".bold(), "2.5".into(), "5.0".bold()]),
)
.y_axis(
Axis::default()
.title("Y Axis")
.style(Style::default().gray())
.bounds([0.0, 5.0])
.labels(vec!["0".bold(), "2.5".into(), "5.0".bold()]),
)
.legend_position(Some(LegendPosition::TopLeft))
.hidden_legend_constraints((Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)));
f.render_widget(chart, area)
}
fn render_scatter(f: &mut Frame, area: Rect) {
let datasets = vec![
Dataset::default()
.name("Heavy")
.marker(Marker::Dot)
.graph_type(GraphType::Scatter)
.style(Style::new().yellow())
.data(&HEAVY_PAYLOAD_DATA),
Dataset::default()
.name("Medium")
.marker(Marker::Braille)
.graph_type(GraphType::Scatter)
.style(Style::new().magenta())
.data(&MEDIUM_PAYLOAD_DATA),
Dataset::default()
.name("Small")
.marker(Marker::Dot)
.graph_type(GraphType::Scatter)
.style(Style::new().cyan())
.data(&SMALL_PAYLOAD_DATA),
];
let chart = Chart::new(datasets)
.block(
Block::new().borders(Borders::all()).title(
Title::default()
.content("Scatter chart".cyan().bold())
.alignment(Alignment::Center),
),
)
.x_axis(
Axis::default()
.title("Year")
.bounds([1960., 2020.])
.style(Style::default().fg(Color::Gray))
.labels(vec!["1960".into(), "1990".into(), "2020".into()]),
)
.y_axis(
Axis::default()
.title("Cost")
.bounds([0., 75000.])
.style(Style::default().fg(Color::Gray))
.labels(vec!["0".into(), "37 500".into(), "75 000".into()]),
)
.hidden_legend_constraints((Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)));
f.render_widget(chart, area);
}
// Data from https://ourworldindata.org/space-exploration-satellites
const HEAVY_PAYLOAD_DATA: [(f64, f64); 9] = [
(1965., 8200.),
(1967., 5400.),
(1981., 65400.),
(1989., 30800.),
(1997., 10200.),
(2004., 11600.),
(2014., 4500.),
(2016., 7900.),
(2018., 1500.),
];
const MEDIUM_PAYLOAD_DATA: [(f64, f64); 29] = [
(1963., 29500.),
(1964., 30600.),
(1965., 177900.),
(1965., 21000.),
(1966., 17900.),
(1966., 8400.),
(1975., 17500.),
(1982., 8300.),
(1985., 5100.),
(1988., 18300.),
(1990., 38800.),
(1990., 9900.),
(1991., 18700.),
(1992., 9100.),
(1994., 10500.),
(1994., 8500.),
(1994., 8700.),
(1997., 6200.),
(1999., 18000.),
(1999., 7600.),
(1999., 8900.),
(1999., 9600.),
(2000., 16000.),
(2001., 10000.),
(2002., 10400.),
(2002., 8100.),
(2010., 2600.),
(2013., 13600.),
(2017., 8000.),
];
const SMALL_PAYLOAD_DATA: [(f64, f64); 23] = [
(1961., 118500.),
(1962., 14900.),
(1975., 21400.),
(1980., 32800.),
(1988., 31100.),
(1990., 41100.),
(1993., 23600.),
(1994., 20600.),
(1994., 34600.),
(1996., 50600.),
(1997., 19200.),
(1997., 45800.),
(1998., 19100.),
(2000., 73100.),
(2003., 11200.),
(2008., 12600.),
(2010., 30500.),
(2012., 20000.),
(2013., 10600.),
(2013., 34500.),
(2015., 10600.),
(2018., 23100.),
(2019., 17300.),
];

View File

@@ -2,63 +2,85 @@
///
/// Requires a terminal that supports 24-bit color (true color) and unicode.
use std::{
error::Error,
io::{stdout, Stdout},
rc::Rc,
time::Duration,
io::stdout,
time::{Duration, Instant},
};
use color_eyre::config::HookBuilder;
use crossterm::{
event::{self, Event, KeyCode, KeyEventKind},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
};
use palette::{
convert::{FromColorUnclamped, IntoColorUnclamped},
Okhsv, Srgb,
};
use palette::{convert::FromColorUnclamped, Okhsv, Srgb};
use ratatui::{prelude::*, widgets::*};
type Result<T> = std::result::Result<T, Box<dyn Error>>;
fn main() -> Result<()> {
install_panic_hook();
App::new()?.run()
fn main() -> color_eyre::Result<()> {
App::run()
}
#[derive(Debug, Default)]
struct App {
terminal: Terminal<CrosstermBackend<Stdout>>,
should_quit: bool,
// a 2d vec of the colors to render, calculated when the size changes as this is expensive
// to calculate every frame
colors: Vec<Vec<Color>>,
last_size: Rect,
fps: Fps,
frame_count: usize,
}
#[derive(Debug)]
struct Fps {
frame_count: usize,
last_instant: Instant,
fps: Option<f32>,
}
struct AppWidget<'a> {
title: Paragraph<'a>,
fps_widget: FpsWidget<'a>,
rgb_colors_widget: RgbColorsWidget<'a>,
}
struct FpsWidget<'a> {
fps: &'a Fps,
}
struct RgbColorsWidget<'a> {
/// The colors to render - should be double the height of the area
colors: &'a Vec<Vec<Color>>,
/// the number of elapsed frames that have passed - used to animate the colors
frame_count: usize,
}
impl App {
pub fn new() -> Result<Self> {
Ok(Self {
terminal: Terminal::new(CrosstermBackend::new(stdout()))?,
should_quit: false,
})
}
pub fn run() -> color_eyre::Result<()> {
install_panic_hook()?;
pub fn run(mut self) -> Result<()> {
init_terminal()?;
self.terminal.clear()?;
while !self.should_quit {
self.draw()?;
self.handle_events()?;
let mut terminal = init_terminal()?;
let mut app = Self::default();
while !app.should_quit {
app.tick();
terminal.draw(|frame| {
let size = frame.size();
app.setup_colors(size);
frame.render_widget(AppWidget::new(&app), size);
})?;
app.handle_events()?;
}
restore_terminal()?;
Ok(())
}
fn draw(&mut self) -> Result<()> {
self.terminal.draw(|frame| {
frame.render_widget(RgbColors, frame.size());
})?;
Ok(())
fn tick(&mut self) {
self.frame_count += 1;
self.fps.tick();
}
fn handle_events(&mut self) -> Result<()> {
if event::poll(Duration::from_millis(100))? {
fn handle_events(&mut self) -> color_eyre::Result<()> {
if event::poll(Duration::from_secs_f32(1.0 / 60.0))? {
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
self.should_quit = true;
@@ -67,80 +89,140 @@ impl App {
}
Ok(())
}
}
impl Drop for App {
fn drop(&mut self) {
let _ = restore_terminal();
fn setup_colors(&mut self, size: Rect) {
// only update the colors if the size has changed since the last time we rendered
if self.last_size.width == size.width && self.last_size.height == size.height {
return;
}
self.last_size = size;
let Rect { width, height, .. } = size;
// double the height because each screen row has two rows of half block pixels
let height = height * 2;
self.colors.clear();
for y in 0..height {
let mut row = Vec::new();
for x in 0..width {
let hue = x as f32 * 360.0 / width as f32;
let value = (height - y) as f32 / height as f32;
let saturation = Okhsv::max_saturation();
let color = Okhsv::new(hue, saturation, value);
let color = Srgb::<f32>::from_color_unclamped(color);
let color: Srgb<u8> = color.into_format();
let color = Color::Rgb(color.red, color.green, color.blue);
row.push(color);
}
self.colors.push(row);
}
}
}
struct RgbColors;
impl Fps {
fn tick(&mut self) {
self.frame_count += 1;
let elapsed = self.last_instant.elapsed();
// update the fps every second, but only if we've rendered at least 2 frames (to avoid
// noise in the fps calculation)
if elapsed > Duration::from_secs(1) && self.frame_count > 2 {
self.fps = Some(self.frame_count as f32 / elapsed.as_secs_f32());
self.frame_count = 0;
self.last_instant = Instant::now();
}
}
}
impl Widget for RgbColors {
impl Default for Fps {
fn default() -> Self {
Self {
frame_count: 0,
last_instant: Instant::now(),
fps: None,
}
}
}
impl<'a> AppWidget<'a> {
fn new(app: &'a App) -> Self {
let title =
Paragraph::new("colors_rgb example. Press q to quit").alignment(Alignment::Center);
Self {
title,
fps_widget: FpsWidget { fps: &app.fps },
rgb_colors_widget: RgbColorsWidget {
colors: &app.colors,
frame_count: app.frame_count,
},
}
}
}
impl Widget for AppWidget<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
let layout = Self::layout(area);
Self::render_title(layout[0], buf);
Self::render_colors(layout[1], buf);
}
}
impl RgbColors {
fn layout(area: Rect) -> Rc<[Rect]> {
Layout::default()
let main_layout = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(1), Constraint::Min(0)])
.split(area)
}
.split(area);
let title_layout = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Min(0), Constraint::Length(8)])
.split(main_layout[0]);
fn render_title(area: Rect, buf: &mut Buffer) {
Paragraph::new("colors_rgb example. Press q to quit")
.dark_gray()
.alignment(Alignment::Center)
.render(area, buf);
self.title.render(title_layout[0], buf);
self.fps_widget.render(title_layout[1], buf);
self.rgb_colors_widget.render(main_layout[1], buf);
}
}
/// Render a colored grid of half block characters (`"▀"`) each with a different RGB color.
fn render_colors(area: Rect, buf: &mut Buffer) {
impl Widget for RgbColorsWidget<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
let colors = self.colors;
for (xi, x) in (area.left()..area.right()).enumerate() {
// animate the colors by shifting the x index by the frame number
let xi = (xi + self.frame_count) % (area.width as usize);
for (yi, y) in (area.top()..area.bottom()).enumerate() {
let hue = xi as f32 * 360.0 / area.width as f32;
let value_fg = (yi as f32) / (area.height as f32 - 0.5);
let fg = Okhsv::<f32>::new(hue, Okhsv::max_saturation(), value_fg);
let fg: Srgb = fg.into_color_unclamped();
let fg: Srgb<u8> = fg.into_format();
let fg = Color::Rgb(fg.red, fg.green, fg.blue);
let value_bg = (yi as f32 + 0.5) / (area.height as f32 - 0.5);
let bg = Okhsv::new(hue, Okhsv::max_saturation(), value_bg);
let bg = Srgb::<f32>::from_color_unclamped(bg);
let bg: Srgb<u8> = bg.into_format();
let bg = Color::Rgb(bg.red, bg.green, bg.blue);
let fg = colors[yi * 2][xi];
let bg = colors[yi * 2 + 1][xi];
buf.get_mut(x, y).set_char('▀').set_fg(fg).set_bg(bg);
}
}
}
}
/// Install a panic hook that restores the terminal before panicking.
fn install_panic_hook() {
better_panic::install();
let prev_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
let _ = restore_terminal();
prev_hook(info);
}));
impl<'a> Widget for FpsWidget<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
if let Some(fps) = self.fps.fps {
let text = format!("{:.1} fps", fps);
Paragraph::new(text).render(area, buf);
}
}
}
fn init_terminal() -> Result<()> {
enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
/// Install a panic hook that restores the terminal before panicking.
fn install_panic_hook() -> color_eyre::Result<()> {
let (panic, error) = HookBuilder::default().into_hooks();
let panic = panic.into_panic_hook();
let error = error.into_eyre_hook();
color_eyre::eyre::set_hook(Box::new(move |e| {
let _ = restore_terminal();
error(e)
}))?;
std::panic::set_hook(Box::new(move |info| {
let _ = restore_terminal();
panic(info)
}));
Ok(())
}
fn restore_terminal() -> Result<()> {
fn init_terminal() -> color_eyre::Result<Terminal<impl Backend>> {
enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
terminal.clear()?;
terminal.hide_cursor()?;
Ok(terminal)
}
fn restore_terminal() -> color_eyre::Result<()> {
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
Ok(())

View File

@@ -1,13 +1,21 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/colors_rgb.tape`
# note that this script sometimes results in the gif having screen tearing
# issues. I'm not sure why, but it's not a problem with the library.
Output "target/colors_rgb.gif"
Set Theme "Aardvark Blue"
Set Width 1200
Set Height 800
Set Height 1200
# unsure if these help the screen tearing issue, but they don't hurt
Set Framerate 60
Set CursorBlink false
Hide
Type "cargo run --example=colors_rgb --features=crossterm"
Type "cargo run --example=colors_rgb --features=crossterm --release"
Enter
Sleep 2s
Screenshot "target/colors_rgb.png"
# Screenshot "target/colors_rgb.png"
Show
Sleep 1s
Sleep 10s

View File

@@ -216,12 +216,12 @@ fn handle_key_event(
) -> ControlFlow<()> {
match key.code {
KeyCode::Char('q') => return ControlFlow::Break(()),
KeyCode::Left => {
KeyCode::Left | KeyCode::Char('h') => {
button_states[*selected_button] = State::Normal;
*selected_button = selected_button.saturating_sub(1);
button_states[*selected_button] = State::Selected;
}
KeyCode::Right => {
KeyCode::Right | KeyCode::Char('l') => {
button_states[*selected_button] = State::Normal;
*selected_button = selected_button.saturating_add(1).min(2);
button_states[*selected_button] = State::Selected;

View File

@@ -55,11 +55,11 @@ fn run_app<B: Backend>(
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Left | KeyCode::Char('h') => app.on_left(),
KeyCode::Up | KeyCode::Char('k') => app.on_up(),
KeyCode::Right | KeyCode::Char('l') => app.on_right(),
KeyCode::Down | KeyCode::Char('j') => app.on_down(),
KeyCode::Char(c) => app.on_key(c),
KeyCode::Left => app.on_left(),
KeyCode::Up => app.on_up(),
KeyCode::Right => app.on_right(),
KeyCode::Down => app.on_down(),
_ => {}
}
}

View File

@@ -39,11 +39,11 @@ fn run_app<B: Backend>(
match events.recv()? {
Event::Input(key) => match key {
Key::Up | Key::Char('k') => app.on_up(),
Key::Down | Key::Char('j') => app.on_down(),
Key::Left | Key::Char('h') => app.on_left(),
Key::Right | Key::Char('l') => app.on_right(),
Key::Char(c) => app.on_key(c),
Key::Up => app.on_up(),
Key::Down => app.on_down(),
Key::Left => app.on_left(),
Key::Right => app.on_right(),
_ => {}
},
Event::Tick => app.on_tick(),

View File

@@ -45,10 +45,10 @@ fn run_app(
{
match input {
InputEvent::Key(key_code) => match key_code.key {
KeyCode::UpArrow => app.on_up(),
KeyCode::DownArrow => app.on_down(),
KeyCode::LeftArrow => app.on_left(),
KeyCode::RightArrow => app.on_right(),
KeyCode::UpArrow | KeyCode::Char('k') => app.on_up(),
KeyCode::DownArrow | KeyCode::Char('j') => app.on_down(),
KeyCode::LeftArrow | KeyCode::Char('h') => app.on_left(),
KeyCode::RightArrow | KeyCode::Char('l') => app.on_right(),
KeyCode::Char(c) => app.on_key(c),
_ => {}
},

View File

@@ -289,18 +289,20 @@ fn draw_second_tab(f: &mut Frame, app: &mut App, area: Rect) {
};
Row::new(vec![s.name, s.location, s.status]).style(style)
});
let table = Table::new(rows)
.header(
Row::new(vec!["Server", "Location", "Status"])
.style(Style::default().fg(Color::Yellow))
.bottom_margin(1),
)
.block(Block::default().title("Servers").borders(Borders::ALL))
.widths(&[
let table = Table::new(
rows,
[
Constraint::Length(15),
Constraint::Length(15),
Constraint::Length(10),
]);
],
)
.header(
Row::new(vec!["Server", "Location", "Status"])
.style(Style::default().fg(Color::Yellow))
.bottom_margin(1),
)
.block(Block::default().title("Servers").borders(Borders::ALL));
f.render_widget(table, chunks[0]);
let map = Canvas::default()
@@ -393,12 +395,14 @@ fn draw_third_tab(f: &mut Frame, _app: &mut App, area: Rect) {
Row::new(cells)
})
.collect();
let table = Table::new(items)
.block(Block::default().title("Colors").borders(Borders::ALL))
.widths(&[
let table = Table::new(
items,
[
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
]);
],
)
.block(Block::default().title("Colors").borders(Borders::ALL));
f.render_widget(table, chunks[0]);
}

View File

@@ -146,10 +146,9 @@ fn render_ingredients(selected_row: usize, area: Rect, buf: &mut Buffer) {
let rows = INGREDIENTS.iter().map(|&i| i.into()).collect_vec();
let theme = THEME.recipe;
StatefulWidget::render(
Table::new(rows)
Table::new(rows, [Constraint::Length(7), Constraint::Length(30)])
.block(Block::new().style(theme.ingredients))
.header(Row::new(vec!["Qty", "Ingredient"]).style(theme.ingredients_header))
.widths(&[Constraint::Length(7), Constraint::Length(30)])
.highlight_style(Style::new().light_yellow()),
area,
buf,

View File

@@ -50,9 +50,8 @@ fn render_hops(selected_row: usize, area: Rect, buf: &mut Buffer) {
.title_alignment(Alignment::Center)
.padding(Padding::new(1, 1, 1, 1));
StatefulWidget::render(
Table::new(rows)
Table::new(rows, [Constraint::Max(100), Constraint::Length(15)])
.header(Row::new(vec!["Host", "Address"]).set_style(THEME.traceroute.header))
.widths(&[Constraint::Max(100), Constraint::Length(15)])
.highlight_style(THEME.traceroute.selected)
.block(block),
area,

View File

@@ -181,9 +181,9 @@ fn run_app<B: Backend>(
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Char('q') => return Ok(()),
KeyCode::Left => app.items.unselect(),
KeyCode::Down => app.items.next(),
KeyCode::Up => app.items.previous(),
KeyCode::Left | KeyCode::Char('h') => app.items.unselect(),
KeyCode::Down | KeyCode::Char('j') => app.items.next(),
KeyCode::Up | KeyCode::Char('k') => app.items.previous(),
_ => {}
}
}
@@ -273,6 +273,6 @@ fn ui(f: &mut Frame, app: &mut App) {
.collect();
let events_list = List::new(events)
.block(Block::default().borders(Borders::ALL).title("List"))
.start_corner(Corner::BottomLeft);
.direction(ListDirection::BottomToTop);
f.render_widget(events_list, chunks[1]);
}

71
examples/ratatui-logo.rs Normal file
View File

@@ -0,0 +1,71 @@
use std::{
io::{self, stdout},
thread::sleep,
time::Duration,
};
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use indoc::indoc;
use itertools::izip;
use ratatui::{prelude::*, widgets::*};
/// A fun example of using half block characters to draw a logo
fn main() -> io::Result<()> {
let r = indoc! {"
▄▄▄
█▄▄▀
█ █
"}
.lines();
let a = indoc! {"
▄▄
█▄▄█
█ █
"}
.lines();
let t = indoc! {"
▄▄▄
"}
.lines();
let u = indoc! {"
▄ ▄
█ █
▀▄▄▀
"}
.lines();
let i = indoc! {"
"}
.lines();
let mut terminal = init()?;
terminal.draw(|frame| {
let logo = izip!(r, a.clone(), t.clone(), a, t, u, i)
.map(|(r, a, t, a2, t2, u, i)| {
format!("{:5}{:5}{:4}{:5}{:4}{:5}{:5}", r, a, t, a2, t2, u, i)
})
.collect::<Vec<_>>()
.join("\n");
frame.render_widget(Paragraph::new(logo), frame.size());
})?;
sleep(Duration::from_secs(5));
restore()?;
println!();
Ok(())
}
pub fn init() -> io::Result<Terminal<impl Backend>> {
enable_raw_mode()?;
let options = TerminalOptions {
viewport: Viewport::Inline(3),
};
Terminal::with_options(CrosstermBackend::new(stdout()), options)
}
pub fn restore() -> io::Result<()> {
disable_raw_mode()?;
Ok(())
}

View File

@@ -0,0 +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/popup.tape`
Output "target/ratatui-logo.gif"
Set Theme "Aardvark Blue"
Set Width 550
Set Height 220
Hide
Type "cargo run --example=ratatui-logo --features=crossterm"
Enter
Sleep 2s
Show
Sleep 2s

View File

@@ -104,8 +104,8 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Char('q') => return Ok(()),
KeyCode::Down => app.next(),
KeyCode::Up => app.previous(),
KeyCode::Down | KeyCode::Char('j') => app.next(),
KeyCode::Up | KeyCode::Char('k') => app.previous(),
_ => {}
}
}
@@ -127,6 +127,13 @@ fn ui(f: &mut Frame, app: &mut App) {
.style(normal_style)
.height(1)
.bottom_margin(1);
let footer_cells = ["Footer1", "Footer2", "Footer3"]
.iter()
.map(|f| Cell::from(*f).style(Style::default().fg(Color::Yellow)));
let footer = Row::new(footer_cells)
.style(normal_style)
.height(1)
.top_margin(1);
let rows = app.items.iter().map(|item| {
let height = item
.iter()
@@ -137,15 +144,18 @@ fn ui(f: &mut Frame, app: &mut App) {
let cells = item.iter().map(|c| Cell::from(*c));
Row::new(cells).height(height as u16).bottom_margin(1)
});
let t = Table::new(rows)
.header(header)
.block(Block::default().borders(Borders::ALL).title("Table"))
.highlight_style(selected_style)
.highlight_symbol(">> ")
.widths(&[
let t = Table::new(
rows,
[
Constraint::Percentage(50),
Constraint::Max(30),
Constraint::Min(10),
]);
],
)
.header(header)
.footer(footer)
.block(Block::default().borders(Borders::ALL).title("Table"))
.highlight_style(selected_style)
.highlight_symbol(">> ");
f.render_stateful_widget(t, rects[0], &mut app.state);
}

View File

@@ -69,8 +69,8 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Char('q') => return Ok(()),
KeyCode::Right => app.next(),
KeyCode::Left => app.previous(),
KeyCode::Right | KeyCode::Char('l') => app.next(),
KeyCode::Left | KeyCode::Char('h') => app.previous(),
_ => {}
}
}

View File

@@ -3,3 +3,4 @@ group_imports = "StdExternalCrate"
imports_granularity = "Crate"
wrap_comments = true
comment_width = 100
format_code_in_doc_comments = true

View File

@@ -11,7 +11,7 @@
//!
//! Additionally, a [`TestBackend`] is provided for testing purposes.
//!
//! See the [Backend Comparison] section of the [Ratatui Book] for more details on the different
//! See the [Backend Comparison] section of the [Ratatui Website] for more details on the different
//! backends.
//!
//! Each backend supports a number of features, such as [raw mode](#raw-mode), [alternate
@@ -26,6 +26,7 @@
//!
//! ```rust,no_run
//! use std::io::stdout;
//!
//! use ratatui::prelude::*;
//!
//! let backend = CrosstermBackend::new(stdout());
@@ -97,8 +98,8 @@
//! [Termwiz]: https://crates.io/crates/termwiz
//! [examples]: https://github.com/ratatui-org/ratatui/tree/main/examples#readme
//! [Backend Comparison]:
//! https://ratatui-org.github.io/ratatui-book/concepts/backends/comparison.html
//! [Ratatui Book]: https://ratatui-org.github.io/ratatui-book
//! https://ratatui.rs/concepts/backends/comparison/
//! [Ratatui Website]: https://ratatui-org.github.io/ratatui-book
use std::io;
use strum::{Display, EnumString};
@@ -228,7 +229,7 @@ pub trait Backend {
/// [`get_cursor`]: Backend::get_cursor
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()>;
/// Clears the whole terminal scree
/// Clears the whole terminal screen
///
/// # Example
///

View File

@@ -10,8 +10,8 @@ use crossterm::{
cursor::{Hide, MoveTo, Show},
execute, queue,
style::{
Attribute as CAttribute, Color as CColor, Print, SetAttribute, SetBackgroundColor,
SetForegroundColor,
Attribute as CAttribute, Attributes as CAttributes, Color as CColor, ContentStyle, Print,
SetAttribute, SetBackgroundColor, SetForegroundColor,
},
terminal::{self, Clear},
};
@@ -21,7 +21,7 @@ use crate::{
buffer::Cell,
layout::Size,
prelude::Rect,
style::{Color, Modifier},
style::{Color, Modifier, Style},
};
/// A [`Backend`] implementation that uses [Crossterm] to render to the terminal.
@@ -43,7 +43,8 @@ use crate::{
/// # Example
///
/// ```rust,no_run
/// use std::io::{stdout, stderr};
/// use std::io::{stderr, stdout};
///
/// use crossterm::{
/// terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
/// ExecutableCommand,
@@ -161,7 +162,7 @@ where
underline_color = cell.underline_color;
}
queue!(self.writer, Print(&cell.symbol))?;
queue!(self.writer, Print(cell.symbol()))?;
}
#[cfg(feature = "underline-color")]
@@ -274,6 +275,32 @@ impl From<Color> for CColor {
}
}
impl From<CColor> for Color {
fn from(value: CColor) -> Self {
match value {
CColor::Reset => Self::Reset,
CColor::Black => Self::Black,
CColor::DarkRed => Self::Red,
CColor::DarkGreen => Self::Green,
CColor::DarkYellow => Self::Yellow,
CColor::DarkBlue => Self::Blue,
CColor::DarkMagenta => Self::Magenta,
CColor::DarkCyan => Self::Cyan,
CColor::Grey => Self::Gray,
CColor::DarkGrey => Self::DarkGray,
CColor::Red => Self::LightRed,
CColor::Green => Self::LightGreen,
CColor::Blue => Self::LightBlue,
CColor::Yellow => Self::LightYellow,
CColor::Magenta => Self::LightMagenta,
CColor::Cyan => Self::LightCyan,
CColor::White => Self::White,
CColor::Rgb { r, g, b } => Self::Rgb(r, g, b),
CColor::AnsiValue(v) => Self::Indexed(v),
}
}
}
/// The `ModifierDiff` struct is used to calculate the difference between two `Modifier`
/// values. This is useful when updating the terminal display, as it allows for more
/// efficient updates by only sending the necessary changes.
@@ -344,3 +371,303 @@ impl ModifierDiff {
Ok(())
}
}
impl From<CAttribute> for Modifier {
fn from(value: CAttribute) -> Self {
// `Attribute*s*` (note the *s*) contains multiple `Attribute`
// We convert `Attribute` to `Attribute*s*` (containing only 1 value) to avoid implementing
// the conversion again
Modifier::from(CAttributes::from(value))
}
}
impl From<CAttributes> for Modifier {
fn from(value: CAttributes) -> Self {
let mut res = Modifier::empty();
if value.has(CAttribute::Bold) {
res |= Modifier::BOLD;
}
if value.has(CAttribute::Dim) {
res |= Modifier::DIM;
}
if value.has(CAttribute::Italic) {
res |= Modifier::ITALIC;
}
if value.has(CAttribute::Underlined)
|| value.has(CAttribute::DoubleUnderlined)
|| value.has(CAttribute::Undercurled)
|| value.has(CAttribute::Underdotted)
|| value.has(CAttribute::Underdashed)
{
res |= Modifier::UNDERLINED;
}
if value.has(CAttribute::SlowBlink) {
res |= Modifier::SLOW_BLINK;
}
if value.has(CAttribute::RapidBlink) {
res |= Modifier::RAPID_BLINK;
}
if value.has(CAttribute::Reverse) {
res |= Modifier::REVERSED;
}
if value.has(CAttribute::Hidden) {
res |= Modifier::HIDDEN;
}
if value.has(CAttribute::CrossedOut) {
res |= Modifier::CROSSED_OUT;
}
res
}
}
impl From<ContentStyle> for Style {
fn from(value: ContentStyle) -> Self {
let mut sub_modifier = Modifier::empty();
if value.attributes.has(CAttribute::NoBold) {
sub_modifier |= Modifier::BOLD;
}
if value.attributes.has(CAttribute::NoItalic) {
sub_modifier |= Modifier::ITALIC;
}
if value.attributes.has(CAttribute::NotCrossedOut) {
sub_modifier |= Modifier::CROSSED_OUT;
}
if value.attributes.has(CAttribute::NoUnderline) {
sub_modifier |= Modifier::UNDERLINED;
}
if value.attributes.has(CAttribute::NoHidden) {
sub_modifier |= Modifier::HIDDEN;
}
if value.attributes.has(CAttribute::NoBlink) {
sub_modifier |= Modifier::RAPID_BLINK | Modifier::SLOW_BLINK;
}
if value.attributes.has(CAttribute::NoReverse) {
sub_modifier |= Modifier::REVERSED;
}
Self {
fg: value.foreground_color.map(|c| c.into()),
bg: value.background_color.map(|c| c.into()),
#[cfg(feature = "underline-color")]
underline_color: value.underline_color.map(|c| c.into()),
add_modifier: value.attributes.into(),
sub_modifier,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_crossterm_color() {
assert_eq!(Color::from(CColor::Reset), Color::Reset);
assert_eq!(Color::from(CColor::Black), Color::Black);
assert_eq!(Color::from(CColor::DarkGrey), Color::DarkGray);
assert_eq!(Color::from(CColor::Red), Color::LightRed);
assert_eq!(Color::from(CColor::DarkRed), Color::Red);
assert_eq!(Color::from(CColor::Green), Color::LightGreen);
assert_eq!(Color::from(CColor::DarkGreen), Color::Green);
assert_eq!(Color::from(CColor::Yellow), Color::LightYellow);
assert_eq!(Color::from(CColor::DarkYellow), Color::Yellow);
assert_eq!(Color::from(CColor::Blue), Color::LightBlue);
assert_eq!(Color::from(CColor::DarkBlue), Color::Blue);
assert_eq!(Color::from(CColor::Magenta), Color::LightMagenta);
assert_eq!(Color::from(CColor::DarkMagenta), Color::Magenta);
assert_eq!(Color::from(CColor::Cyan), Color::LightCyan);
assert_eq!(Color::from(CColor::DarkCyan), Color::Cyan);
assert_eq!(Color::from(CColor::White), Color::White);
assert_eq!(Color::from(CColor::Grey), Color::Gray);
assert_eq!(
Color::from(CColor::Rgb { r: 0, g: 0, b: 0 }),
Color::Rgb(0, 0, 0)
);
assert_eq!(
Color::from(CColor::Rgb {
r: 10,
g: 20,
b: 30
}),
Color::Rgb(10, 20, 30)
);
assert_eq!(Color::from(CColor::AnsiValue(32)), Color::Indexed(32));
assert_eq!(Color::from(CColor::AnsiValue(37)), Color::Indexed(37));
}
mod modifier {
use super::*;
#[test]
fn from_crossterm_attribute() {
assert_eq!(Modifier::from(CAttribute::Reset), Modifier::empty());
assert_eq!(Modifier::from(CAttribute::Bold), Modifier::BOLD);
assert_eq!(Modifier::from(CAttribute::Italic), Modifier::ITALIC);
assert_eq!(Modifier::from(CAttribute::Underlined), Modifier::UNDERLINED);
assert_eq!(
Modifier::from(CAttribute::DoubleUnderlined),
Modifier::UNDERLINED
);
assert_eq!(
Modifier::from(CAttribute::Underdotted),
Modifier::UNDERLINED
);
assert_eq!(Modifier::from(CAttribute::Dim), Modifier::DIM);
assert_eq!(
Modifier::from(CAttribute::NormalIntensity),
Modifier::empty()
);
assert_eq!(
Modifier::from(CAttribute::CrossedOut),
Modifier::CROSSED_OUT
);
assert_eq!(Modifier::from(CAttribute::NoUnderline), Modifier::empty());
assert_eq!(Modifier::from(CAttribute::OverLined), Modifier::empty());
assert_eq!(Modifier::from(CAttribute::SlowBlink), Modifier::SLOW_BLINK);
assert_eq!(
Modifier::from(CAttribute::RapidBlink),
Modifier::RAPID_BLINK
);
assert_eq!(Modifier::from(CAttribute::Hidden), Modifier::HIDDEN);
assert_eq!(Modifier::from(CAttribute::NoHidden), Modifier::empty());
assert_eq!(Modifier::from(CAttribute::Reverse), Modifier::REVERSED);
}
#[test]
fn from_crossterm_attributes() {
assert_eq!(
Modifier::from(CAttributes::from(CAttribute::Bold)),
Modifier::BOLD
);
assert_eq!(
Modifier::from(CAttributes::from(
[CAttribute::Bold, CAttribute::Italic].as_ref()
)),
Modifier::BOLD | Modifier::ITALIC
);
assert_eq!(
Modifier::from(CAttributes::from(
[CAttribute::Bold, CAttribute::NotCrossedOut].as_ref()
)),
Modifier::BOLD
);
assert_eq!(
Modifier::from(CAttributes::from(
[CAttribute::Dim, CAttribute::Underdotted].as_ref()
)),
Modifier::DIM | Modifier::UNDERLINED
);
assert_eq!(
Modifier::from(CAttributes::from(
[CAttribute::Dim, CAttribute::SlowBlink, CAttribute::Italic].as_ref()
)),
Modifier::DIM | Modifier::SLOW_BLINK | Modifier::ITALIC
);
assert_eq!(
Modifier::from(CAttributes::from(
[
CAttribute::Hidden,
CAttribute::NoUnderline,
CAttribute::NotCrossedOut
]
.as_ref()
)),
Modifier::HIDDEN
);
assert_eq!(
Modifier::from(CAttributes::from(CAttribute::Reverse)),
Modifier::REVERSED
);
assert_eq!(
Modifier::from(CAttributes::from(CAttribute::Reset)),
Modifier::empty()
);
assert_eq!(
Modifier::from(CAttributes::from(
[CAttribute::RapidBlink, CAttribute::CrossedOut].as_ref()
)),
Modifier::RAPID_BLINK | Modifier::CROSSED_OUT
);
}
}
#[test]
fn from_crossterm_content_style() {
assert_eq!(Style::from(ContentStyle::default()), Style::default());
assert_eq!(
Style::from(ContentStyle {
foreground_color: Some(CColor::DarkYellow),
..Default::default()
}),
Style::default().fg(Color::Yellow)
);
assert_eq!(
Style::from(ContentStyle {
background_color: Some(CColor::DarkYellow),
..Default::default()
}),
Style::default().bg(Color::Yellow)
);
assert_eq!(
Style::from(ContentStyle {
attributes: CAttributes::from(CAttribute::Bold),
..Default::default()
}),
Style::default().add_modifier(Modifier::BOLD)
);
assert_eq!(
Style::from(ContentStyle {
attributes: CAttributes::from(CAttribute::NoBold),
..Default::default()
}),
Style::default().remove_modifier(Modifier::BOLD)
);
assert_eq!(
Style::from(ContentStyle {
attributes: CAttributes::from(CAttribute::Italic),
..Default::default()
}),
Style::default().add_modifier(Modifier::ITALIC)
);
assert_eq!(
Style::from(ContentStyle {
attributes: CAttributes::from(CAttribute::NoItalic),
..Default::default()
}),
Style::default().remove_modifier(Modifier::ITALIC)
);
assert_eq!(
Style::from(ContentStyle {
attributes: CAttributes::from([CAttribute::Bold, CAttribute::Italic].as_ref()),
..Default::default()
}),
Style::default()
.add_modifier(Modifier::BOLD)
.add_modifier(Modifier::ITALIC)
);
assert_eq!(
Style::from(ContentStyle {
attributes: CAttributes::from([CAttribute::NoBold, CAttribute::NoItalic].as_ref()),
..Default::default()
}),
Style::default()
.remove_modifier(Modifier::BOLD)
.remove_modifier(Modifier::ITALIC)
);
}
#[test]
#[cfg(feature = "underline-color")]
fn from_crossterm_content_style_underline() {
assert_eq!(
Style::from(ContentStyle {
underline_color: Some(CColor::DarkRed),
..Default::default()
}),
Style::default().underline_color(Color::Red)
)
}
}

View File

@@ -9,11 +9,13 @@ use std::{
io::{self, Write},
};
use termion::{color as tcolor, style as tstyle};
use crate::{
backend::{Backend, ClearType, WindowSize},
buffer::Cell,
prelude::Rect,
style::{Color, Modifier},
style::{Color, Modifier, Style},
};
/// A [`Backend`] implementation that uses [Termion] to render to the terminal.
@@ -36,7 +38,8 @@ use crate::{
/// # Example
///
/// ```rust,no_run
/// use std::io::{stdout, stderr};
/// use std::io::{stderr, stdout};
///
/// use ratatui::prelude::*;
/// use termion::{raw::IntoRawMode, screen::IntoAlternateScreen};
///
@@ -179,7 +182,7 @@ where
write!(string, "{}", Bg(cell.bg)).unwrap();
bg = cell.bg;
}
string.push_str(&cell.symbol);
string.push_str(cell.symbol());
}
write!(
self.writer,
@@ -274,6 +277,82 @@ impl fmt::Display for Bg {
}
}
macro_rules! from_termion_for_color {
($termion_color:ident, $color: ident) => {
impl From<tcolor::$termion_color> for Color {
fn from(_: tcolor::$termion_color) -> Self {
Color::$color
}
}
impl From<tcolor::Bg<tcolor::$termion_color>> for Style {
fn from(_: tcolor::Bg<tcolor::$termion_color>) -> Self {
Style::default().bg(Color::$color)
}
}
impl From<tcolor::Fg<tcolor::$termion_color>> for Style {
fn from(_: tcolor::Fg<tcolor::$termion_color>) -> Self {
Style::default().fg(Color::$color)
}
}
};
}
from_termion_for_color!(Reset, Reset);
from_termion_for_color!(Black, Black);
from_termion_for_color!(Red, Red);
from_termion_for_color!(Green, Green);
from_termion_for_color!(Yellow, Yellow);
from_termion_for_color!(Blue, Blue);
from_termion_for_color!(Magenta, Magenta);
from_termion_for_color!(Cyan, Cyan);
from_termion_for_color!(White, Gray);
from_termion_for_color!(LightBlack, DarkGray);
from_termion_for_color!(LightRed, LightRed);
from_termion_for_color!(LightGreen, LightGreen);
from_termion_for_color!(LightBlue, LightBlue);
from_termion_for_color!(LightYellow, LightYellow);
from_termion_for_color!(LightMagenta, LightMagenta);
from_termion_for_color!(LightCyan, LightCyan);
from_termion_for_color!(LightWhite, White);
impl From<tcolor::AnsiValue> for Color {
fn from(value: tcolor::AnsiValue) -> Self {
Color::Indexed(value.0)
}
}
impl From<tcolor::Bg<tcolor::AnsiValue>> for Style {
fn from(value: tcolor::Bg<tcolor::AnsiValue>) -> Self {
Style::default().bg(Color::Indexed(value.0 .0))
}
}
impl From<tcolor::Fg<tcolor::AnsiValue>> for Style {
fn from(value: tcolor::Fg<tcolor::AnsiValue>) -> Self {
Style::default().fg(Color::Indexed(value.0 .0))
}
}
impl From<tcolor::Rgb> for Color {
fn from(value: tcolor::Rgb) -> Self {
Color::Rgb(value.0, value.1, value.2)
}
}
impl From<tcolor::Bg<tcolor::Rgb>> for Style {
fn from(value: tcolor::Bg<tcolor::Rgb>) -> Self {
Style::default().bg(Color::Rgb(value.0 .0, value.0 .1, value.0 .2))
}
}
impl From<tcolor::Fg<tcolor::Rgb>> for Style {
fn from(value: tcolor::Fg<tcolor::Rgb>) -> Self {
Style::default().fg(Color::Rgb(value.0 .0, value.0 .1, value.0 .2))
}
}
impl fmt::Display for ModifierDiff {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let remove = self.from - self.to;
@@ -338,3 +417,147 @@ impl fmt::Display for ModifierDiff {
Ok(())
}
}
macro_rules! from_termion_for_modifier {
($termion_modifier:ident, $modifier: ident) => {
impl From<tstyle::$termion_modifier> for Modifier {
fn from(_: tstyle::$termion_modifier) -> Self {
Modifier::$modifier
}
}
};
}
from_termion_for_modifier!(Invert, REVERSED);
from_termion_for_modifier!(Bold, BOLD);
from_termion_for_modifier!(Italic, ITALIC);
from_termion_for_modifier!(Underline, UNDERLINED);
from_termion_for_modifier!(Faint, DIM);
from_termion_for_modifier!(CrossedOut, CROSSED_OUT);
from_termion_for_modifier!(Blink, SLOW_BLINK);
impl From<termion::style::Reset> for Modifier {
fn from(_: termion::style::Reset) -> Self {
Modifier::empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::style::Stylize;
#[test]
fn from_termion_color() {
assert_eq!(Color::from(tcolor::Reset), Color::Reset);
assert_eq!(Color::from(tcolor::Black), Color::Black);
assert_eq!(Color::from(tcolor::Red), Color::Red);
assert_eq!(Color::from(tcolor::Green), Color::Green);
assert_eq!(Color::from(tcolor::Yellow), Color::Yellow);
assert_eq!(Color::from(tcolor::Blue), Color::Blue);
assert_eq!(Color::from(tcolor::Magenta), Color::Magenta);
assert_eq!(Color::from(tcolor::Cyan), Color::Cyan);
assert_eq!(Color::from(tcolor::White), Color::Gray);
assert_eq!(Color::from(tcolor::LightBlack), Color::DarkGray);
assert_eq!(Color::from(tcolor::LightRed), Color::LightRed);
assert_eq!(Color::from(tcolor::LightGreen), Color::LightGreen);
assert_eq!(Color::from(tcolor::LightBlue), Color::LightBlue);
assert_eq!(Color::from(tcolor::LightYellow), Color::LightYellow);
assert_eq!(Color::from(tcolor::LightMagenta), Color::LightMagenta);
assert_eq!(Color::from(tcolor::LightCyan), Color::LightCyan);
assert_eq!(Color::from(tcolor::LightWhite), Color::White);
assert_eq!(Color::from(tcolor::AnsiValue(31)), Color::Indexed(31));
assert_eq!(Color::from(tcolor::Rgb(1, 2, 3)), Color::Rgb(1, 2, 3));
}
#[test]
fn from_termion_bg() {
use tc::Bg;
use tcolor as tc;
assert_eq!(Style::from(Bg(tc::Reset)), Style::new().bg(Color::Reset));
assert_eq!(Style::from(Bg(tc::Black)), Style::new().on_black());
assert_eq!(Style::from(Bg(tc::Red)), Style::new().on_red());
assert_eq!(Style::from(Bg(tc::Green)), Style::new().on_green());
assert_eq!(Style::from(Bg(tc::Yellow)), Style::new().on_yellow());
assert_eq!(Style::from(Bg(tc::Blue)), Style::new().on_blue());
assert_eq!(Style::from(Bg(tc::Magenta)), Style::new().on_magenta());
assert_eq!(Style::from(Bg(tc::Cyan)), Style::new().on_cyan());
assert_eq!(Style::from(Bg(tc::White)), Style::new().on_gray());
assert_eq!(Style::from(Bg(tc::LightBlack)), Style::new().on_dark_gray());
assert_eq!(Style::from(Bg(tc::LightRed)), Style::new().on_light_red());
assert_eq!(
Style::from(Bg(tc::LightGreen)),
Style::new().on_light_green()
);
assert_eq!(Style::from(Bg(tc::LightBlue)), Style::new().on_light_blue());
assert_eq!(
Style::from(Bg(tc::LightYellow)),
Style::new().on_light_yellow()
);
assert_eq!(
Style::from(Bg(tc::LightMagenta)),
Style::new().on_light_magenta()
);
assert_eq!(Style::from(Bg(tc::LightCyan)), Style::new().on_light_cyan());
assert_eq!(Style::from(Bg(tc::LightWhite)), Style::new().on_white());
assert_eq!(
Style::from(Bg(tc::AnsiValue(31))),
Style::new().bg(Color::Indexed(31))
);
assert_eq!(
Style::from(Bg(tc::Rgb(1, 2, 3))),
Style::new().bg(Color::Rgb(1, 2, 3))
);
}
#[test]
fn from_termion_fg() {
use tc::Fg;
use tcolor as tc;
assert_eq!(Style::from(Fg(tc::Reset)), Style::new().fg(Color::Reset));
assert_eq!(Style::from(Fg(tc::Black)), Style::new().black());
assert_eq!(Style::from(Fg(tc::Red)), Style::new().red());
assert_eq!(Style::from(Fg(tc::Green)), Style::new().green());
assert_eq!(Style::from(Fg(tc::Yellow)), Style::new().yellow());
assert_eq!(Style::from(Fg(tc::Blue)), Style::default().blue());
assert_eq!(Style::from(Fg(tc::Magenta)), Style::default().magenta());
assert_eq!(Style::from(Fg(tc::Cyan)), Style::default().cyan());
assert_eq!(Style::from(Fg(tc::White)), Style::default().gray());
assert_eq!(Style::from(Fg(tc::LightBlack)), Style::new().dark_gray());
assert_eq!(Style::from(Fg(tc::LightRed)), Style::new().light_red());
assert_eq!(Style::from(Fg(tc::LightGreen)), Style::new().light_green());
assert_eq!(Style::from(Fg(tc::LightBlue)), Style::new().light_blue());
assert_eq!(
Style::from(Fg(tc::LightYellow)),
Style::new().light_yellow()
);
assert_eq!(
Style::from(Fg(tc::LightMagenta)),
Style::new().light_magenta()
);
assert_eq!(Style::from(Fg(tc::LightCyan)), Style::new().light_cyan());
assert_eq!(Style::from(Fg(tc::LightWhite)), Style::new().white());
assert_eq!(
Style::from(Fg(tc::AnsiValue(31))),
Style::default().fg(Color::Indexed(31))
);
assert_eq!(
Style::from(Fg(tc::Rgb(1, 2, 3))),
Style::default().fg(Color::Rgb(1, 2, 3))
);
}
#[test]
fn from_termion_style() {
assert_eq!(Modifier::from(tstyle::Invert), Modifier::REVERSED);
assert_eq!(Modifier::from(tstyle::Bold), Modifier::BOLD);
assert_eq!(Modifier::from(tstyle::Italic), Modifier::ITALIC);
assert_eq!(Modifier::from(tstyle::Underline), Modifier::UNDERLINED);
assert_eq!(Modifier::from(tstyle::Faint), Modifier::DIM);
assert_eq!(Modifier::from(tstyle::CrossedOut), Modifier::CROSSED_OUT);
assert_eq!(Modifier::from(tstyle::Blink), Modifier::SLOW_BLINK);
assert_eq!(Modifier::from(tstyle::Reset), Modifier::empty());
}
}

View File

@@ -9,8 +9,8 @@ use std::{error::Error, io};
use termwiz::{
caps::Capabilities,
cell::{AttributeChange, Blink, Intensity, Underline},
color::{AnsiColor, ColorAttribute, SrgbaTuple},
cell::{AttributeChange, Blink, CellAttributes, Intensity, Underline},
color::{AnsiColor, ColorAttribute, ColorSpec, LinearRgba, RgbColor, SrgbaTuple},
surface::{Change, CursorVisibility, Position},
terminal::{buffered::BufferedTerminal, ScreenSize, SystemTerminal, Terminal},
};
@@ -20,7 +20,7 @@ use crate::{
buffer::Cell,
layout::Size,
prelude::Rect,
style::{Color, Modifier},
style::{Color, Modifier, Style},
};
/// A [`Backend`] implementation that uses [Termwiz] to render to the terminal.
@@ -176,7 +176,7 @@ impl Backend for TermwizBackend {
},
)));
self.buffered_terminal.add_change(&cell.symbol);
self.buffered_terminal.add_change(cell.symbol());
}
Ok(())
}
@@ -249,12 +249,73 @@ impl Backend for TermwizBackend {
}
}
impl From<CellAttributes> for Style {
fn from(value: CellAttributes) -> Self {
let mut style = Style::new()
.add_modifier(value.intensity().into())
.add_modifier(value.underline().into())
.add_modifier(value.blink().into());
if value.italic() {
style.add_modifier |= Modifier::ITALIC;
}
if value.reverse() {
style.add_modifier |= Modifier::REVERSED;
}
if value.strikethrough() {
style.add_modifier |= Modifier::CROSSED_OUT;
}
if value.invisible() {
style.add_modifier |= Modifier::HIDDEN;
}
style.fg = Some(value.foreground().into());
style.bg = Some(value.background().into());
#[cfg(feature = "underline_color")]
{
style.underline_color = Some(value.underline_color().into());
}
style
}
}
impl From<Intensity> for Modifier {
fn from(value: Intensity) -> Self {
match value {
Intensity::Normal => Modifier::empty(),
Intensity::Bold => Modifier::BOLD,
Intensity::Half => Modifier::DIM,
}
}
}
impl From<Underline> for Modifier {
fn from(value: Underline) -> Self {
match value {
Underline::None => Modifier::empty(),
_ => Modifier::UNDERLINED,
}
}
}
impl From<Blink> for Modifier {
fn from(value: Blink) -> Self {
match value {
Blink::None => Modifier::empty(),
Blink::Slow => Modifier::SLOW_BLINK,
Blink::Rapid => Modifier::RAPID_BLINK,
}
}
}
impl From<Color> for ColorAttribute {
fn from(color: Color) -> ColorAttribute {
match color {
Color::Reset => ColorAttribute::Default,
Color::Black => AnsiColor::Black.into(),
Color::Gray | Color::DarkGray => AnsiColor::Grey.into(),
Color::DarkGray => AnsiColor::Grey.into(),
Color::Gray => AnsiColor::Silver.into(),
Color::Red => AnsiColor::Maroon.into(),
Color::LightRed => AnsiColor::Red.into(),
Color::Green => AnsiColor::Green.into(),
@@ -276,7 +337,326 @@ impl From<Color> for ColorAttribute {
}
}
impl From<AnsiColor> for Color {
fn from(value: AnsiColor) -> Self {
match value {
AnsiColor::Black => Color::Black,
AnsiColor::Grey => Color::DarkGray,
AnsiColor::Silver => Color::Gray,
AnsiColor::Maroon => Color::Red,
AnsiColor::Red => Color::LightRed,
AnsiColor::Green => Color::Green,
AnsiColor::Lime => Color::LightGreen,
AnsiColor::Olive => Color::Yellow,
AnsiColor::Yellow => Color::LightYellow,
AnsiColor::Purple => Color::Magenta,
AnsiColor::Fuchsia => Color::LightMagenta,
AnsiColor::Teal => Color::Cyan,
AnsiColor::Aqua => Color::LightCyan,
AnsiColor::White => Color::White,
AnsiColor::Navy => Color::Blue,
AnsiColor::Blue => Color::LightBlue,
}
}
}
impl From<ColorAttribute> for Color {
fn from(value: ColorAttribute) -> Self {
match value {
ColorAttribute::TrueColorWithDefaultFallback(srgba)
| ColorAttribute::TrueColorWithPaletteFallback(srgba, _) => srgba.into(),
ColorAttribute::PaletteIndex(i) => Color::Indexed(i),
ColorAttribute::Default => Color::Reset,
}
}
}
impl From<ColorSpec> for Color {
fn from(value: ColorSpec) -> Self {
match value {
ColorSpec::Default => Color::Reset,
ColorSpec::PaletteIndex(i) => Color::Indexed(i),
ColorSpec::TrueColor(srgba) => srgba.into(),
}
}
}
impl From<SrgbaTuple> for Color {
fn from(value: SrgbaTuple) -> Self {
let (r, g, b, _) = value.to_srgb_u8();
Color::Rgb(r, g, b)
}
}
impl From<RgbColor> for Color {
fn from(value: RgbColor) -> Self {
let (r, g, b) = value.to_tuple_rgb8();
Color::Rgb(r, g, b)
}
}
impl From<LinearRgba> for Color {
fn from(value: LinearRgba) -> Self {
value.to_srgb().into()
}
}
#[inline]
fn u16_max(i: usize) -> u16 {
u16::try_from(i).unwrap_or(u16::MAX)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::style::Stylize;
mod into_color {
use Color as C;
use super::*;
#[test]
fn from_linear_rgba() {
// full black + opaque
assert_eq!(C::from(LinearRgba(0., 0., 0., 1.)), Color::Rgb(0, 0, 0));
// full black + transparent
assert_eq!(C::from(LinearRgba(0., 0., 0., 0.)), Color::Rgb(0, 0, 0));
// full white + opaque
assert_eq!(C::from(LinearRgba(1., 1., 1., 1.)), C::Rgb(254, 254, 254));
// full white + transparent
assert_eq!(C::from(LinearRgba(1., 1., 1., 0.)), C::Rgb(254, 254, 254));
// full red
assert_eq!(C::from(LinearRgba(1., 0., 0., 1.)), C::Rgb(254, 0, 0));
// full green
assert_eq!(C::from(LinearRgba(0., 1., 0., 1.)), C::Rgb(0, 254, 0));
// full blue
assert_eq!(C::from(LinearRgba(0., 0., 1., 1.)), C::Rgb(0, 0, 254));
// See https://stackoverflow.com/questions/12524623/what-are-the-practical-differences-when-working-with-colors-in-a-linear-vs-a-no
// for an explanation
// half red
assert_eq!(C::from(LinearRgba(0.214, 0., 0., 1.)), C::Rgb(127, 0, 0));
// half green
assert_eq!(C::from(LinearRgba(0., 0.214, 0., 1.)), C::Rgb(0, 127, 0));
// half blue
assert_eq!(C::from(LinearRgba(0., 0., 0.214, 1.)), C::Rgb(0, 0, 127));
}
#[test]
fn from_srgba() {
// full black + opaque
assert_eq!(C::from(SrgbaTuple(0., 0., 0., 1.)), Color::Rgb(0, 0, 0));
// full black + transparent
assert_eq!(C::from(SrgbaTuple(0., 0., 0., 0.)), Color::Rgb(0, 0, 0));
// full white + opaque
assert_eq!(C::from(SrgbaTuple(1., 1., 1., 1.)), C::Rgb(255, 255, 255));
// full white + transparent
assert_eq!(C::from(SrgbaTuple(1., 1., 1., 0.)), C::Rgb(255, 255, 255));
// full red
assert_eq!(C::from(SrgbaTuple(1., 0., 0., 1.)), C::Rgb(255, 0, 0));
// full green
assert_eq!(C::from(SrgbaTuple(0., 1., 0., 1.)), C::Rgb(0, 255, 0));
// full blue
assert_eq!(C::from(SrgbaTuple(0., 0., 1., 1.)), C::Rgb(0, 0, 255));
// half red
assert_eq!(C::from(SrgbaTuple(0.5, 0., 0., 1.)), C::Rgb(127, 0, 0));
// half green
assert_eq!(C::from(SrgbaTuple(0., 0.5, 0., 1.)), C::Rgb(0, 127, 0));
// half blue
assert_eq!(C::from(SrgbaTuple(0., 0., 0.5, 1.)), C::Rgb(0, 0, 127));
}
#[test]
fn from_rgbcolor() {
// full black
assert_eq!(C::from(RgbColor::new_8bpc(0, 0, 0)), Color::Rgb(0, 0, 0));
// full white
assert_eq!(
C::from(RgbColor::new_8bpc(255, 255, 255)),
C::Rgb(255, 255, 255)
);
// full red
assert_eq!(C::from(RgbColor::new_8bpc(255, 0, 0)), C::Rgb(255, 0, 0));
// full green
assert_eq!(C::from(RgbColor::new_8bpc(0, 255, 0)), C::Rgb(0, 255, 0));
// full blue
assert_eq!(C::from(RgbColor::new_8bpc(0, 0, 255)), C::Rgb(0, 0, 255));
// half red
assert_eq!(C::from(RgbColor::new_8bpc(127, 0, 0)), C::Rgb(127, 0, 0));
// half green
assert_eq!(C::from(RgbColor::new_8bpc(0, 127, 0)), C::Rgb(0, 127, 0));
// half blue
assert_eq!(C::from(RgbColor::new_8bpc(0, 0, 127)), C::Rgb(0, 0, 127));
}
#[test]
fn from_colorspec() {
assert_eq!(C::from(ColorSpec::Default), C::Reset);
assert_eq!(C::from(ColorSpec::PaletteIndex(33)), C::Indexed(33));
assert_eq!(
C::from(ColorSpec::TrueColor(SrgbaTuple(0., 0., 0., 1.))),
C::Rgb(0, 0, 0)
);
}
#[test]
fn from_colorattribute() {
assert_eq!(C::from(ColorAttribute::Default), C::Reset);
assert_eq!(C::from(ColorAttribute::PaletteIndex(32)), C::Indexed(32));
assert_eq!(
C::from(ColorAttribute::TrueColorWithDefaultFallback(SrgbaTuple(
0., 0., 0., 1.
))),
C::Rgb(0, 0, 0)
);
assert_eq!(
C::from(ColorAttribute::TrueColorWithPaletteFallback(
SrgbaTuple(0., 0., 0., 1.),
31
)),
C::Rgb(0, 0, 0)
);
}
#[test]
fn from_ansicolor() {
assert_eq!(C::from(AnsiColor::Black), Color::Black);
assert_eq!(C::from(AnsiColor::Grey), Color::DarkGray);
assert_eq!(C::from(AnsiColor::Silver), Color::Gray);
assert_eq!(C::from(AnsiColor::Maroon), Color::Red);
assert_eq!(C::from(AnsiColor::Red), Color::LightRed);
assert_eq!(C::from(AnsiColor::Green), Color::Green);
assert_eq!(C::from(AnsiColor::Lime), Color::LightGreen);
assert_eq!(C::from(AnsiColor::Olive), Color::Yellow);
assert_eq!(C::from(AnsiColor::Yellow), Color::LightYellow);
assert_eq!(C::from(AnsiColor::Purple), Color::Magenta);
assert_eq!(C::from(AnsiColor::Fuchsia), Color::LightMagenta);
assert_eq!(C::from(AnsiColor::Teal), Color::Cyan);
assert_eq!(C::from(AnsiColor::Aqua), Color::LightCyan);
assert_eq!(C::from(AnsiColor::White), Color::White);
assert_eq!(C::from(AnsiColor::Navy), Color::Blue);
assert_eq!(C::from(AnsiColor::Blue), Color::LightBlue);
}
}
mod into_modifier {
use super::*;
#[test]
fn from_intensity() {
assert_eq!(Modifier::from(Intensity::Normal), Modifier::empty());
assert_eq!(Modifier::from(Intensity::Bold), Modifier::BOLD);
assert_eq!(Modifier::from(Intensity::Half), Modifier::DIM);
}
#[test]
fn from_underline() {
assert_eq!(Modifier::from(Underline::None), Modifier::empty());
assert_eq!(Modifier::from(Underline::Single), Modifier::UNDERLINED);
assert_eq!(Modifier::from(Underline::Double), Modifier::UNDERLINED);
assert_eq!(Modifier::from(Underline::Curly), Modifier::UNDERLINED);
assert_eq!(Modifier::from(Underline::Dashed), Modifier::UNDERLINED);
assert_eq!(Modifier::from(Underline::Dotted), Modifier::UNDERLINED);
}
#[test]
fn from_blink() {
assert_eq!(Modifier::from(Blink::None), Modifier::empty());
assert_eq!(Modifier::from(Blink::Slow), Modifier::SLOW_BLINK);
assert_eq!(Modifier::from(Blink::Rapid), Modifier::RAPID_BLINK);
}
}
#[test]
fn from_cell_attribute_for_style() {
// default
assert_eq!(
Style::from(CellAttributes::default()),
Style::new().fg(Color::Reset).bg(Color::Reset)
);
// foreground color
assert_eq!(
Style::from(
CellAttributes::default()
.set_foreground(ColorAttribute::PaletteIndex(31))
.to_owned()
),
Style::new().fg(Color::Indexed(31)).bg(Color::Reset)
);
// background color
assert_eq!(
Style::from(
CellAttributes::default()
.set_background(ColorAttribute::PaletteIndex(31))
.to_owned()
),
Style::new().fg(Color::Reset).bg(Color::Indexed(31))
);
// underline color
#[cfg(feature = "underline_color")]
assert_eq!(
Style::from(
CellAttributes::default()
.set_underline_color(AnsiColor::Red)
.set
.to_owned()
),
Style::new()
.fg(Color::Reset)
.bg(Color::Reset)
.underline_color(Color::Red)
);
// underlined
assert_eq!(
Style::from(
CellAttributes::default()
.set_underline(Underline::Single)
.to_owned()
),
Style::new().fg(Color::Reset).bg(Color::Reset).underlined()
);
// blink
assert_eq!(
Style::from(CellAttributes::default().set_blink(Blink::Slow).to_owned()),
Style::new().fg(Color::Reset).bg(Color::Reset).slow_blink()
);
// intensity
assert_eq!(
Style::from(
CellAttributes::default()
.set_intensity(Intensity::Bold)
.to_owned()
),
Style::new().fg(Color::Reset).bg(Color::Reset).bold()
);
// italic
assert_eq!(
Style::from(CellAttributes::default().set_italic(true).to_owned()),
Style::new().fg(Color::Reset).bg(Color::Reset).italic()
);
// reversed
assert_eq!(
Style::from(CellAttributes::default().set_reverse(true).to_owned()),
Style::new().fg(Color::Reset).bg(Color::Reset).reversed()
);
// strikethrough
assert_eq!(
Style::from(CellAttributes::default().set_strikethrough(true).to_owned()),
Style::new().fg(Color::Reset).bg(Color::Reset).crossed_out()
);
// hidden
assert_eq!(
Style::from(CellAttributes::default().set_invisible(true).to_owned()),
Style::new().fg(Color::Reset).bg(Color::Reset).hidden()
);
}
}

View File

@@ -9,7 +9,7 @@ use std::{
use unicode_width::UnicodeWidthStr;
use crate::{
backend::{Backend, WindowSize},
backend::{Backend, ClearType, WindowSize},
buffer::{Buffer, Cell},
layout::{Rect, Size},
};
@@ -56,11 +56,11 @@ fn buffer_view(buffer: &Buffer) -> String {
view.push('"');
for (x, c) in cells.iter().enumerate() {
if skip == 0 {
view.push_str(&c.symbol);
view.push_str(c.symbol());
} else {
overwritten.push((x, &c.symbol));
overwritten.push((x, c.symbol()));
}
skip = std::cmp::max(skip, c.symbol.width()).saturating_sub(1);
skip = std::cmp::max(skip, c.symbol().width()).saturating_sub(1);
}
view.push('"');
if !overwritten.is_empty() {
@@ -179,6 +179,71 @@ impl Backend for TestBackend {
Ok(())
}
fn clear_region(&mut self, clear_type: super::ClearType) -> io::Result<()> {
match clear_type {
ClearType::All => self.clear()?,
ClearType::AfterCursor => {
let index = self.buffer.index_of(self.pos.0, self.pos.1) + 1;
self.buffer.content[index..].fill(Cell::default());
}
ClearType::BeforeCursor => {
let index = self.buffer.index_of(self.pos.0, self.pos.1);
self.buffer.content[..index].fill(Cell::default());
}
ClearType::CurrentLine => {
let line_start_index = self.buffer.index_of(0, self.pos.1);
let line_end_index = self.buffer.index_of(self.width - 1, self.pos.1);
self.buffer.content[line_start_index..=line_end_index].fill(Cell::default());
}
ClearType::UntilNewLine => {
let index = self.buffer.index_of(self.pos.0, self.pos.1);
let line_end_index = self.buffer.index_of(self.width - 1, self.pos.1);
self.buffer.content[index..=line_end_index].fill(Cell::default());
}
}
Ok(())
}
/// Inserts n line breaks at the current cursor position.
///
/// After the insertion, the cursor x position will be incremented by 1 (unless it's already
/// at the end of line). This is a common behaviour of terminals in raw mode.
///
/// If the number of lines to append is fewer than the number of lines in the buffer after the
/// cursor y position then the cursor is moved down by n rows.
///
/// If the number of lines to append is greater than the number of lines in the buffer after
/// the cursor y position then that number of empty lines (at most the buffer's height in this
/// case but this limit is instead replaced with scrolling in most backend implementations) will
/// be added after the current position and the cursor will be moved to the last row.
fn append_lines(&mut self, n: u16) -> io::Result<()> {
let (cur_x, cur_y) = self.get_cursor()?;
// the next column ensuring that we don't go past the last column
let new_cursor_x = cur_x.saturating_add(1).min(self.width.saturating_sub(1));
let max_y = self.height.saturating_sub(1);
let lines_after_cursor = max_y.saturating_sub(cur_y);
if n > lines_after_cursor {
let rotate_by = n.saturating_sub(lines_after_cursor).min(max_y);
if rotate_by == self.height - 1 {
self.clear()?;
}
self.set_cursor(0, rotate_by)?;
self.clear_region(ClearType::BeforeCursor)?;
self.buffer
.content
.rotate_left((self.width * rotate_by).into());
}
let new_cursor_y = cur_y.saturating_add(n).min(max_y);
self.set_cursor(new_cursor_x, new_cursor_y)?;
Ok(())
}
fn size(&self) -> Result<Rect, io::Error> {
Ok(Rect::new(0, 0, self.width, self.height))
}
@@ -310,13 +375,299 @@ mod tests {
#[test]
fn clear() {
let mut backend = TestBackend::new(10, 2);
let mut backend = TestBackend::new(10, 4);
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]));
backend.assert_buffer(&Buffer::with_lines(vec![
" ",
" ",
" ",
" ",
]));
}
#[test]
fn clear_region_all() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines(vec![
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
]);
backend.clear_region(ClearType::All).unwrap();
backend.assert_buffer(&Buffer::with_lines(vec![
" ",
" ",
" ",
" ",
" ",
]));
}
#[test]
fn clear_region_after_cursor() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines(vec![
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
]);
backend.set_cursor(3, 2).unwrap();
backend.clear_region(ClearType::AfterCursor).unwrap();
backend.assert_buffer(&Buffer::with_lines(vec![
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaa ",
" ",
" ",
]));
}
#[test]
fn clear_region_before_cursor() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines(vec![
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
]);
backend.set_cursor(5, 3).unwrap();
backend.clear_region(ClearType::BeforeCursor).unwrap();
backend.assert_buffer(&Buffer::with_lines(vec![
" ",
" ",
" ",
" aaaaa",
"aaaaaaaaaa",
]));
}
#[test]
fn clear_region_current_line() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines(vec![
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
]);
backend.set_cursor(3, 1).unwrap();
backend.clear_region(ClearType::CurrentLine).unwrap();
backend.assert_buffer(&Buffer::with_lines(vec![
"aaaaaaaaaa",
" ",
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
]));
}
#[test]
fn clear_region_until_new_line() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines(vec![
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
]);
backend.set_cursor(3, 0).unwrap();
backend.clear_region(ClearType::UntilNewLine).unwrap();
backend.assert_buffer(&Buffer::with_lines(vec![
"aaa ",
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
]));
}
#[test]
fn append_lines_not_at_last_line() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines(vec![
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
"dddddddddd",
"eeeeeeeeee",
]);
backend.set_cursor(0, 0).unwrap();
// If the cursor is not at the last line in the terminal the addition of a
// newline simply moves the cursor down and to the right
backend.append_lines(1).unwrap();
assert_eq!(backend.get_cursor().unwrap(), (1, 1));
backend.append_lines(1).unwrap();
assert_eq!(backend.get_cursor().unwrap(), (2, 2));
backend.append_lines(1).unwrap();
assert_eq!(backend.get_cursor().unwrap(), (3, 3));
backend.append_lines(1).unwrap();
assert_eq!(backend.get_cursor().unwrap(), (4, 4));
// As such the buffer should remain unchanged
backend.assert_buffer(&Buffer::with_lines(vec![
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
"dddddddddd",
"eeeeeeeeee",
]));
}
#[test]
fn append_lines_at_last_line() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines(vec![
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
"dddddddddd",
"eeeeeeeeee",
]);
// If the cursor is at the last line in the terminal the addition of a
// newline will scroll the contents of the buffer
backend.set_cursor(0, 4).unwrap();
backend.append_lines(1).unwrap();
backend.buffer = Buffer::with_lines(vec![
"bbbbbbbbbb",
"cccccccccc",
"dddddddddd",
"eeeeeeeeee",
" ",
]);
// It also moves the cursor to the right, as is common of the behaviour of
// terminals in raw-mode
assert_eq!(backend.get_cursor().unwrap(), (1, 4));
}
#[test]
fn append_multiple_lines_not_at_last_line() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines(vec![
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
"dddddddddd",
"eeeeeeeeee",
]);
backend.set_cursor(0, 0).unwrap();
// If the cursor is not at the last line in the terminal the addition of multiple
// newlines simply moves the cursor n lines down and to the right by 1
backend.append_lines(4).unwrap();
assert_eq!(backend.get_cursor().unwrap(), (1, 4));
// As such the buffer should remain unchanged
backend.assert_buffer(&Buffer::with_lines(vec![
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
"dddddddddd",
"eeeeeeeeee",
]));
}
#[test]
fn append_multiple_lines_past_last_line() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines(vec![
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
"dddddddddd",
"eeeeeeeeee",
]);
backend.set_cursor(0, 3).unwrap();
backend.append_lines(3).unwrap();
assert_eq!(backend.get_cursor().unwrap(), (1, 4));
backend.assert_buffer(&Buffer::with_lines(vec![
"cccccccccc",
"dddddddddd",
"eeeeeeeeee",
" ",
" ",
]));
}
#[test]
fn append_multiple_lines_where_cursor_at_end_appends_height_lines() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines(vec![
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
"dddddddddd",
"eeeeeeeeee",
]);
backend.set_cursor(0, 4).unwrap();
backend.append_lines(5).unwrap();
assert_eq!(backend.get_cursor().unwrap(), (1, 4));
backend.assert_buffer(&Buffer::with_lines(vec![
" ",
" ",
" ",
" ",
" ",
]));
}
#[test]
fn append_multiple_lines_where_cursor_appends_height_lines() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines(vec![
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
"dddddddddd",
"eeeeeeeeee",
]);
backend.set_cursor(0, 0).unwrap();
backend.append_lines(5).unwrap();
assert_eq!(backend.get_cursor().unwrap(), (1, 4));
backend.assert_buffer(&Buffer::with_lines(vec![
"bbbbbbbbbb",
"cccccccccc",
"dddddddddd",
"eeeeeeeeee",
" ",
]));
}
#[test]

File diff suppressed because it is too large Load Diff

82
src/buffer/assert.rs Normal file
View File

@@ -0,0 +1,82 @@
/// Assert that two buffers are equal by comparing their areas and content.
///
/// On panic, displays the areas or the content and a diff of the contents.
#[macro_export]
macro_rules! assert_buffer_eq {
($actual_expr:expr, $expected_expr:expr) => {
match (&$actual_expr, &$expected_expr) {
(actual, expected) => {
if actual.area != expected.area {
panic!(
indoc::indoc!(
"
buffer areas not equal
expected: {:?}
actual: {:?}"
),
expected, actual
);
}
let diff = expected.diff(&actual);
if !diff.is_empty() {
let nice_diff = diff
.iter()
.enumerate()
.map(|(i, (x, y, cell))| {
let expected_cell = expected.get(*x, *y);
indoc::formatdoc! {"
{i}: at ({x}, {y})
expected: {expected_cell:?}
actual: {cell:?}
"}
})
.collect::<Vec<String>>()
.join("\n");
panic!(
indoc::indoc!(
"
buffer contents not equal
expected: {:?}
actual: {:?}
diff:
{}"
),
expected, actual, nice_diff
);
}
// shouldn't get here, but this guards against future behavior
// that changes equality but not area or content
assert_eq!(actual, expected, "buffers not equal");
}
}
};
}
#[cfg(test)]
mod tests {
use crate::prelude::*;
#[test]
fn assert_buffer_eq_does_not_panic_on_equal_buffers() {
let buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
let other_buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
assert_buffer_eq!(buffer, other_buffer);
}
#[should_panic]
#[test]
fn assert_buffer_eq_panics_on_unequal_area() {
let buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
let other_buffer = Buffer::empty(Rect::new(0, 0, 6, 1));
assert_buffer_eq!(buffer, other_buffer);
}
#[should_panic]
#[test]
fn assert_buffer_eq_panics_on_unequal_style() {
let buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
let mut other_buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
other_buffer.set_string(0, 0, " ", Style::default().fg(Color::Red));
assert_buffer_eq!(buffer, other_buffer);
}
}

891
src/buffer/buffer.rs Normal file
View File

@@ -0,0 +1,891 @@
use std::{
cmp::min,
fmt::{Debug, Formatter, Result},
};
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use crate::{buffer::Cell, prelude::*};
/// A buffer that maps to the desired content of the terminal after the draw call
///
/// No widget in the library interacts directly with the terminal. Instead each of them is required
/// to draw their state to an intermediate buffer. It is basically a grid where each cell contains
/// a grapheme, a foreground color and a background color. This grid will then be used to output
/// the appropriate escape sequences and characters to draw the UI as the user has defined it.
///
/// # Examples:
///
/// ```
/// use ratatui::{buffer::Cell, prelude::*};
///
/// let mut buf = Buffer::empty(Rect {
/// x: 0,
/// y: 0,
/// width: 10,
/// height: 5,
/// });
/// buf.get_mut(0, 2).set_symbol("x");
/// assert_eq!(buf.get(0, 2).symbol(), "x");
///
/// buf.set_string(
/// 3,
/// 0,
/// "string",
/// Style::default().fg(Color::Red).bg(Color::White),
/// );
/// let cell = buf.get_mut(5, 0);
/// assert_eq!(cell.symbol(), "r");
/// assert_eq!(cell.fg, Color::Red);
/// assert_eq!(cell.bg, Color::White);
///
/// 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,
/// The content of the buffer. The length of this Vec should always be equal to area.width *
/// area.height
pub content: Vec<Cell>,
}
impl Buffer {
/// Returns a Buffer with all cells set to the default one
pub fn empty(area: Rect) -> Buffer {
let cell = Cell::default();
Buffer::filled(area, &cell)
}
/// Returns a Buffer with all cells initialized with the attributes of the given Cell
pub fn filled(area: Rect, cell: &Cell) -> Buffer {
let size = area.area() as usize;
let mut content = Vec::with_capacity(size);
for _ in 0..size {
content.push(cell.clone());
}
Buffer { area, content }
}
/// Returns a Buffer containing the given lines
pub fn with_lines<'a, S>(lines: Vec<S>) -> Buffer
where
S: Into<Line<'a>>,
{
let lines = lines.into_iter().map(Into::into).collect::<Vec<_>>();
let height = lines.len() as u16;
let width = lines.iter().map(Line::width).max().unwrap_or_default() as u16;
let mut buffer = Buffer::empty(Rect::new(0, 0, width, height));
for (y, line) in lines.iter().enumerate() {
buffer.set_line(0, y as u16, line, width);
}
buffer
}
/// Returns the content of the buffer as a slice
pub fn content(&self) -> &[Cell] {
&self.content
}
/// Returns the area covered by this buffer
pub fn area(&self) -> &Rect {
&self.area
}
/// Returns a reference to Cell at the given coordinates
pub fn get(&self, x: u16, y: u16) -> &Cell {
let i = self.index_of(x, y);
&self.content[i]
}
/// Returns a mutable reference to Cell at the given coordinates
pub fn get_mut(&mut self, x: u16, y: u16) -> &mut Cell {
let i = self.index_of(x, y);
&mut self.content[i]
}
/// Returns the index in the `Vec<Cell>` for the given global (x, y) coordinates.
///
/// Global coordinates are offset by the Buffer's area offset (`x`/`y`).
///
/// # Examples
///
/// ```
/// # use ratatui::prelude::*;
/// let rect = Rect::new(200, 100, 10, 10);
/// let buffer = Buffer::empty(rect);
/// // Global coordinates to the top corner of this buffer's area
/// assert_eq!(buffer.index_of(200, 100), 0);
/// ```
///
/// # Panics
///
/// Panics when given an coordinate that is outside of this Buffer's area.
///
/// ```should_panic
/// # use ratatui::prelude::*;
/// let rect = Rect::new(200, 100, 10, 10);
/// let buffer = Buffer::empty(rect);
/// // Top coordinate is outside of the buffer in global coordinate space, as the Buffer's area
/// // starts at (200, 100).
/// buffer.index_of(0, 0); // Panics
/// ```
pub fn index_of(&self, x: u16, y: u16) -> usize {
debug_assert!(
x >= self.area.left()
&& x < self.area.right()
&& y >= self.area.top()
&& y < self.area.bottom(),
"Trying to access position outside the buffer: x={x}, y={y}, area={:?}",
self.area
);
((y - self.area.y) * self.area.width + (x - self.area.x)) as usize
}
/// Returns the (global) coordinates of a cell given its index
///
/// Global coordinates are offset by the Buffer's area offset (`x`/`y`).
///
/// # Examples
///
/// ```
/// # use ratatui::prelude::*;
/// let rect = Rect::new(200, 100, 10, 10);
/// let buffer = Buffer::empty(rect);
/// assert_eq!(buffer.pos_of(0), (200, 100));
/// assert_eq!(buffer.pos_of(14), (204, 101));
/// ```
///
/// # Panics
///
/// Panics when given an index that is outside the Buffer's content.
///
/// ```should_panic
/// # use ratatui::prelude::*;
/// let rect = Rect::new(0, 0, 10, 10); // 100 cells in total
/// let buffer = Buffer::empty(rect);
/// // Index 100 is the 101th cell, which lies outside of the area of this Buffer.
/// buffer.pos_of(100); // Panics
/// ```
pub fn pos_of(&self, i: usize) -> (u16, u16) {
debug_assert!(
i < self.content.len(),
"Trying to get the coords of a cell outside the buffer: i={i} len={}",
self.content.len()
);
(
self.area.x + (i as u16) % self.area.width,
self.area.y + (i as u16) / self.area.width,
)
}
/// Print a string, starting at the position (x, y)
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
pub fn set_string<T, S>(&mut self, x: u16, y: u16, string: T, style: S)
where
T: AsRef<str>,
S: Into<Style>,
{
self.set_stringn(x, y, string, usize::MAX, style.into());
}
/// Print at most the first n characters of a string if enough space is available
/// until the end of the line
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
pub fn set_stringn<T, S>(
&mut self,
x: u16,
y: u16,
string: T,
width: usize,
style: S,
) -> (u16, u16)
where
T: AsRef<str>,
S: Into<Style>,
{
let style = style.into();
let mut index = self.index_of(x, y);
let mut x_offset = x as usize;
let graphemes = UnicodeSegmentation::graphemes(string.as_ref(), true);
let max_offset = min(self.area.right() as usize, width.saturating_add(x as usize));
for s in graphemes {
let width = s.width();
if width == 0 {
continue;
}
// `x_offset + width > max_offset` could be integer overflow on 32-bit machines if we
// change dimensions to usize or u32 and someone resizes the terminal to 1x2^32.
if width > max_offset.saturating_sub(x_offset) {
break;
}
self.content[index].set_symbol(s);
self.content[index].set_style(style);
// Reset following cells if multi-width (they would be hidden by the grapheme),
for i in index + 1..index + width {
self.content[i].reset();
}
index += width;
x_offset += width;
}
(x_offset as u16, y)
}
/// Print a line, starting at the position (x, y)
pub fn set_line(&mut self, x: u16, y: u16, line: &Line<'_>, width: u16) -> (u16, u16) {
let mut remaining_width = width;
let mut x = x;
for span in &line.spans {
if remaining_width == 0 {
break;
}
let pos = self.set_stringn(
x,
y,
span.content.as_ref(),
remaining_width as usize,
span.style,
);
let w = pos.0.saturating_sub(x);
x = pos.0;
remaining_width = remaining_width.saturating_sub(w);
}
(x, y)
}
/// Print a span, starting at the position (x, y)
pub fn set_span(&mut self, x: u16, y: u16, span: &Span<'_>, width: u16) -> (u16, u16) {
self.set_stringn(x, y, span.content.as_ref(), width as usize, span.style)
}
/// Set the style of all cells in the given area.
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
pub fn set_style<S: Into<Style>>(&mut self, area: Rect, style: S) {
let style = style.into();
let area = self.area.intersection(area);
for y in area.top()..area.bottom() {
for x in area.left()..area.right() {
self.get_mut(x, y).set_style(style);
}
}
}
/// Resize the buffer so that the mapped area matches the given area and that the buffer
/// length is equal to area.width * area.height
pub fn resize(&mut self, area: Rect) {
let length = area.area() as usize;
if self.content.len() > length {
self.content.truncate(length);
} else {
self.content.resize(length, Cell::default());
}
self.area = area;
}
/// Reset all cells in the buffer
pub fn reset(&mut self) {
for c in &mut self.content {
c.reset();
}
}
/// Merge an other buffer into this one
pub fn merge(&mut self, other: &Buffer) {
let area = self.area.union(other.area);
let cell = Cell::default();
self.content.resize(area.area() as usize, cell.clone());
// Move original content to the appropriate space
let size = self.area.area() as usize;
for i in (0..size).rev() {
let (x, y) = self.pos_of(i);
// New index in content
let k = ((y - area.y) * area.width + x - area.x) as usize;
if i != k {
self.content[k] = self.content[i].clone();
self.content[i] = cell.clone();
}
}
// Push content of the other buffer into this one (may erase previous
// data)
let size = other.area.area() as usize;
for i in 0..size {
let (x, y) = other.pos_of(i);
// New index in content
let k = ((y - area.y) * area.width + x - area.x) as usize;
self.content[k] = other.content[i].clone();
}
self.area = area;
}
/// Builds a minimal sequence of coordinates and Cells necessary to update the UI from
/// self to other.
///
/// We're assuming that buffers are well-formed, that is no double-width cell is followed by
/// a non-blank cell.
///
/// # Multi-width characters handling:
///
/// ```text
/// (Index:) `01`
/// Prev: `コ`
/// Next: `aa`
/// Updates: `0: a, 1: a'
/// ```
///
/// ```text
/// (Index:) `01`
/// Prev: `a `
/// Next: `コ`
/// Updates: `0: コ` (double width symbol at index 0 - skip index 1)
/// ```
///
/// ```text
/// (Index:) `012`
/// Prev: `aaa`
/// Next: `aコ`
/// Updates: `0: a, 1: コ` (double width symbol at index 1 - skip index 2)
/// ```
pub fn diff<'a>(&self, other: &'a Buffer) -> Vec<(u16, u16, &'a Cell)> {
let previous_buffer = &self.content;
let next_buffer = &other.content;
let mut updates: Vec<(u16, u16, &Cell)> = vec![];
// Cells invalidated by drawing/replacing preceding multi-width characters:
let mut invalidated: usize = 0;
// Cells from the current buffer to skip due to preceding multi-width characters taking
// their place (the skipped cells should be blank anyway), or due to per-cell-skipping:
let mut to_skip: usize = 0;
for (i, (current, previous)) in next_buffer.iter().zip(previous_buffer.iter()).enumerate() {
if !current.skip && (current != previous || invalidated > 0) && to_skip == 0 {
let (x, y) = self.pos_of(i);
updates.push((x, y, &next_buffer[i]));
}
to_skip = current.symbol().width().saturating_sub(1);
let affected_width = std::cmp::max(current.symbol().width(), previous.symbol().width());
invalidated = std::cmp::max(affected_width, invalidated).saturating_sub(1);
}
updates
}
}
impl Debug for Buffer {
/// Writes a debug representation of the buffer to the given formatter.
///
/// The format is like a pretty printed struct, with the following fields:
/// * `area`: displayed as `Rect { x: 1, y: 2, width: 3, height: 4 }`
/// * `content`: displayed as a list of strings representing the content of the buffer
/// * `styles`: displayed as a list of: `{ x: 1, y: 2, fg: Color::Red, bg: Color::Blue,
/// modifier: Modifier::BOLD }` only showing a value when there is a change in style.
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
f.write_fmt(format_args!(
"Buffer {{\n area: {:?},\n content: [\n",
&self.area
))?;
let mut last_style = None;
let mut styles = vec![];
for (y, line) in self.content.chunks(self.area.width as usize).enumerate() {
let mut overwritten = vec![];
let mut skip: usize = 0;
f.write_str(" \"")?;
for (x, c) in line.iter().enumerate() {
if skip == 0 {
f.write_str(c.symbol())?;
} else {
overwritten.push((x, c.symbol()));
}
skip = std::cmp::max(skip, c.symbol().width()).saturating_sub(1);
#[cfg(feature = "underline-color")]
{
let style = (c.fg, c.bg, c.underline_color, c.modifier);
if last_style != Some(style) {
last_style = Some(style);
styles.push((x, y, c.fg, c.bg, c.underline_color, c.modifier));
}
}
#[cfg(not(feature = "underline-color"))]
{
let style = (c.fg, c.bg, c.modifier);
if last_style != Some(style) {
last_style = Some(style);
styles.push((x, y, c.fg, c.bg, c.modifier));
}
}
}
if !overwritten.is_empty() {
f.write_fmt(format_args!(
"// hidden by multi-width symbols: {overwritten:?}"
))?;
}
f.write_str("\",\n")?;
}
f.write_str(" ],\n styles: [\n")?;
for s in styles {
#[cfg(feature = "underline-color")]
f.write_fmt(format_args!(
" x: {}, y: {}, fg: {:?}, bg: {:?}, underline: {:?}, modifier: {:?},\n",
s.0, s.1, s.2, s.3, s.4, s.5
))?;
#[cfg(not(feature = "underline-color"))]
f.write_fmt(format_args!(
" x: {}, y: {}, fg: {:?}, bg: {:?}, modifier: {:?},\n",
s.0, s.1, s.2, s.3, s.4
))?;
}
f.write_str(" ]\n}")?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::assert_buffer_eq;
fn cell(s: &str) -> Cell {
let mut cell = Cell::default();
cell.set_symbol(s);
cell
}
#[test]
fn debug() {
let mut buf = Buffer::empty(Rect::new(0, 0, 12, 2));
buf.set_string(0, 0, "Hello World!", Style::default());
buf.set_string(
0,
1,
"G'day World!",
Style::default()
.fg(Color::Green)
.bg(Color::Yellow)
.add_modifier(Modifier::BOLD),
);
#[cfg(feature = "underline-color")]
assert_eq!(
format!("{buf:?}"),
indoc::indoc!(
"
Buffer {
area: Rect { x: 0, y: 0, width: 12, height: 2 },
content: [
\"Hello World!\",
\"G'day World!\",
],
styles: [
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
x: 0, y: 1, fg: Green, bg: Yellow, underline: Reset, modifier: BOLD,
]
}"
)
);
#[cfg(not(feature = "underline-color"))]
assert_eq!(
format!("{buf:?}"),
indoc::indoc!(
"
Buffer {
area: Rect { x: 0, y: 0, width: 12, height: 2 },
content: [
\"Hello World!\",
\"G'day World!\",
],
styles: [
x: 0, y: 0, fg: Reset, bg: Reset, modifier: NONE,
x: 0, y: 1, fg: Green, bg: Yellow, modifier: BOLD,
]
}"
)
);
}
#[test]
fn it_translates_to_and_from_coordinates() {
let rect = Rect::new(200, 100, 50, 80);
let buf = Buffer::empty(rect);
// First cell is at the upper left corner.
assert_eq!(buf.pos_of(0), (200, 100));
assert_eq!(buf.index_of(200, 100), 0);
// Last cell is in the lower right.
assert_eq!(buf.pos_of(buf.content.len() - 1), (249, 179));
assert_eq!(buf.index_of(249, 179), buf.content.len() - 1);
}
#[test]
#[should_panic(expected = "outside the buffer")]
fn pos_of_panics_on_out_of_bounds() {
let rect = Rect::new(0, 0, 10, 10);
let buf = Buffer::empty(rect);
// There are a total of 100 cells; zero-indexed means that 100 would be the 101st cell.
buf.pos_of(100);
}
#[test]
#[should_panic(expected = "outside the buffer")]
fn index_of_panics_on_out_of_bounds() {
let rect = Rect::new(0, 0, 10, 10);
let buf = Buffer::empty(rect);
// width is 10; zero-indexed means that 10 would be the 11th cell.
buf.index_of(10, 0);
}
#[test]
fn set_string() {
let area = Rect::new(0, 0, 5, 1);
let mut buffer = Buffer::empty(area);
// Zero-width
buffer.set_stringn(0, 0, "aaa", 0, Style::default());
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" "]));
buffer.set_string(0, 0, "aaa", Style::default());
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["aaa "]));
// Width limit:
buffer.set_stringn(0, 0, "bbbbbbbbbbbbbb", 4, Style::default());
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["bbbb "]));
buffer.set_string(0, 0, "12345", Style::default());
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["12345"]));
// Width truncation:
buffer.set_string(0, 0, "123456", Style::default());
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["12345"]));
// multi-line
buffer = Buffer::empty(Rect::new(0, 0, 5, 2));
buffer.set_string(0, 0, "12345", Style::default());
buffer.set_string(0, 1, "67890", Style::default());
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["12345", "67890"]));
}
#[test]
fn set_string_multi_width_overwrite() {
let area = Rect::new(0, 0, 5, 1);
let mut buffer = Buffer::empty(area);
// multi-width overwrite
buffer.set_string(0, 0, "aaaaa", Style::default());
buffer.set_string(0, 0, "称号", Style::default());
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["称号a"]));
}
#[test]
fn set_string_zero_width() {
let area = Rect::new(0, 0, 1, 1);
let mut buffer = Buffer::empty(area);
// Leading grapheme with zero width
let s = "\u{1}a";
buffer.set_stringn(0, 0, s, 1, Style::default());
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["a"]));
// Trailing grapheme with zero with
let s = "a\u{1}";
buffer.set_stringn(0, 0, s, 1, Style::default());
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["a"]));
}
#[test]
fn set_string_double_width() {
let area = Rect::new(0, 0, 5, 1);
let mut buffer = Buffer::empty(area);
buffer.set_string(0, 0, "コン", Style::default());
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["コン "]));
// Only 1 space left.
buffer.set_string(0, 0, "コンピ", Style::default());
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["コン "]));
}
#[test]
fn set_style() {
let mut buffer = Buffer::with_lines(vec!["aaaaa", "bbbbb", "ccccc"]);
buffer.set_style(Rect::new(0, 1, 5, 1), Style::new().red());
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec!["aaaaa".into(), "bbbbb".red(), "ccccc".into(),])
);
}
#[test]
fn set_style_does_not_panic_when_out_of_area() {
let mut buffer = Buffer::with_lines(vec!["aaaaa", "bbbbb", "ccccc"]);
buffer.set_style(Rect::new(0, 1, 10, 3), Style::new().red());
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec!["aaaaa".into(), "bbbbb".red(), "ccccc".red(),])
);
}
#[test]
fn with_lines() {
let buffer =
Buffer::with_lines(vec!["┌────────┐", "│コンピュ│", "│ーa 上で│", "└────────┘"]);
assert_eq!(buffer.area.x, 0);
assert_eq!(buffer.area.y, 0);
assert_eq!(buffer.area.width, 10);
assert_eq!(buffer.area.height, 4);
}
#[test]
fn diff_empty_empty() {
let area = Rect::new(0, 0, 40, 40);
let prev = Buffer::empty(area);
let next = Buffer::empty(area);
let diff = prev.diff(&next);
assert_eq!(diff, vec![]);
}
#[test]
fn diff_empty_filled() {
let area = Rect::new(0, 0, 40, 40);
let prev = Buffer::empty(area);
let next = Buffer::filled(area, Cell::default().set_symbol("a"));
let diff = prev.diff(&next);
assert_eq!(diff.len(), 40 * 40);
}
#[test]
fn diff_filled_filled() {
let area = Rect::new(0, 0, 40, 40);
let prev = Buffer::filled(area, Cell::default().set_symbol("a"));
let next = Buffer::filled(area, Cell::default().set_symbol("a"));
let diff = prev.diff(&next);
assert_eq!(diff, vec![]);
}
#[test]
fn diff_single_width() {
let prev = Buffer::with_lines(vec![
" ",
"┌Title─┐ ",
"│ │ ",
"│ │ ",
"└──────┘ ",
]);
let next = Buffer::with_lines(vec![
" ",
"┌TITLE─┐ ",
"│ │ ",
"│ │ ",
"└──────┘ ",
]);
let diff = prev.diff(&next);
assert_eq!(
diff,
vec![
(2, 1, &cell("I")),
(3, 1, &cell("T")),
(4, 1, &cell("L")),
(5, 1, &cell("E")),
]
);
}
#[test]
#[rustfmt::skip]
fn diff_multi_width() {
let prev = Buffer::with_lines(vec![
"┌Title─┐ ",
"└──────┘ ",
]);
let next = Buffer::with_lines(vec![
"┌称号──┐ ",
"└──────┘ ",
]);
let diff = prev.diff(&next);
assert_eq!(
diff,
vec![
(1, 0, &cell("")),
// Skipped "i"
(3, 0, &cell("")),
// Skipped "l"
(5, 0, &cell("")),
]
);
}
#[test]
fn diff_multi_width_offset() {
let prev = Buffer::with_lines(vec!["┌称号──┐"]);
let next = Buffer::with_lines(vec!["┌─称号─┐"]);
let diff = prev.diff(&next);
assert_eq!(
diff,
vec![(1, 0, &cell("")), (2, 0, &cell("")), (4, 0, &cell("")),]
);
}
#[test]
fn diff_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 merge() {
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: 2,
width: 2,
height: 2,
},
Cell::default().set_symbol("2"),
);
one.merge(&two);
assert_buffer_eq!(one, Buffer::with_lines(vec!["11", "11", "22", "22"]));
}
#[test]
fn merge2() {
let mut one = Buffer::filled(
Rect {
x: 2,
y: 2,
width: 2,
height: 2,
},
Cell::default().set_symbol("1"),
);
let two = Buffer::filled(
Rect {
x: 0,
y: 0,
width: 2,
height: 2,
},
Cell::default().set_symbol("2"),
);
one.merge(&two);
assert_buffer_eq!(
one,
Buffer::with_lines(vec!["22 ", "22 ", " 11", " 11"])
);
}
#[test]
fn merge3() {
let mut one = Buffer::filled(
Rect {
x: 3,
y: 3,
width: 2,
height: 2,
},
Cell::default().set_symbol("1"),
);
let two = Buffer::filled(
Rect {
x: 1,
y: 1,
width: 3,
height: 4,
},
Cell::default().set_symbol("2"),
);
one.merge(&two);
let mut merged = Buffer::with_lines(vec!["222 ", "222 ", "2221", "2221"]);
merged.area = Rect {
x: 1,
y: 1,
width: 4,
height: 4,
};
assert_buffer_eq!(one, merged);
}
#[test]
fn 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 merge_skip2() {
let mut one = Buffer::filled(
Rect {
x: 0,
y: 0,
width: 2,
height: 2,
},
Cell::default().set_symbol("1").set_skip(true),
);
let two = Buffer::filled(
Rect {
x: 0,
y: 1,
width: 2,
height: 2,
},
Cell::default().set_symbol("2"),
);
one.merge(&two);
let skipped: Vec<bool> = one.content().iter().map(|c| c.skip).collect();
assert_eq!(skipped, vec![true, true, false, false, false, false]);
}
#[test]
fn with_lines_accepts_into_lines() {
use crate::style::Stylize;
let mut buf = Buffer::empty(Rect::new(0, 0, 3, 2));
buf.set_string(0, 0, "foo", Style::new().red());
buf.set_string(0, 1, "bar", Style::new().blue());
assert_eq!(buf, Buffer::with_lines(vec!["foo".red(), "bar".blue()]));
}
}

160
src/buffer/cell.rs Normal file
View File

@@ -0,0 +1,160 @@
use std::fmt::Debug;
use crate::prelude::*;
/// A buffer cell
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Cell {
#[deprecated(
since = "0.24.1",
note = "This field will be hidden at next major version. Use `Cell::symbol` method to get \
the value. Use `Cell::set_symbol` to update the field. Use `Cell::default` to \
create `Cell` instance"
)]
/// The string to be drawn in the cell.
///
/// This accepts unicode grapheme clusters which might take up more than one cell.
pub symbol: String,
/// The foreground color of the cell.
pub fg: Color,
/// The background color of the cell.
pub bg: Color,
/// The underline color of the cell.
///
/// This is only used when the `underline-color` feature is enabled.
#[cfg(feature = "underline-color")]
pub underline_color: Color,
/// The modifier of the cell.
pub modifier: Modifier,
/// Whether the cell should be skipped when copying (diffing) the buffer to the screen.
pub skip: bool,
}
#[allow(deprecated)] // For Cell::symbol
impl Cell {
/// Gets the symbol of the cell.
pub fn symbol(&self) -> &str {
self.symbol.as_str()
}
/// Sets the symbol of the cell.
pub fn set_symbol(&mut self, symbol: &str) -> &mut Cell {
self.symbol.clear();
self.symbol.push_str(symbol);
self
}
/// Sets the symbol of the cell to a single character.
pub fn set_char(&mut self, ch: char) -> &mut Cell {
self.symbol.clear();
self.symbol.push(ch);
self
}
/// Sets the foreground color of the cell.
pub fn set_fg(&mut self, color: Color) -> &mut Cell {
self.fg = color;
self
}
/// Sets the background color of the cell.
pub fn set_bg(&mut self, color: Color) -> &mut Cell {
self.bg = color;
self
}
/// Sets the style of the cell.
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
pub fn set_style<S: Into<Style>>(&mut self, style: S) -> &mut Cell {
let style = style.into();
if let Some(c) = style.fg {
self.fg = c;
}
if let Some(c) = style.bg {
self.bg = c;
}
#[cfg(feature = "underline-color")]
if let Some(c) = style.underline_color {
self.underline_color = c;
}
self.modifier.insert(style.add_modifier);
self.modifier.remove(style.sub_modifier);
self
}
/// Returns the style of the cell.
pub fn style(&self) -> Style {
#[cfg(feature = "underline-color")]
return Style::default()
.fg(self.fg)
.bg(self.bg)
.underline_color(self.underline_color)
.add_modifier(self.modifier);
#[cfg(not(feature = "underline-color"))]
return Style::default()
.fg(self.fg)
.bg(self.bg)
.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
}
/// Resets the cell to the default state.
pub fn reset(&mut self) {
self.symbol.clear();
self.symbol.push(' ');
self.fg = Color::Reset;
self.bg = Color::Reset;
#[cfg(feature = "underline-color")]
{
self.underline_color = Color::Reset;
}
self.modifier = Modifier::empty();
self.skip = false;
}
}
impl Default for Cell {
fn default() -> Cell {
#[allow(deprecated)] // For Cell::symbol
Cell {
symbol: " ".into(),
fg: Color::Reset,
bg: Color::Reset,
#[cfg(feature = "underline-color")]
underline_color: Color::Reset,
modifier: Modifier::empty(),
skip: false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn symbol_field() {
let mut cell = Cell::default();
assert_eq!(cell.symbol(), " ");
cell.set_symbol(""); // Multi-byte character
assert_eq!(cell.symbol(), "");
cell.set_symbol("👨‍👩‍👧‍👦"); // Multiple code units combined with ZWJ
assert_eq!(cell.symbol(), "👨‍👩‍👧‍👦");
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,10 @@ use std::{
use crate::prelude::*;
mod offset;
pub use offset::*;
/// A simple rectangle used in the computation of the layout and to give widgets a hint about the
/// area they are supposed to render to.
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
@@ -106,6 +110,26 @@ impl Rect {
}
}
/// Moves the `Rect` without modifying its size.
///
/// Moves the `Rect` according to the given offset without modifying its [`width`](Rect::width)
/// or [`height`](Rect::height).
/// - Positive `x` moves the whole `Rect` to the right, negative to the left.
/// - Positive `y` moves the whole `Rect` to the bottom, negative to the top.
///
/// See [`Offset`] for details.
pub fn offset(self, offset: Offset) -> Rect {
Rect {
x: i32::from(self.x)
.saturating_add(offset.x)
.clamp(0, (u16::MAX - self.width) as i32) as u16,
y: i32::from(self.y)
.saturating_add(offset.y)
.clamp(0, (u16::MAX - self.height) as i32) as u16,
..self
}
}
/// Returns a new rect that contains both the current one and the given one.
pub fn union(self, other: Rect) -> Rect {
let x1 = min(self.x, other.x);
@@ -131,8 +155,8 @@ impl Rect {
Rect {
x: x1,
y: y1,
width: x2 - x1,
height: y2 - y1,
width: x2.saturating_sub(x1),
height: y2.saturating_sub(y1),
}
}
@@ -207,6 +231,39 @@ mod tests {
);
}
#[test]
fn offset() {
assert_eq!(
Rect::new(1, 2, 3, 4).offset(Offset { x: 5, y: 6 }),
Rect::new(6, 8, 3, 4),
);
}
#[test]
fn negative_offset() {
assert_eq!(
Rect::new(4, 3, 3, 4).offset(Offset { x: -2, y: -1 }),
Rect::new(2, 2, 3, 4),
);
}
#[test]
fn negative_offset_saturate() {
assert_eq!(
Rect::new(1, 2, 3, 4).offset(Offset { x: -5, y: -6 }),
Rect::new(0, 0, 3, 4),
);
}
/// Offsets a [`Rect`] making it go outside [`u16::MAX`], it should keep its size.
#[test]
fn offset_saturate_max() {
assert_eq!(
Rect::new(u16::MAX - 500, u16::MAX - 500, 100, 100).offset(Offset { x: 1000, y: 1000 }),
Rect::new(u16::MAX - 100, u16::MAX - 100, 100, 100),
);
}
#[test]
fn union() {
assert_eq!(
@@ -223,6 +280,14 @@ mod tests {
);
}
#[test]
fn intersection_underflow() {
assert_eq!(
Rect::new(1, 1, 2, 2).intersection(Rect::new(4, 4, 2, 2)),
Rect::new(4, 4, 0, 0)
);
}
#[test]
fn intersects() {
assert!(Rect::new(1, 2, 3, 4).intersects(Rect::new(2, 3, 4, 5)));

12
src/layout/rect/offset.rs Normal file
View File

@@ -0,0 +1,12 @@
/// Amounts by which to move a [`Rect`](super::Rect).
///
/// Positive numbers move to the right/bottom and negative to the left/top.
///
/// See [`Rect::offset`](super::Rect::offset)
#[derive(Debug, Default, Clone, Copy)]
pub struct Offset {
/// How much to move on the X axis
pub x: i32,
/// How much to move on the Y axis
pub y: i32,
}

View File

@@ -4,27 +4,29 @@
//!
//! <div align="center">
//!
//! [![Crate Badge]](https://crates.io/crates/ratatui) [![License Badge]](./LICENSE) [![CI
//! Badge]](https://github.com/ratatui-org/ratatui/actions?query=workflow%3ACI+) [![Docs
//! Badge]](https://docs.rs/crate/ratatui/)<br>
//! [![Dependencies Badge]](https://deps.rs/repo/github/ratatui-org/ratatui) [![Codecov
//! Badge]](https://app.codecov.io/gh/ratatui-org/ratatui) [![Discord
//! Badge]](https://discord.gg/pMCEU9hNEj) [![Matrix
//! Badge]](https://matrix.to/#/#ratatui:matrix.org)<br>
//! [Documentation](https://docs.rs/ratatui) · [Ratatui Book](https://ratatui.rs) ·
//! [Examples](https://github.com/ratatui-org/ratatui/tree/main/examples) · [Report a
//! bug](https://github.com/ratatui-org/ratatui/issues/new?labels=bug&projects=&template=bug_report.md)
//! · [Request a
//! Feature](https://github.com/ratatui-org/ratatui/issues/new?labels=enhancement&projects=&template=feature_request.md)
//! [![Crate Badge]](https://crates.io/crates/ratatui)
//! [![License Badge]](./LICENSE)
//! [![CI Badge]](https://github.com/ratatui-org/ratatui/actions?query=workflow%3ACI+)
//! [![Docs Badge]](https://docs.rs/crate/ratatui/)<br>
//! [![Dependencies Badge]](https://deps.rs/repo/github/ratatui-org/ratatui)
//! [![Codecov Badge]](https://app.codecov.io/gh/ratatui-org/ratatui)
//! [![Discord Badge]](https://discord.gg/pMCEU9hNEj)
//! [![Matrix Badge]](https://matrix.to/#/#ratatui:matrix.org)<br>
//!
//! [Documentation](https://docs.rs/ratatui)
//! · [Ratatui Website](https://ratatui.rs)
//! · [Examples](https://github.com/ratatui-org/ratatui/tree/main/examples)
//! · [Report a bug](https://github.com/ratatui-org/ratatui/issues/new?labels=bug&projects=&template=bug_report.md)
//! · [Request a Feature](https://github.com/ratatui-org/ratatui/issues/new?labels=enhancement&projects=&template=feature_request.md)
//! · [Send a Pull Request](https://github.com/ratatui-org/ratatui/compare)
//!
//! </div>
//!
//! # Ratatui
//!
//! [Ratatui] is a crate for cooking up terminal user interfaces in rust. It is a lightweight
//! library that provides a set of widgets and utilities to build complex rust TUIs. Ratatui was
//! forked from the [Tui-rs crate] in 2023 in order to continue its development.
//! [Ratatui] is a crate for cooking up terminal user interfaces in Rust. It is a lightweight
//! library that provides a set of widgets and utilities to build complex Rust TUIs. Ratatui was
//! forked from the [tui-rs] crate in 2023 in order to continue its development.
//!
//! ## Installation
//!
@@ -35,7 +37,7 @@
//! ```
//!
//! Ratatui uses [Crossterm] by default as it works on most platforms. See the [Installation]
//! section of the [Ratatui Book] for more details on how to use other backends ([Termion] /
//! section of the [Ratatui Website] for more details on how to use other backends ([Termion] /
//! [Termwiz]).
//!
//! ## Introduction
@@ -43,12 +45,12 @@
//! Ratatui is based on the principle of immediate rendering with intermediate buffers. This means
//! that for each frame, your app must render all widgets that are supposed to be part of the UI.
//! This is in contrast to the retained mode style of rendering where widgets are updated and then
//! automatically redrawn on the next frame. See the [Rendering] section of the [Ratatui Book] for
//! more info.
//! automatically redrawn on the next frame. See the [Rendering] section of the [Ratatui Website]
//! for more info.
//!
//! ## Other documentation
//!
//! - [Ratatui Book] - explains the library's concepts and provides step-by-step tutorials
//! - [Ratatui Website] - explains the library's concepts and provides step-by-step tutorials
//! - [Examples] - a collection of examples that demonstrate how to use the library.
//! - [API Documentation] - the full API documentation for the library on docs.rs.
//! - [Changelog] - generated by [git-cliff] utilizing [Conventional Commits].
@@ -60,12 +62,11 @@
//! The following example demonstrates the minimal amount of code necessary to setup a terminal and
//! render "Hello World!". The full code for this example which contains a little more detail is in
//! [hello_world.rs]. For more guidance on different ways to structure your application see the
//! [Application Patterns] and [Hello World tutorial] sections in the [Ratatui Book] and the various
//! [Examples]. There are also several starter templates available:
//! [Application Patterns] and [Hello World tutorial] sections in the [Ratatui Website] and the
//! various [Examples]. There are also several starter templates available:
//!
//! - [rust-tui-template]
//! - [ratatui-async-template] (book and template)
//! - [simple-tui-rs]
//! - [template]
//! - [async-template] (book and template)
//!
//! Every application built with `ratatui` needs to implement the following steps:
//!
@@ -89,30 +90,31 @@
//!
//! Most applications should enter the Alternate Screen when starting and leave it when exiting and
//! also enable raw mode to disable line buffering and enable reading key events. See the [`backend`
//! module] and the [Backends] section of the [Ratatui Book] for more info.
//! module] and the [Backends] section of the [Ratatui Website] for more info.
//!
//! ### Drawing the UI
//!
//! The drawing logic is delegated to a closure that takes a [`Frame`] instance as argument. The
//! [`Frame`] provides the size of the area to draw to and allows the app to render any [`Widget`]
//! using the provided [`render_widget`] method. See the [Widgets] section of the [Ratatui Book] for
//! more info.
//! using the provided [`render_widget`] method. See the [Widgets] section of the [Ratatui Website]
//! for more info.
//!
//! ### Handling events
//!
//! Ratatui does not include any input handling. Instead event handling can be implemented by
//! calling backend library methods directly. See the [Handling Events] section of the [Ratatui
//! Book] for more info. For example, if you are using [Crossterm], you can use the
//! Website] for more info. For example, if you are using [Crossterm], you can use the
//! [`crossterm::event`] module to handle events.
//!
//! ### Example
//!
//! ```rust,no_run
//! use std::io::{self, stdout};
//!
//! use crossterm::{
//! event::{self, Event, KeyCode},
//! terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
//! ExecutableCommand,
//! terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}
//! };
//! use ratatui::{prelude::*, widgets::*};
//!
@@ -138,7 +140,7 @@
//! if key.kind == event::KeyEventKind::Press && key.code == KeyCode::Char('q') {
//! return Ok(true);
//! }
//! }
//! }
//! }
//! Ok(false)
//! }
@@ -161,20 +163,21 @@
//! The library comes with a basic yet useful layout management object called [`Layout`] which
//! allows you to split the available space into multiple areas and then render widgets in each
//! area. This lets you describe a responsive terminal UI by nesting layouts. See the [Layout]
//! section of the [Ratatui Book] for more info.
//! section of the [Ratatui Website] for more info.
//!
//! ```rust,no_run
//! use ratatui::{prelude::*, widgets::*};
//!
//! fn ui(frame: &mut Frame) {
//! let main_layout = Layout::default()
//! .direction(Direction::Vertical)
//! .constraints([
//! let main_layout = Layout::new(
//! Direction::Vertical,
//! [
//! Constraint::Length(1),
//! Constraint::Min(0),
//! Constraint::Length(1),
//! ])
//! .split(frame.size());
//! ],
//! )
//! .split(frame.size());
//! frame.render_widget(
//! Block::new().borders(Borders::TOP).title("Title Bar"),
//! main_layout[0],
@@ -184,10 +187,11 @@
//! main_layout[2],
//! );
//!
//! let inner_layout = Layout::default()
//! .direction(Direction::Horizontal)
//! .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
//! .split(main_layout[1]);
//! let inner_layout = Layout::new(
//! Direction::Horizontal,
//! [Constraint::Percentage(50), Constraint::Percentage(50)],
//! )
//! .split(main_layout[1]);
//! frame.render_widget(
//! Block::default().borders(Borders::ALL).title("Left"),
//! inner_layout[0],
@@ -213,22 +217,23 @@
//! important one is [`Style`] which represents the foreground and background colors and the text
//! attributes of a [`Span`]. The [`style` module] also provides a [`Stylize`] trait that allows
//! short-hand syntax to apply a style to widgets and text. See the [Styling Text] section of the
//! [Ratatui Book] for more info.
//! [Ratatui Website] for more info.
//!
//! ```rust,no_run
//! use ratatui::{prelude::*, widgets::*};
//!
//! fn ui(frame: &mut Frame) {
//! let areas = Layout::default()
//! .direction(Direction::Vertical)
//! .constraints([
//! let areas = Layout::new(
//! Direction::Vertical,
//! [
//! Constraint::Length(1),
//! Constraint::Length(1),
//! Constraint::Length(1),
//! Constraint::Length(1),
//! Constraint::Min(0),
//! ])
//! .split(frame.size());
//! ],
//! )
//! .split(frame.size());
//!
//! let span1 = Span::raw("Hello ");
//! let span2 = Span::styled(
@@ -282,21 +287,20 @@
doc = "[`calendar`]: widgets::calendar::Monthly"
)]
//!
//! [Ratatui Book]: https://ratatui.rs
//! [Installation]: https://ratatui.rs/installation.html
//! [Rendering]: https://ratatui.rs/concepts/rendering/index.html
//! [Application Patterns]: https://ratatui.rs/concepts/application_patterns/index.html
//! [Hello World tutorial]: https://ratatui.rs/tutorial/hello_world.html
//! [Backends]: https://ratatui.rs/concepts/backends/index.html
//! [Widgets]: https://ratatui.rs/how-to/widgets/index.html
//! [Handling Events]: https://ratatui.rs/concepts/event_handling.html
//! [Layout]: https://ratatui.rs/how-to/layout/index.html
//! [Styling Text]: https://ratatui.rs/how-to/render/style-text.html
//! [rust-tui-template]: https://github.com/ratatui-org/rust-tui-template
//! [ratatui-async-template]: https://ratatui-org.github.io/ratatui-async-template/
//! [simple-tui-rs]: https://github.com/pmsanford/simple-tui-rs
//! [Ratatui Website]: https://ratatui.rs/
//! [Installation]: https://ratatui.rs/installation/
//! [Rendering]: https://ratatui.rs/concepts/rendering/
//! [Application Patterns]: https://ratatui.rs/concepts/application-patterns/
//! [Hello World tutorial]: https://ratatui.rs/tutorials/hello-world/
//! [Backends]: https://ratatui.rs/concepts/backends/
//! [Widgets]: https://ratatui.rs/how-to/widgets/
//! [Handling Events]: https://ratatui.rs/concepts/event-handling/
//! [Layout]: https://ratatui.rs/how-to/layout/
//! [Styling Text]: https://ratatui.rs/how-to/render/style-text/
//! [template]: https://github.com/ratatui-org/template
//! [async-template]: https://ratatui-org.github.io/async-template
//! [Examples]: https://github.com/ratatui-org/ratatui/tree/main/examples
//! [git-cliff]: https://github.com/orhun/git-cliff
//! [git-cliff]: https://git-cliff.org
//! [Conventional Commits]: https://www.conventionalcommits.org
//! [API Documentation]: https://docs.rs/ratatui
//! [Changelog]: https://github.com/ratatui-org/ratatui/blob/main/CHANGELOG.md
@@ -322,7 +326,7 @@
//! [Crossterm]: https://crates.io/crates/crossterm
//! [Termion]: https://crates.io/crates/termion
//! [Termwiz]: https://crates.io/crates/termwiz
//! [Tui-rs crate]: https://crates.io/crates/tui
//! [tui-rs]: https://crates.io/crates/tui
//! [hello_world.rs]: https://github.com/ratatui-org/ratatui/blob/main/examples/hello_world.rs
//! [Crate Badge]: https://img.shields.io/crates/v/ratatui?logo=rust&style=flat-square
//! [CI Badge]:

View File

@@ -16,9 +16,9 @@
//! use ratatui::prelude::*;
//!
//! let heading_style = Style::new()
//! .fg(Color::Black)
//! .bg(Color::Green)
//! .add_modifier(Modifier::ITALIC | Modifier::BOLD);
//! .fg(Color::Black)
//! .bg(Color::Green)
//! .add_modifier(Modifier::ITALIC | Modifier::BOLD);
//! let span = Span::styled("hello", heading_style);
//! ```
//!
@@ -44,16 +44,24 @@
//! use ratatui::{prelude::*, widgets::*};
//!
//! assert_eq!(
//! "hello".red().on_blue().bold(),
//! "hello".red().on_blue().bold(),
//! Span::styled(
//! "hello",
//! Style::default().fg(Color::Red).bg(Color::Blue).add_modifier(Modifier::BOLD))
//! Style::default()
//! .fg(Color::Red)
//! .bg(Color::Blue)
//! .add_modifier(Modifier::BOLD)
//! )
//! );
//!
//! assert_eq!(
//! Paragraph::new("hello").red().on_blue().bold(),
//! Paragraph::new("hello")
//! .style(Style::default().fg(Color::Red).bg(Color::Blue).add_modifier(Modifier::BOLD))
//! Paragraph::new("hello").style(
//! Style::default()
//! .fg(Color::Red)
//! .bg(Color::Blue)
//! .add_modifier(Modifier::BOLD)
//! )
//! );
//! ```
//!
@@ -74,6 +82,9 @@ bitflags! {
///
/// They are bitflags so they can easily be composed.
///
/// `From<Modifier> for Style` is implemented so you can use `Modifier` anywhere that accepts
/// `Into<Style>`.
///
/// ## Examples
///
/// ```rust
@@ -113,7 +124,7 @@ impl fmt::Debug for Modifier {
/// Style lets you control the main characteristics of the displayed elements.
///
/// ```rust
/// use ratatui::{prelude::*};
/// use ratatui::prelude::*;
///
/// Style::default()
/// .fg(Color::Black)
@@ -130,23 +141,43 @@ impl fmt::Debug for Modifier {
///
/// For more information about the style shorthands, see the [`Stylize`] trait.
///
/// We implement conversions from [`Color`] and [`Modifier`] to [`Style`] so you can use them
/// anywhere that accepts `Into<Style>`.
///
/// ```rust
/// # use ratatui::prelude::*;
/// Line::styled("hello", Style::new().fg(Color::Red));
/// // simplifies to
/// Line::styled("hello", Color::Red);
///
/// Line::styled("hello", Style::new().add_modifier(Modifier::BOLD));
/// // simplifies to
/// Line::styled("hello", Modifier::BOLD);
/// ```
///
/// Styles represents an incremental change. If you apply the styles S1, S2, S3 to a cell of the
/// terminal buffer, the style of this cell will be the result of the merge of S1, S2 and S3, not
/// just S3.
///
/// ```rust
/// use ratatui::{prelude::*};
/// use ratatui::prelude::*;
///
/// let styles = [
/// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC),
/// Style::default().bg(Color::Red).add_modifier(Modifier::UNDERLINED),
/// Style::default()
/// .fg(Color::Blue)
/// .add_modifier(Modifier::BOLD | Modifier::ITALIC),
/// Style::default()
/// .bg(Color::Red)
/// .add_modifier(Modifier::UNDERLINED),
/// #[cfg(feature = "underline-color")]
/// Style::default().underline_color(Color::Green),
/// Style::default().fg(Color::Yellow).remove_modifier(Modifier::ITALIC),
/// Style::default()
/// .fg(Color::Yellow)
/// .remove_modifier(Modifier::ITALIC),
/// ];
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
/// for style in &styles {
/// buffer.get_mut(0, 0).set_style(*style);
/// buffer.get_mut(0, 0).set_style(*style);
/// }
/// assert_eq!(
/// Style {
@@ -165,15 +196,17 @@ impl fmt::Debug for Modifier {
/// reset all properties until that point use [`Style::reset`].
///
/// ```
/// use ratatui::{prelude::*};
/// use ratatui::prelude::*;
///
/// let styles = [
/// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC),
/// Style::default()
/// .fg(Color::Blue)
/// .add_modifier(Modifier::BOLD | Modifier::ITALIC),
/// Style::reset().fg(Color::Yellow),
/// ];
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
/// for style in &styles {
/// buffer.get_mut(0, 0).set_style(*style);
/// buffer.get_mut(0, 0).set_style(*style);
/// }
/// assert_eq!(
/// Style {
@@ -211,10 +244,11 @@ impl Styled for Style {
*self
}
fn set_style(self, style: Style) -> Self::Item {
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
self.patch(style)
}
}
impl Style {
pub const fn new() -> Style {
Style {
@@ -249,6 +283,7 @@ impl Style {
/// let diff = Style::default().fg(Color::Red);
/// assert_eq!(style.patch(diff), Style::default().fg(Color::Red));
/// ```
#[must_use = "`fg` returns the modified style without modifying the original"]
pub const fn fg(mut self, color: Color) -> Style {
self.fg = Some(color);
self
@@ -264,6 +299,7 @@ impl Style {
/// let diff = Style::default().bg(Color::Red);
/// assert_eq!(style.patch(diff), Style::default().bg(Color::Red));
/// ```
#[must_use = "`bg` returns the modified style without modifying the original"]
pub const fn bg(mut self, color: Color) -> Style {
self.bg = Some(color);
self
@@ -283,11 +319,21 @@ impl Style {
///
/// ```rust
/// # use ratatui::prelude::*;
/// let style = Style::default().underline_color(Color::Blue).add_modifier(Modifier::UNDERLINED);
/// let diff = Style::default().underline_color(Color::Red).add_modifier(Modifier::UNDERLINED);
/// assert_eq!(style.patch(diff), Style::default().underline_color(Color::Red).add_modifier(Modifier::UNDERLINED));
/// let style = Style::default()
/// .underline_color(Color::Blue)
/// .add_modifier(Modifier::UNDERLINED);
/// let diff = Style::default()
/// .underline_color(Color::Red)
/// .add_modifier(Modifier::UNDERLINED);
/// assert_eq!(
/// style.patch(diff),
/// Style::default()
/// .underline_color(Color::Red)
/// .add_modifier(Modifier::UNDERLINED)
/// );
/// ```
#[cfg(feature = "underline-color")]
#[must_use = "`underline_color` returns the modified style without modifying the original"]
pub const fn underline_color(mut self, color: Color) -> Style {
self.underline_color = Some(color);
self
@@ -307,6 +353,7 @@ impl Style {
/// assert_eq!(patched.add_modifier, Modifier::BOLD | Modifier::ITALIC);
/// assert_eq!(patched.sub_modifier, Modifier::empty());
/// ```
#[must_use = "`add_modifier` returns the modified style without modifying the original"]
pub const fn add_modifier(mut self, modifier: Modifier) -> Style {
self.sub_modifier = self.sub_modifier.difference(modifier);
self.add_modifier = self.add_modifier.union(modifier);
@@ -327,6 +374,7 @@ impl Style {
/// assert_eq!(patched.add_modifier, Modifier::BOLD);
/// assert_eq!(patched.sub_modifier, Modifier::ITALIC);
/// ```
#[must_use = "`remove_modifier` returns the modified style without modifying the original"]
pub const fn remove_modifier(mut self, modifier: Modifier) -> Style {
self.add_modifier = self.add_modifier.difference(modifier);
self.sub_modifier = self.sub_modifier.union(modifier);
@@ -336,6 +384,9 @@ impl Style {
/// Results in a combined style that is equivalent to applying the two individual styles to
/// a style one after the other.
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// ## Examples
/// ```
/// # use ratatui::prelude::*;
@@ -344,9 +395,12 @@ impl Style {
/// let combined = style_1.patch(style_2);
/// assert_eq!(
/// Style::default().patch(style_1).patch(style_2),
/// Style::default().patch(combined));
/// Style::default().patch(combined)
/// );
/// ```
pub fn patch(mut self, other: Style) -> Style {
#[must_use = "`patch` returns the modified style without modifying the original"]
pub fn patch<S: Into<Style>>(mut self, other: S) -> Style {
let other = other.into();
self.fg = other.fg.or(self.fg);
self.bg = other.bg.or(self.bg);
@@ -364,6 +418,134 @@ impl Style {
}
}
impl From<Color> for Style {
/// Creates a new `Style` with the given foreground color.
///
/// To specify a foreground and background color, use the `from((fg, bg))` constructor.
///
/// # Example
///
/// ```rust
/// # use ratatui::prelude::*;
/// let style = Style::from(Color::Red);
/// ```
fn from(color: Color) -> Self {
Self::new().fg(color)
}
}
impl From<(Color, Color)> for Style {
/// Creates a new `Style` with the given foreground and background colors.
///
/// # Example
///
/// ```rust
/// # use ratatui::prelude::*;
/// // red foreground, blue background
/// let style = Style::from((Color::Red, Color::Blue));
/// // default foreground, blue background
/// let style = Style::from((Color::Reset, Color::Blue));
/// ```
fn from((fg, bg): (Color, Color)) -> Self {
Self::new().fg(fg).bg(bg)
}
}
impl From<Modifier> for Style {
/// Creates a new `Style` with the given modifier added.
///
/// To specify multiple modifiers, use the `|` operator.
///
/// To specify modifiers to add and remove, use the `from((add_modifier, sub_modifier))`
/// constructor.
///
/// # Example
///
/// ```rust
/// # use ratatui::prelude::*;
/// // add bold and italic
/// let style = Style::from(Modifier::BOLD|Modifier::ITALIC);
fn from(modifier: Modifier) -> Self {
Self::new().add_modifier(modifier)
}
}
impl From<(Modifier, Modifier)> for Style {
/// Creates a new `Style` with the given modifiers added and removed.
///
/// # Example
///
/// ```rust
/// # use ratatui::prelude::*;
/// // add bold and italic, remove dim
/// let style = Style::from((Modifier::BOLD | Modifier::ITALIC, Modifier::DIM));
/// ```
fn from((add_modifier, sub_modifier): (Modifier, Modifier)) -> Self {
Self::new()
.add_modifier(add_modifier)
.remove_modifier(sub_modifier)
}
}
impl From<(Color, Modifier)> for Style {
/// Creates a new `Style` with the given foreground color and modifier added.
///
/// To specify multiple modifiers, use the `|` operator.
///
/// # Example
///
/// ```rust
/// # use ratatui::prelude::*;
/// // red foreground, add bold and italic
/// let style = Style::from((Color::Red, Modifier::BOLD | Modifier::ITALIC));
/// ```
fn from((fg, modifier): (Color, Modifier)) -> Self {
Self::new().fg(fg).add_modifier(modifier)
}
}
impl From<(Color, Color, Modifier)> for Style {
/// Creates a new `Style` with the given foreground and background colors and modifier added.
///
/// To specify multiple modifiers, use the `|` operator.
///
/// # Example
///
/// ```rust
/// # use ratatui::prelude::*;
/// // red foreground, blue background, add bold and italic
/// let style = Style::from((Color::Red, Color::Blue, Modifier::BOLD | Modifier::ITALIC));
/// ```
fn from((fg, bg, modifier): (Color, Color, Modifier)) -> Self {
Self::new().fg(fg).bg(bg).add_modifier(modifier)
}
}
impl From<(Color, Color, Modifier, Modifier)> for Style {
/// Creates a new `Style` with the given foreground and background colors and modifiers added
/// and removed.
///
/// # Example
///
/// ```rust
/// # use ratatui::prelude::*;
/// // red foreground, blue background, add bold and italic, remove dim
/// let style = Style::from((
/// Color::Red,
/// Color::Blue,
/// Modifier::BOLD | Modifier::ITALIC,
/// Modifier::DIM,
/// ));
/// ```
fn from((fg, bg, add_modifier, sub_modifier): (Color, Color, Modifier, Modifier)) -> Self {
Self::new()
.fg(fg)
.bg(bg)
.add_modifier(add_modifier)
.remove_modifier(sub_modifier)
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -625,4 +807,79 @@ mod tests {
// reset
assert_eq!(Style::new().reset(), Style::reset());
}
#[test]
fn from_color() {
assert_eq!(Style::from(Color::Red), Style::new().fg(Color::Red));
}
#[test]
fn from_color_color() {
assert_eq!(
Style::from((Color::Red, Color::Blue)),
Style::new().fg(Color::Red).bg(Color::Blue)
);
}
#[test]
fn from_modifier() {
assert_eq!(
Style::from(Modifier::BOLD | Modifier::ITALIC),
Style::new()
.add_modifier(Modifier::BOLD)
.add_modifier(Modifier::ITALIC)
);
}
#[test]
fn from_modifier_modifier() {
assert_eq!(
Style::from((Modifier::BOLD | Modifier::ITALIC, Modifier::DIM)),
Style::new()
.add_modifier(Modifier::BOLD)
.add_modifier(Modifier::ITALIC)
.remove_modifier(Modifier::DIM)
);
}
#[test]
fn from_color_modifier() {
assert_eq!(
Style::from((Color::Red, Modifier::BOLD | Modifier::ITALIC)),
Style::new()
.fg(Color::Red)
.add_modifier(Modifier::BOLD)
.add_modifier(Modifier::ITALIC)
);
}
#[test]
fn from_color_color_modifier() {
assert_eq!(
Style::from((Color::Red, Color::Blue, Modifier::BOLD | Modifier::ITALIC)),
Style::new()
.fg(Color::Red)
.bg(Color::Blue)
.add_modifier(Modifier::BOLD)
.add_modifier(Modifier::ITALIC)
);
}
#[test]
fn from_color_color_modifier_modifier() {
assert_eq!(
Style::from((
Color::Red,
Color::Blue,
Modifier::BOLD | Modifier::ITALIC,
Modifier::DIM
)),
Style::new()
.fg(Color::Red)
.bg(Color::Blue)
.add_modifier(Modifier::BOLD)
.add_modifier(Modifier::ITALIC)
.remove_modifier(Modifier::DIM)
);
}
}

View File

@@ -35,10 +35,14 @@ use std::{
/// - we support `-` and `_` and ` ` as separators for all colors
/// - we support both `gray` and `grey` spellings
///
/// `From<Color> for Style` is implemented by creating a style with the foreground color set to the
/// given color. This allows you to use colors anywhere that accepts `Into<Style>`.
///
/// # Example
///
/// ```
/// use std::str::FromStr;
///
/// use ratatui::prelude::*;
///
/// assert_eq!(Color::from_str("red"), Ok(Color::Red));
@@ -60,7 +64,7 @@ use std::{
///
/// [ANSI color table]: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub enum Color {
/// Resets the foreground or background color
#[default]
@@ -123,6 +127,17 @@ pub enum Color {
Indexed(u8),
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for Color {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
FromStr::from_str(&s).map_err(serde::de::Error::custom)
}
}
/// Error type indicating a failure to parse a color string.
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub struct ParseColorError;
@@ -147,6 +162,7 @@ impl std::error::Error for ParseColorError {}
///
/// ```
/// use std::str::FromStr;
///
/// use ratatui::prelude::*;
///
/// let color: Color = Color::from_str("blue").unwrap();
@@ -249,6 +265,9 @@ impl Display for Color {
mod tests {
use std::error::Error;
#[cfg(feature = "serde")]
use serde::de::{Deserialize, IntoDeserializer};
use super::*;
#[test]
@@ -359,4 +378,46 @@ mod tests {
assert_eq!(format!("{}", Color::Rgb(255, 0, 0)), "#FF0000");
assert_eq!(format!("{}", Color::Reset), "Reset");
}
#[cfg(feature = "serde")]
#[test]
fn deserialize() -> Result<(), serde::de::value::Error> {
assert_eq!(
Color::Black,
Color::deserialize("Black".into_deserializer())?
);
assert_eq!(
Color::Magenta,
Color::deserialize("magenta".into_deserializer())?
);
assert_eq!(
Color::LightGreen,
Color::deserialize("LightGreen".into_deserializer())?
);
assert_eq!(
Color::White,
Color::deserialize("bright-white".into_deserializer())?
);
assert_eq!(
Color::Indexed(42),
Color::deserialize("42".into_deserializer())?
);
assert_eq!(
Color::Rgb(0, 255, 0),
Color::deserialize("#00ff00".into_deserializer())?
);
Ok(())
}
#[cfg(feature = "serde")]
#[test]
fn deserialize_error() {
let color: Result<_, serde::de::value::Error> =
Color::deserialize("invalid".into_deserializer());
assert!(color.is_err());
let color: Result<_, serde::de::value::Error> =
Color::deserialize("#00000000".into_deserializer());
assert!(color.is_err());
}
}

View File

@@ -13,8 +13,14 @@ use crate::{
pub trait Styled {
type Item;
/// Returns the style of the object.
fn style(&self) -> Style;
fn set_style(self, style: Style) -> Self::Item;
/// Sets the style of the object.
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item;
}
/// Generates two methods for each color, one for setting the foreground color (`red()`, `blue()`,
@@ -40,11 +46,13 @@ macro_rules! color {
( $color:ident ) => {
paste! {
#[doc = "Sets the foreground color to [`" $color "`](Color::" $color:camel ")."]
#[must_use = concat!("`", stringify!($color), "` returns the modified style without modifying the original")]
fn $color(self) -> T {
self.fg(Color::[<$color:camel>])
}
#[doc = "Sets the background color to [`" $color "`](Color::" $color:camel ")."]
#[must_use = concat!("`on_", stringify!($color), "` returns the modified style without modifying the original")]
fn [<on_ $color>](self) -> T {
self.bg(Color::[<$color:camel>])
}
@@ -76,6 +84,7 @@ macro_rules! modifier {
( $modifier:ident ) => {
paste! {
#[doc = "Adds the [`" $modifier:upper "`](Modifier::" $modifier:upper ") modifier."]
#[must_use = concat!("`", stringify!($modifier), "` returns the modified style without modifying the original")]
fn [<$modifier>](self) -> T {
self.add_modifier(Modifier::[<$modifier:upper>])
}
@@ -83,6 +92,7 @@ macro_rules! modifier {
paste! {
#[doc = "Removes the [`" $modifier:upper "`](Modifier::" $modifier:upper ") modifier."]
#[must_use = concat!("`not_", stringify!($modifier), "` returns the modified style without modifying the original")]
fn [<not_ $modifier>](self) -> T {
self.remove_modifier(Modifier::[<$modifier:upper>])
}
@@ -123,13 +133,22 @@ macro_rules! modifier {
/// "world".green().on_yellow().not_bold(),
/// ]);
/// let paragraph = Paragraph::new(line).italic().underlined();
/// let block = Block::default().title("Title").borders(Borders::ALL).on_white().bold();
/// let block = Block::default()
/// .title("Title")
/// .borders(Borders::ALL)
/// .on_white()
/// .bold();
/// ```
pub trait Stylize<'a, T>: Sized {
#[must_use = "`bg` returns the modified style without modifying the original"]
fn bg(self, color: Color) -> T;
#[must_use = "`fg` returns the modified style without modifying the original"]
fn fg<S: Into<Color>>(self, color: S) -> T;
#[must_use = "`reset` returns the modified style without modifying the original"]
fn reset(self) -> T;
#[must_use = "`add_modifier` returns the modified style without modifying the original"]
fn add_modifier(self, modifier: Modifier) -> T;
#[must_use = "`remove_modifier` returns the modified style without modifying the original"]
fn remove_modifier(self, modifier: Modifier) -> T;
color!(black);
@@ -196,7 +215,7 @@ impl<'a> Styled for &'a str {
Style::default()
}
fn set_style(self, style: Style) -> Self::Item {
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
Span::styled(self, style)
}
}
@@ -208,7 +227,7 @@ impl Styled for String {
Style::default()
}
fn set_style(self, style: Style) -> Self::Item {
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
Span::styled(self, style)
}
}

View File

@@ -398,12 +398,12 @@ pub mod braille {
/// Marker to use when plotting data points
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
pub enum Marker {
/// One point per cell in shape of dot ("•")
/// One point per cell in shape of dot (`•`)
#[default]
Dot,
/// One point per cell in shape of a block ("█")
/// One point per cell in shape of a block (`█`)
Block,
/// One point per cell in the shape of a bar ("▄")
/// One point per cell in the shape of a bar (`▄`)
Bar,
/// Use the [Unicode Braille Patterns](https://en.wikipedia.org/wiki/Braille_Patterns) block to
/// represent data points.
@@ -412,9 +412,9 @@ pub enum Marker {
///
/// Note: Support for this marker is limited to terminals and fonts that support Unicode
/// Braille Patterns. If your terminal does not support this, you will see unicode replacement
/// characters (<EFBFBD>) instead of Braille dots.
/// characters (`<60>`) instead of Braille dots (`⠓`, `⣇`, `⣿`).
Braille,
/// Use the unicode block and half block characters ("█", "▄", and "▀") to represent points in
/// Use the unicode block and half block characters (`█`, `▄`, and `▀`) to represent points in
/// a grid that is double the resolution of the terminal. Because each terminal cell is
/// generally about twice as tall as it is wide, this allows for a square grid of pixels.
HalfBlock,

View File

@@ -12,6 +12,7 @@
//!
//! ```rust,no_run
//! use std::io::stdout;
//!
//! use ratatui::{prelude::*, widgets::Paragraph};
//!
//! let backend = CrosstermBackend::new(stdout());
@@ -108,6 +109,7 @@ pub struct TerminalOptions {
///
/// ```rust,no_run
/// use std::io::stdout;
///
/// use ratatui::{prelude::*, widgets::Paragraph};
///
/// let backend = CrosstermBackend::new(stdout());
@@ -198,10 +200,7 @@ where
/// # use ratatui::{prelude::*, backend::TestBackend};
/// let backend = CrosstermBackend::new(stdout());
/// let viewport = Viewport::Fixed(Rect::new(0, 0, 10, 10));
/// let terminal = Terminal::with_options(
/// backend,
/// TerminalOptions { viewport },
/// )?;
/// let terminal = Terminal::with_options(backend, TerminalOptions { viewport })?;
/// # std::io::Result::Ok(())
/// ```
pub fn with_options(mut backend: B, options: TerminalOptions) -> io::Result<Terminal<B>> {
@@ -459,8 +458,9 @@ where
/// Paragraph::new(Line::from(vec![
/// Span::raw("This line will be added "),
/// Span::styled("before", Style::default().fg(Color::Blue)),
/// Span::raw(" the current viewport")
/// ])).render(buf.area, buf);
/// Span::raw(" the current viewport"),
/// ]))
/// .render(buf.area, buf);
/// });
/// ```
pub fn insert_before<F>(&mut self, height: u16, draw_fn: F) -> io::Result<()>
@@ -471,38 +471,50 @@ where
return Ok(());
}
// Clear the viewport off the screen
self.clear()?;
let height = height.min(self.last_known_size.height);
self.backend.append_lines(height)?;
let missing_lines =
height.saturating_sub(self.last_known_size.bottom() - self.viewport_area.top());
// Move the viewport by height, but don't move it past the bottom of the terminal
let viewport_at_bottom = self.last_known_size.bottom() - self.viewport_area.height;
self.set_viewport_area(Rect {
y: self
.viewport_area
.y
.saturating_add(height)
.min(viewport_at_bottom),
..self.viewport_area
});
// Draw contents into buffer
let area = Rect {
x: self.viewport_area.left(),
y: self.viewport_area.top().saturating_sub(missing_lines),
y: 0,
width: self.viewport_area.width,
height,
};
let mut buffer = Buffer::empty(area);
draw_fn(&mut buffer);
let iter = buffer.content.iter().enumerate().map(|(i, c)| {
let (x, y) = buffer.pos_of(i);
(x, y, c)
});
self.backend.draw(iter)?;
self.backend.flush()?;
// Split buffer into screen-sized chunks and draw
let max_chunk_size = (self.viewport_area.top() * area.width).into();
for buffer_content_chunk in buffer.content.chunks(max_chunk_size) {
let chunk_size = buffer_content_chunk.len() as u16 / area.width;
let remaining_lines = self.last_known_size.height - area.bottom();
let missing_lines = self.viewport_area.height.saturating_sub(remaining_lines);
self.backend.append_lines(self.viewport_area.height)?;
self.backend
.append_lines(self.viewport_area.height.saturating_sub(1) + chunk_size)?;
self.set_viewport_area(Rect {
x: area.left(),
y: area.bottom().saturating_sub(missing_lines),
width: area.width,
height: self.viewport_area.height,
});
let iter = buffer_content_chunk.iter().enumerate().map(|(i, c)| {
let (x, y) = buffer.pos_of(i);
(
x,
self.viewport_area.top().saturating_sub(chunk_size) + y,
c,
)
});
self.backend.draw(iter)?;
self.backend.flush()?;
self.set_cursor(self.viewport_area.left(), self.viewport_area.top())?;
}
Ok(())
}
@@ -570,7 +582,11 @@ pub struct Frame<'a> {
impl Frame<'_> {
/// The size of the current frame
///
/// This is guaranteed not to change when rendering.
/// This is guaranteed not to change during rendering, so may be called multiple times.
///
/// If your app listens for a resize event from the backend, it should ignore the values from
/// the event for any calculations that are used to render the current frame and use this value
/// instead as this is the size of the buffer that is used to render the current frame.
pub fn size(&self) -> Rect {
self.viewport_area
}
@@ -616,10 +632,7 @@ impl Frame<'_> {
/// # let mut terminal = Terminal::new(backend).unwrap();
/// # let mut frame = terminal.get_frame();
/// let mut state = ListState::default().with_selected(Some(1));
/// let list = List::new(vec![
/// ListItem::new("Item 1"),
/// ListItem::new("Item 2"),
/// ]);
/// let list = List::new(vec![ListItem::new("Item 1"), ListItem::new("Item 2")]);
/// let area = Rect::new(0, 0, 5, 5);
/// frame.render_stateful_widget(list, area, &mut state);
/// ```

View File

@@ -31,9 +31,8 @@
//! // Converted to Line(vec![
//! // Span { content: Cow::Borrowed("My title"), style: Style { fg: Some(Color::Yellow), .. }
//! // ])
//! let block = Block::default().title(
//! Span::styled("My title", Style::default().fg(Color::Yellow))
//! );
//! let block =
//! Block::default().title(Span::styled("My title", Style::default().fg(Color::Yellow)));
//!
//! // A string with multiple styles.
//! // Converted to Line(vec![
@@ -46,8 +45,6 @@
//! ]);
//! ```
use crate::style::Style;
mod grapheme;
pub use grapheme::StyledGrapheme;

View File

@@ -1,4 +1,4 @@
use crate::style::{Style, Styled};
use crate::prelude::*;
/// A grapheme associated to a style.
/// Note that, although `StyledGrapheme` is the smallest divisible unit of text,
@@ -12,8 +12,15 @@ pub struct StyledGrapheme<'a> {
}
impl<'a> StyledGrapheme<'a> {
pub fn new(symbol: &'a str, style: Style) -> StyledGrapheme<'a> {
StyledGrapheme { symbol, style }
/// Creates a new `StyledGrapheme` with the given symbol and style.
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
pub fn new<S: Into<Style>>(symbol: &'a str, style: S) -> StyledGrapheme<'a> {
StyledGrapheme {
symbol,
style: style.into(),
}
}
}
@@ -24,8 +31,8 @@ impl<'a> Styled for StyledGrapheme<'a> {
self.style
}
fn set_style(mut self, style: Style) -> Self::Item {
self.style = style;
fn set_style<S: Into<Style>>(mut self, style: S) -> Self::Item {
self.style = style.into();
self
}
}
@@ -33,7 +40,6 @@ impl<'a> Styled for StyledGrapheme<'a> {
#[cfg(test)]
mod tests {
use super::*;
use crate::prelude::*;
#[test]
fn new() {

View File

@@ -1,23 +1,106 @@
#![deny(missing_docs)]
use std::borrow::Cow;
use super::{Span, Style, StyledGrapheme};
use crate::layout::Alignment;
use super::StyledGrapheme;
use crate::{prelude::*, widgets::Widget};
/// A line of text, consisting of one or more [`Span`]s.
///
/// [`Line`]s are used wherever text is displayed in the terminal and represent a single line of
/// text. When a [`Line`] is rendered, it is rendered as a single line of text, with each [`Span`]
/// being rendered in order (left to right).
///
/// [`Line`]s can be created from [`Span`]s, [`String`]s, and [`&str`]s. They can be styled with a
/// [`Style`], and have an [`Alignment`].
///
/// The line's [`Alignment`] is used by the rendering widget to determine how to align the line
/// within the available space. If the line is longer than the available space, the alignment is
/// ignored and the line is truncated.
///
/// The line's [`Style`] is used by the rendering widget to determine how to style the line. If the
/// line is longer than the available space, the style is applied to the entire line, and the line
/// is truncated. Each [`Span`] in the line will be styled with the [`Style`] of the line, and then
/// with its own [`Style`].
///
/// `Line` implements the [`Widget`] trait, which means it can be rendered to a [`Buffer`]. Usually
/// apps will use the [`Paragraph`] widget instead of rendering a [`Line`] directly as it provides
/// more functionality.
///
/// # Constructor Methods
///
/// - [`Line::default`] creates a line with empty content and the default style.
/// - [`Line::raw`] creates a line with the given content and the default style.
/// - [`Line::styled`] creates a line with the given content and style.
///
/// # Setter Methods
///
/// These methods are fluent setters. They return a `Line` with the property set.
///
/// - [`Line::spans`] sets the content of the line.
/// - [`Line::style`] sets the style of the line.
/// - [`Line::alignment`] sets the alignment of the line.
///
/// # Other Methods
///
/// - [`Line::patch_style`] patches the style of the line, adding modifiers from the given style.
/// - [`Line::reset_style`] resets the style of the line.
/// - [`Line::width`] returns the unicode width of the content held by this line.
/// - [`Line::styled_graphemes`] returns an iterator over the graphemes held by this line.
///
/// # Compatibility Notes
///
/// Before v0.26.0, [`Line`] did not have a `style` field and instead relied on only the styles that
/// were set on each [`Span`] contained in the `spans` field. The [`Line::patch_style`] method was
/// the only way to set the overall style for individual lines. For this reason, this field may not
/// be supported yet by all widgets (outside of the `ratatui` crate itself).
///
/// # Examples
///
/// ```rust
/// use ratatui::prelude::*;
///
/// Line::raw("unstyled");
/// Line::styled("yellow text", Style::new().yellow());
/// Line::from("red text").style(Style::new().red());
/// Line::from(String::from("unstyled"));
/// Line::from(vec![
/// Span::styled("Hello", Style::new().blue()),
/// Span::raw(" world!"),
/// ]);
/// ```
///
/// [`Paragraph`]: crate::widgets::Paragraph
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Line<'a> {
/// The spans that make up this line of text.
pub spans: Vec<Span<'a>>,
/// The style of this line of text.
pub style: Style,
/// The alignment of this line of text.
pub alignment: Option<Alignment>,
}
impl<'a> Line<'a> {
/// Create a line with the default style.
///
/// `content` can be any type that is convertible to [`Cow<str>`] (e.g. [`&str`], [`String`],
/// [`Cow<str>`], or your own type that implements [`Into<Cow<str>>`]).
///
/// A [`Line`] can specify a [`Style`], which will be applied before the style of each [`Span`]
/// in the line.
///
/// Any newlines in the content are removed.
///
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// # use std::borrow::Cow;
/// Line::raw("test content");
/// Line::raw(String::from("test content"));
/// Line::raw(Cow::from("test content"));
/// ```
pub fn raw<T>(content: T) -> Line<'a>
where
@@ -29,38 +112,120 @@ impl<'a> Line<'a> {
.lines()
.map(|v| Span::raw(v.to_string()))
.collect(),
alignment: None,
..Default::default()
}
}
/// Create a line with a style.
/// Create a line with the given style.
// `content` can be any type that is convertible to [`Cow<str>`] (e.g. [`&str`], [`String`],
/// [`Cow<str>`], or your own type that implements [`Into<Cow<str>>`]).
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// # Examples
///
/// Any newlines in the content are removed.
///
/// ```rust
/// # use ratatui::prelude::*;
/// # use std::borrow::Cow;
/// let style = Style::new().yellow().italic();
/// Line::styled("My text", style);
/// Line::styled(String::from("My text"), style);
/// Line::styled(Cow::from("test content"), style);
/// ```
pub fn styled<T, S>(content: T, style: S) -> Line<'a>
where
T: Into<Cow<'a, str>>,
S: Into<Style>,
{
Line {
spans: content
.into()
.lines()
.map(|v| Span::raw(v.to_string()))
.collect(),
style: style.into(),
..Default::default()
}
}
/// Sets the spans of this line of text.
///
/// `spans` accepts any iterator that yields items that are convertible to [`Span`] (e.g.
/// [`&str`], [`String`], [`Span`], or your own type that implements [`Into<Span>`]).
///
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
/// Line::styled("My text", style);
/// Line::styled(String::from("My text"), style);
/// let line = Line::default().spans(vec!["Hello".blue(), " world!".green()]);
/// let line = Line::default().spans([1, 2, 3].iter().map(|i| format!("Item {}", i)));
/// ```
pub fn styled<T>(content: T, style: Style) -> Line<'a>
#[must_use = "method moves the value of self and returns the modified value"]
pub fn spans<I>(mut self, spans: I) -> Self
where
T: Into<Cow<'a, str>>,
I: IntoIterator,
I::Item: Into<Span<'a>>,
{
Line::from(Span::styled(content, style))
self.spans = spans.into_iter().map(Into::into).collect();
self
}
/// Sets the style of this line of text.
///
/// Defaults to [`Style::default()`].
///
/// Note: This field was added in v0.26.0. Prior to that, the style of a line was determined
/// only by the style of each [`Span`] contained in the line. For this reason, this field may
/// not be supported by all widgets (outside of the `ratatui` crate itself).
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// # Examples
/// ```rust
/// # use ratatui::prelude::*;
/// let mut line = Line::from("foo").style(Style::new().red());
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = style.into();
self
}
/// Sets the target alignment for this line of text.
///
/// Defaults to: [`None`], meaning the alignment is determined by the rendering widget.
///
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// let mut line = Line::from("Hi, what's up?");
/// assert_eq!(None, line.alignment);
/// assert_eq!(
/// Some(Alignment::Right),
/// line.alignment(Alignment::Right).alignment
/// )
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn alignment(self, alignment: Alignment) -> Self {
Self {
alignment: Some(alignment),
..self
}
}
/// Returns the width of the underlying string.
///
/// ## Examples
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// let line = Line::from(vec![
/// Span::styled("My", Style::default().fg(Color::Yellow)),
/// Span::raw(" text"),
/// ]);
/// assert_eq!(7, line.width());
/// let line = Line::from(vec!["Hello".blue(), " world!".green()]);
/// assert_eq!(12, line.width());
/// ```
pub fn width(&self) -> usize {
self.spans.iter().map(Span::width).sum()
@@ -71,16 +236,21 @@ impl<'a> Line<'a> {
/// `base_style` is the [`Style`] that will be patched with each grapheme [`Style`] to get
/// the resulting [`Style`].
///
/// ## Examples
/// `base_style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`],
/// or your own type that implements [`Into<Style>`]).
///
/// # Examples
///
/// ```rust
/// use std::iter::Iterator;
///
/// use ratatui::{prelude::*, text::StyledGrapheme};
///
/// let line = Line::styled("Text", Style::default().fg(Color::Yellow));
/// let style = Style::default().fg(Color::Green).bg(Color::Black);
/// assert_eq!(
/// line.styled_graphemes(style).collect::<Vec<StyledGrapheme>>(),
/// line.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)),
@@ -89,26 +259,33 @@ impl<'a> Line<'a> {
/// ]
/// );
/// ```
pub fn styled_graphemes(
pub fn styled_graphemes<S: Into<Style>>(
&'a self,
base_style: Style,
base_style: S,
) -> impl Iterator<Item = StyledGrapheme<'a>> {
let style = base_style.into().patch(self.style);
self.spans
.iter()
.flat_map(move |span| span.styled_graphemes(base_style))
.flat_map(move |span| span.styled_graphemes(style))
}
/// Patches the style of each Span in an existing Line, adding modifiers from the given style.
///
/// ## Examples
/// This is useful for when you want to apply a style to a line that already has some styling.
/// In contrast to [`Line::style`], this method will not overwrite the existing style, but
/// instead will add the given style's modifiers to the existing style of each `Span`.
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
/// let mut raw_line = Line::from(vec![
/// Span::raw("My"),
/// Span::raw(" text"),
/// ]);
/// let style = Style::default()
/// .fg(Color::Yellow)
/// .add_modifier(Modifier::ITALIC);
/// let mut raw_line = Line::from(vec![Span::raw("My"), Span::raw(" text")]);
/// let mut styled_line = Line::from(vec![
/// Span::styled("My", style),
/// Span::styled(" text", style),
@@ -119,16 +296,18 @@ impl<'a> Line<'a> {
/// raw_line.patch_style(style);
/// assert_eq!(raw_line, styled_line);
/// ```
pub fn patch_style(&mut self, style: Style) {
pub fn patch_style<S: Into<Style>>(&mut self, style: S) {
let style = style.into();
for span in &mut self.spans {
span.patch_style(style);
}
}
/// Resets the style of each Span in the Line.
///
/// Equivalent to calling `patch_style(Style::reset())`.
///
/// ## Examples
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
@@ -146,24 +325,6 @@ impl<'a> Line<'a> {
span.reset_style();
}
}
/// Sets the target alignment for this line of text.
/// Defaults to: [`None`], meaning the alignment is determined by the rendering widget.
///
/// ## Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// let mut line = Line::from("Hi, what's up?");
/// assert_eq!(None, line.alignment);
/// assert_eq!(Some(Alignment::Right), line.alignment(Alignment::Right).alignment)
/// ```
pub fn alignment(self, alignment: Alignment) -> Self {
Self {
alignment: Some(alignment),
..self
}
}
}
impl<'a> From<String> for Line<'a> {
@@ -202,16 +363,118 @@ impl<'a> From<Line<'a>> for String {
}
}
impl Widget for Line<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
let area = area.intersection(buf.area);
buf.set_style(area, self.style);
let width = self.width() as u16;
let offset = match self.alignment {
Some(Alignment::Left) => 0,
Some(Alignment::Center) => (area.width.saturating_sub(width)) / 2,
Some(Alignment::Right) => area.width.saturating_sub(width),
None => 0,
};
let mut x = area.left().saturating_add(offset);
for span in self.spans {
let span_width = span.width() as u16;
let span_area = Rect {
x,
width: span_width,
..area
};
span.render(span_area, buf);
x = x.saturating_add(span_width);
if x >= area.right() {
break;
}
}
}
}
#[cfg(test)]
mod tests {
use crate::{
layout::Alignment,
style::{Color, Modifier, Style},
text::{Line, Span, StyledGrapheme},
};
use super::*;
#[test]
fn test_width() {
fn raw_str() {
let line = Line::raw("test content");
assert_eq!(line.spans, vec![Span::raw("test content")]);
assert_eq!(line.alignment, None);
let line = Line::raw("a\nb");
assert_eq!(line.spans, vec![Span::raw("a"), Span::raw("b")]);
assert_eq!(line.alignment, None);
}
#[test]
fn styled_str() {
let style = Style::new().yellow();
let content = "Hello, world!";
let line = Line::styled(content, style);
assert_eq!(line.spans, vec![Span::raw(content)]);
assert_eq!(line.style, style);
}
#[test]
fn styled_string() {
let style = Style::new().yellow();
let content = String::from("Hello, world!");
let line = Line::styled(content.clone(), style);
assert_eq!(line.spans, vec![Span::raw(content)]);
assert_eq!(line.style, style);
}
#[test]
fn styled_cow() {
let style = Style::new().yellow();
let content = Cow::from("Hello, world!");
let line = Line::styled(content.clone(), style);
assert_eq!(line.spans, vec![Span::raw(content)]);
assert_eq!(line.style, style);
}
#[test]
fn spans_vec() {
let line = Line::default().spans(vec!["Hello".blue(), " world!".green()]);
assert_eq!(
line.spans,
vec![
Span::styled("Hello", Style::new().blue()),
Span::styled(" world!", Style::new().green()),
]
);
}
#[test]
fn spans_iter() {
let line = Line::default().spans([1, 2, 3].iter().map(|i| format!("Item {i}")));
assert_eq!(
line.spans,
vec![
Span::raw("Item 1"),
Span::raw("Item 2"),
Span::raw("Item 3"),
]
);
}
#[test]
fn style() {
let line = Line::default().style(Style::new().red());
assert_eq!(line.style, Style::new().red());
}
#[test]
fn alignment() {
let line = Line::from("This is left").alignment(Alignment::Left);
assert_eq!(Some(Alignment::Left), line.alignment);
let line = Line::from("This is default");
assert_eq!(None, line.alignment);
}
#[test]
fn width() {
let line = Line::from(vec![
Span::styled("My", Style::default().fg(Color::Yellow)),
Span::raw(" text"),
@@ -223,7 +486,7 @@ mod tests {
}
#[test]
fn test_patch_style() {
fn patch_style() {
let style = Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::ITALIC);
@@ -240,7 +503,7 @@ mod tests {
}
#[test]
fn test_reset_style() {
fn reset_style() {
let mut line = Line::from(vec![
Span::styled("My", Style::default().fg(Color::Yellow)),
Span::styled(" text", Style::default().add_modifier(Modifier::BOLD)),
@@ -252,21 +515,21 @@ mod tests {
}
#[test]
fn test_from_string() {
fn from_string() {
let s = String::from("Hello, world!");
let line = Line::from(s);
assert_eq!(vec![Span::from("Hello, world!")], line.spans);
}
#[test]
fn test_from_str() {
fn from_str() {
let s = "Hello, world!";
let line = Line::from(s);
assert_eq!(vec![Span::from("Hello, world!")], line.spans);
}
#[test]
fn test_from_vec() {
fn from_vec() {
let spans = vec![
Span::styled("Hello,", Style::default().fg(Color::Red)),
Span::styled(" world!", Style::default().fg(Color::Green)),
@@ -276,14 +539,14 @@ mod tests {
}
#[test]
fn test_from_span() {
fn from_span() {
let span = Span::styled("Hello, world!", Style::default().fg(Color::Yellow));
let line = Line::from(span.clone());
assert_eq!(vec![span], line.spans);
}
#[test]
fn test_into_string() {
fn into_string() {
let line = Line::from(vec![
Span::styled("Hello,", Style::default().fg(Color::Red)),
Span::styled(" world!", Style::default().fg(Color::Green)),
@@ -292,15 +555,6 @@ mod tests {
assert_eq!("Hello, world!", s);
}
#[test]
fn test_alignment() {
let line = Line::from("This is left").alignment(Alignment::Left);
assert_eq!(Some(Alignment::Left), line.alignment);
let line = Line::from("This is default");
assert_eq!(None, line.alignment);
}
#[test]
fn styled_graphemes() {
const RED: Style = Style::new().fg(Color::Red);
@@ -331,14 +585,75 @@ mod tests {
);
}
#[test]
fn raw_str() {
let line = Line::raw("test content");
assert_eq!(line.spans, vec![Span::raw("test content")]);
assert_eq!(line.alignment, None);
mod widget {
use super::*;
use crate::assert_buffer_eq;
const BLUE: Style = Style::new().fg(Color::Blue);
const GREEN: Style = Style::new().fg(Color::Green);
const ITALIC: Style = Style::new().add_modifier(Modifier::ITALIC);
let line = Line::raw("a\nb");
assert_eq!(line.spans, vec![Span::raw("a"), Span::raw("b")]);
assert_eq!(line.alignment, None);
fn hello_world() -> Line<'static> {
Line::from(vec![
Span::styled("Hello ", BLUE),
Span::styled("world!", GREEN),
])
.style(ITALIC)
}
#[test]
fn render() {
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
hello_world().render(Rect::new(0, 0, 15, 1), &mut buf);
let mut expected = Buffer::with_lines(vec!["Hello world! "]);
expected.set_style(Rect::new(0, 0, 15, 1), ITALIC);
expected.set_style(Rect::new(0, 0, 6, 1), BLUE);
expected.set_style(Rect::new(6, 0, 6, 1), GREEN);
assert_buffer_eq!(buf, expected);
}
#[test]
fn render_only_styles_line_area() {
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 1));
hello_world().render(Rect::new(0, 0, 15, 1), &mut buf);
let mut expected = Buffer::with_lines(vec!["Hello world! "]);
expected.set_style(Rect::new(0, 0, 15, 1), ITALIC);
expected.set_style(Rect::new(0, 0, 6, 1), BLUE);
expected.set_style(Rect::new(6, 0, 6, 1), GREEN);
assert_buffer_eq!(buf, expected);
}
#[test]
fn render_truncates() {
let mut buf = Buffer::empty(Rect::new(0, 0, 11, 1));
hello_world().render(Rect::new(0, 0, 11, 1), &mut buf);
let mut expected = Buffer::with_lines(vec!["Hello world"]);
expected.set_style(Rect::new(0, 0, 6, 1), BLUE.italic());
expected.set_style(Rect::new(6, 0, 5, 1), GREEN.italic());
assert_buffer_eq!(buf, expected);
}
#[test]
fn render_centered() {
let line = hello_world().alignment(Alignment::Center);
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
line.render(Rect::new(0, 0, 15, 1), &mut buf);
let mut expected = Buffer::with_lines(vec![" Hello world! "]);
expected.set_style(Rect::new(0, 0, 15, 1), ITALIC);
expected.set_style(Rect::new(1, 0, 6, 1), BLUE);
expected.set_style(Rect::new(7, 0, 6, 1), GREEN);
assert_buffer_eq!(buf, expected);
}
#[test]
fn render_right_aligned() {
let line = hello_world().alignment(Alignment::Right);
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
line.render(Rect::new(0, 0, 15, 1), &mut buf);
let mut expected = Buffer::with_lines(vec![" Hello world!"]);
expected.set_style(Rect::new(0, 0, 15, 1), ITALIC);
expected.set_style(Rect::new(3, 0, 6, 1), BLUE);
expected.set_style(Rect::new(9, 0, 6, 1), GREEN);
assert_buffer_eq!(buf, expected);
}
}
}

View File

@@ -4,13 +4,33 @@ use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use super::StyledGrapheme;
use crate::style::{Style, Styled};
use crate::{prelude::*, widgets::Widget};
/// 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.
///
/// # Constructor Methods
///
/// - [`Span::default`] creates an span with empty content and the default style.
/// - [`Span::raw`] creates an span with the specified content and the default style.
/// - [`Span::styled`] creates an span with the specified content and style.
///
/// # Setter Methods
///
/// These methods are fluent setters. They return a new `Span` with the specified property set.
///
/// - [`Span::content`] sets the content of the span.
/// - [`Span::style`] sets the style of the span.
///
/// # Other Methods
///
/// - [`Span::patch_style`] patches the style of the span, adding modifiers from the given style.
/// - [`Span::reset_style`] resets the style of the span.
/// - [`Span::width`] returns the unicode width of the content held by this span.
/// - [`Span::styled_graphemes`] returns an iterator over the graphemes held by this span.
///
/// # Examples
///
/// A `Span` with `style` set to [`Style::default()`] can be created from a `&str`, a `String`, or
@@ -35,21 +55,38 @@ use crate::style::{Style, Styled};
///
/// let span = Span::styled("test content", Style::new().green());
/// let span = Span::styled(String::from("test content"), Style::new().green());
///
/// // using Stylize trait shortcuts
/// 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.
/// `Span` implements the [`Styled`] trait, which allows it to be styled using the shortcut methods
/// defined in the [`Stylize`] trait.
///
/// ```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();
/// let span = Span::raw(String::from("test content"))
/// .green()
/// .on_yellow()
/// .italic();
/// ```
///
/// `Span` implements the [`Widget`] trait, which allows it to be rendered to a [`Buffer`]. Usually
/// apps will use the [`Paragraph`] widget instead of rendering `Span` directly, as it handles text
/// wrapping and alignment for you.
///
/// ```rust
/// use ratatui::prelude::*;
///
/// # fn render_frame(frame: &mut Frame) {
/// frame.render_widget("test content".green().on_yellow().italic(), frame.size());
/// # }
/// ```
/// [`Line`]: crate::text::Line
/// [`Paragraph`]: crate::widgets::Paragraph
/// [`Stylize`]: crate::style::Stylize
/// [`Cow<str>`]: std::borrow::Cow
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
@@ -82,6 +119,12 @@ impl<'a> Span<'a> {
/// Create a span with the specified style.
///
/// `content` accepts any type that is convertible to [`Cow<str>`] (e.g. `&str`, `String`,
/// `&String`, etc.).
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// # Examples
///
/// ```rust
@@ -90,60 +133,66 @@ impl<'a> Span<'a> {
/// Span::styled("test content", style);
/// Span::styled(String::from("test content"), style);
/// ```
pub fn styled<T>(content: T, style: Style) -> Span<'a>
pub fn styled<T, S>(content: T, style: S) -> Span<'a>
where
T: Into<Cow<'a, str>>,
S: Into<Style>,
{
Span {
content: content.into(),
style,
style: style.into(),
}
}
/// 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.
/// Sets the content of the span.
///
/// `base_style` is the [`Style`] that will be patched with the `Span`'s `style` to get the
/// resulting [`Style`].
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Example
/// Accepts any type that can be converted to [`Cow<str>`] (e.g. `&str`, `String`, `&String`,
/// etc.).
///
/// # Examples
///
/// ```rust
/// use std::iter::Iterator;
/// use ratatui::{prelude::*, text::StyledGrapheme};
///
/// let span = Span::styled("Test", Style::new().green().italic());
/// let style = Style::new().red().on_yellow();
/// assert_eq!(
/// span.styled_graphemes(style).collect::<Vec<StyledGrapheme>>(),
/// vec![
/// StyledGrapheme::new("T", Style::new().green().on_yellow().italic()),
/// StyledGrapheme::new("e", Style::new().green().on_yellow().italic()),
/// StyledGrapheme::new("s", Style::new().green().on_yellow().italic()),
/// StyledGrapheme::new("t", Style::new().green().on_yellow().italic()),
/// ],
/// );
/// # use ratatui::prelude::*;
/// let mut span = Span::default().content("content");
/// ```
pub fn styled_graphemes(
&'a self,
base_style: Style,
) -> impl Iterator<Item = StyledGrapheme<'a>> {
self.content
.as_ref()
.graphemes(true)
.filter(|g| *g != "\n")
.map(move |g| StyledGrapheme {
symbol: g,
style: base_style.patch(self.style),
})
#[must_use = "method moves the value of self and returns the modified value"]
pub fn content<T>(mut self, content: T) -> Self
where
T: Into<Cow<'a, str>>,
{
self.content = content.into();
self
}
/// Sets the style of the span.
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// In contrast to [`Span::patch_style`], this method replaces the style of the span instead of
/// patching it.
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// let mut span = Span::default().style(Style::new().green());
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = style.into();
self
}
/// Patches the style of the Span, adding modifiers from the given style.
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// # Example
///
/// ```rust
@@ -152,7 +201,7 @@ impl<'a> Span<'a> {
/// 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) {
pub fn patch_style<S: Into<Style>>(&mut self, style: S) {
self.style = self.style.patch(style);
}
@@ -171,6 +220,51 @@ impl<'a> Span<'a> {
pub fn reset_style(&mut self) {
self.patch_style(Style::reset());
}
/// 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 the `Span`'s `style` to get the
/// resulting [`Style`].
///
/// `base_style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`],
/// or your own type that implements [`Into<Style>`]).
///
/// # Example
///
/// ```rust
/// use std::iter::Iterator;
///
/// use ratatui::{prelude::*, text::StyledGrapheme};
///
/// let span = Span::styled("Test", Style::new().green().italic());
/// let style = Style::new().red().on_yellow();
/// assert_eq!(
/// span.styled_graphemes(style)
/// .collect::<Vec<StyledGrapheme>>(),
/// vec![
/// StyledGrapheme::new("T", Style::new().green().on_yellow().italic()),
/// StyledGrapheme::new("e", Style::new().green().on_yellow().italic()),
/// StyledGrapheme::new("s", Style::new().green().on_yellow().italic()),
/// StyledGrapheme::new("t", Style::new().green().on_yellow().italic()),
/// ],
/// );
/// ```
pub fn styled_graphemes<S: Into<Style>>(
&'a self,
base_style: S,
) -> impl Iterator<Item = StyledGrapheme<'a>> {
let style = base_style.into().patch(self.style);
self.content
.as_ref()
.graphemes(true)
.filter(|g| *g != "\n")
.map(move |g| StyledGrapheme { symbol: g, style })
}
}
impl<'a, T> From<T> for Span<'a>
@@ -189,16 +283,47 @@ impl<'a> Styled for Span<'a> {
self.style
}
fn set_style(mut self, style: Style) -> Self {
self.style = style;
self
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
self.style(style)
}
}
impl Widget for Span<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
let Rect {
x: mut current_x,
y,
width,
..
} = area;
let max_x = Ord::min(current_x.saturating_add(width), buf.area.right());
for g in self.styled_graphemes(Style::default()) {
let symbol_width = g.symbol.width();
let next_x = current_x.saturating_add(symbol_width as u16);
if next_x > max_x {
break;
}
buf.get_mut(current_x, y)
.set_symbol(g.symbol)
.set_style(g.style);
// multi-width graphemes must clear the cells of characters that are hidden by the
// grapheme, otherwise the hidden characters will be re-rendered if the grapheme is
// overwritten.
for i in (current_x + 1)..next_x {
buf.get_mut(i, y).reset();
// it may seem odd that the style of the hidden cells are not set to the style of
// the grapheme, but this is how the existing buffer.set_span() method works.
// buf.get_mut(i, y).set_style(g.style);
}
current_x = next_x;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::style::Stylize;
#[test]
fn default() {
@@ -239,6 +364,18 @@ mod tests {
assert_eq!(span.style, style);
}
#[test]
fn set_content() {
let span = Span::default().content("test content");
assert_eq!(span.content, Cow::Borrowed("test content"));
}
#[test]
fn set_style() {
let span = Span::default().style(Style::new().green());
assert_eq!(span.style, Style::new().green());
}
#[test]
fn from_ref_str_borrowed_cow() {
let content = "test content";
@@ -303,4 +440,103 @@ mod tests {
assert_eq!(stylized.content, Cow::Borrowed("test content"));
assert_eq!(stylized.style, Style::new().green().on_yellow().bold());
}
mod widget {
use super::*;
use crate::{assert_buffer_eq, style::Stylize};
#[test]
fn render() {
let style = Style::new().green().on_yellow();
let span = Span::styled("test content", style);
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
span.render(buf.area, &mut buf);
let expected = Buffer::with_lines(vec![Line::from(vec![
"test content".green().on_yellow(),
" ".into(),
])]);
assert_buffer_eq!(buf, expected);
}
/// When the content of the span is longer than the area passed to render, the content
/// should be truncated
#[test]
fn render_truncates_too_long_content() {
let style = Style::new().green().on_yellow();
let span = Span::styled("test content", style);
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 1));
span.render(buf.area, &mut buf);
let expected =
Buffer::with_lines(vec![Line::from(vec!["test conte".green().on_yellow()])]);
assert_buffer_eq!(buf, expected);
}
/// When there is already a style set on the buffer, the style of the span should be
/// patched with the existing style
#[test]
fn render_patches_existing_style() {
let style = Style::new().green().on_yellow();
let span = Span::styled("test content", style);
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
buf.set_style(buf.area, Style::new().italic());
span.render(buf.area, &mut buf);
let expected = Buffer::with_lines(vec![Line::from(vec![
"test content".green().on_yellow().italic(),
" ".italic(),
])]);
assert_buffer_eq!(buf, expected);
}
/// When the span contains a multi-width grapheme, the grapheme will ensure that the cells
/// of the hidden characters are cleared.
#[test]
fn render_multi_width_symbol() {
let style = Style::new().green().on_yellow();
let span = Span::styled("test 😃 content", style);
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
span.render(buf.area, &mut buf);
// The existing code in buffer.set_line() handles multi-width graphemes by clearing the
// cells of the hidden characters. This test ensures that the existing behavior is
// preserved.
let expected = Buffer::with_lines(vec!["test 😃 content".green().on_yellow()]);
assert_buffer_eq!(buf, expected);
}
/// When the span contains a multi-width grapheme that does not fit in the area passed to
/// render, the entire grapheme will be truncated.
#[test]
fn render_multi_width_symbol_truncates_entire_symbol() {
// the 😃 emoji is 2 columns wide so it will be truncated
let style = Style::new().green().on_yellow();
let span = Span::styled("test 😃 content", style);
let mut buf = Buffer::empty(Rect::new(0, 0, 6, 1));
span.render(buf.area, &mut buf);
let expected = Buffer::with_lines(vec![Line::from(vec![
"test ".green().on_yellow(),
" ".into(),
])]);
assert_buffer_eq!(buf, expected);
}
/// When the area passed to render overflows the buffer, the content should be truncated
/// to fit the buffer.
#[test]
fn render_overflowing_area_truncates() {
let style = Style::new().green().on_yellow();
let span = Span::styled("test content", style);
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
span.render(Rect::new(10, 0, 20, 1), &mut buf);
let expected = Buffer::with_lines(vec![Line::from(vec![
" ".into(),
"test ".green().on_yellow(),
])]);
assert_buffer_eq!(buf, expected);
}
}
}

View File

@@ -1,7 +1,6 @@
use std::borrow::Cow;
use super::{Line, Span};
use crate::style::Style;
use crate::prelude::*;
/// A string split over multiple lines where each line is composed of several clusters, each with
/// their own style.
@@ -13,7 +12,9 @@ use crate::style::Style;
/// ```rust
/// use ratatui::prelude::*;
///
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
/// let style = Style::default()
/// .fg(Color::Yellow)
/// .add_modifier(Modifier::ITALIC);
///
/// // An initial two lines of `Text` built from a `&str`
/// let mut text = Text::from("The first line\nThe second line");
@@ -58,17 +59,23 @@ impl<'a> Text<'a> {
/// Create some text (potentially multiple lines) with a style.
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
/// let style = Style::default()
/// .fg(Color::Yellow)
/// .add_modifier(Modifier::ITALIC);
/// Text::styled("The first line\nThe second line", style);
/// Text::styled(String::from("The first line\nThe second line"), style);
/// ```
pub fn styled<T>(content: T, style: Style) -> Text<'a>
pub fn styled<T, S>(content: T, style: S) -> Text<'a>
where
T: Into<Cow<'a, str>>,
S: Into<Style>,
{
let mut text = Text::raw(content);
text.patch_style(style);
@@ -103,11 +110,16 @@ impl<'a> Text<'a> {
/// Patches the style of each line in an existing Text, adding modifiers from the given style.
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
/// let style = Style::default()
/// .fg(Color::Yellow)
/// .add_modifier(Modifier::ITALIC);
/// let mut raw_text = Text::raw("The first line\nThe second line");
/// let styled_text = Text::styled(String::from("The first line\nThe second line"), style);
/// assert_ne!(raw_text, styled_text);
@@ -115,7 +127,8 @@ impl<'a> Text<'a> {
/// raw_text.patch_style(style);
/// assert_eq!(raw_text, styled_text);
/// ```
pub fn patch_style(&mut self, style: Style) {
pub fn patch_style<S: Into<Style>>(&mut self, style: S) {
let style = style.into();
for line in &mut self.lines {
line.patch_style(style);
}
@@ -128,7 +141,9 @@ impl<'a> Text<'a> {
///
/// ```rust
/// # use ratatui::prelude::*;
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
/// let style = Style::default()
/// .fg(Color::Yellow)
/// .add_modifier(Modifier::ITALIC);
/// let mut text = Text::styled("The first line\nThe second line", style);
///
/// text.reset_style();

View File

@@ -29,14 +29,15 @@ use crate::{layout::Alignment, text::Line};
/// ```
/// use ratatui::{prelude::*, widgets::block::*};
///
/// Title::from(
/// Line::from(vec!["Q".white().underlined(), "uit".gray()])
/// );
/// Title::from(Line::from(vec!["Q".white().underlined(), "uit".gray()]));
/// ```
///
/// Complete example
/// ```
/// use ratatui::{prelude::*, widgets::{*, block::*}};
/// use ratatui::{
/// prelude::*,
/// widgets::{block::*, *},
/// };
///
/// Title::from("Title")
/// .position(Position::Top)
@@ -69,11 +70,9 @@ pub struct Title<'a> {
/// # Example
///
/// ```
/// use ratatui::widgets::{*, block::*};
/// use ratatui::widgets::{block::*, *};
///
/// Block::new().title(
/// Title::from("title").position(Position::Bottom)
/// );
/// Block::new().title(Title::from("title").position(Position::Bottom));
/// ```
#[derive(Debug, Default, Display, EnumString, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Position {
@@ -87,7 +86,7 @@ pub enum Position {
}
impl<'a> Title<'a> {
/// Builder pattern method for setting the title content.
/// Set the title content.
pub fn content<T>(mut self, content: T) -> Title<'a>
where
T: Into<Line<'a>>,
@@ -96,13 +95,15 @@ impl<'a> Title<'a> {
self
}
/// Builder pattern method for setting the title alignment.
/// Set the title alignment.
#[must_use = "method moves the value of self and returns the modified value"]
pub fn alignment(mut self, alignment: Alignment) -> Title<'a> {
self.alignment = Some(alignment);
self
}
/// Builder pattern method for setting the title position.
/// Set the title position.
#[must_use = "method moves the value of self and returns the modified value"]
pub fn position(mut self, position: Position) -> Title<'a> {
self.position = Some(position);
self

View File

@@ -1,7 +1,7 @@
//! `widgets` is a collection of types that implement [`Widget`] or [`StatefulWidget`] or both.
//!
//! All widgets are implemented using the builder pattern and are consumable objects. They are not
//! meant to be stored but used as *commands* to draw common figures in the UI.
//! Widgets are created for each frame as they are consumed after rendered.
//! They are not meant to be stored but used as *commands* to draw common figures in the UI.
//!
//! The available widgets are:
//! - [`Block`]: a basic widget that draws a block with optional borders, titles and styles.
@@ -43,10 +43,10 @@ use bitflags::bitflags;
pub use self::{
barchart::{Bar, BarChart, BarGroup},
block::{Block, BorderType, Padding},
chart::{Axis, Chart, Dataset, GraphType},
chart::{Axis, Chart, Dataset, GraphType, LegendPosition},
clear::Clear,
gauge::{Gauge, LineGauge},
list::{List, ListItem, ListState},
list::{List, ListDirection, ListItem, ListState},
paragraph::{Paragraph, Wrap},
scrollbar::{ScrollDirection, Scrollbar, ScrollbarOrientation, ScrollbarState},
sparkline::{RenderDirection, Sparkline},
@@ -130,6 +130,7 @@ pub trait Widget {
///
/// ```rust,no_run
/// use std::io;
///
/// use ratatui::{backend::TestBackend, prelude::*, widgets::*};
///
/// // Let's say we have some events to display.
@@ -139,7 +140,7 @@ pub trait Widget {
/// // `state` is the state that can be modified by the UI. It stores the index of the selected
/// // item as well as the offset computed during the previous draw call (used to implement
/// // natural scrolling).
/// state: ListState
/// state: ListState,
/// }
///
/// impl Events {
@@ -199,16 +200,17 @@ pub trait Widget {
/// # let backend = TestBackend::new(5, 5);
/// # let mut terminal = Terminal::new(backend).unwrap();
///
/// let mut events = Events::new(vec![
/// String::from("Item 1"),
/// String::from("Item 2")
/// ]);
/// let mut events = Events::new(vec![String::from("Item 1"), String::from("Item 2")]);
///
/// loop {
/// terminal.draw(|f| {
/// // The items managed by the application are transformed to something
/// // that is understood by ratatui.
/// let items: Vec<ListItem>= events.items.iter().map(|i| ListItem::new(i.as_str())).collect();
/// let items: Vec<ListItem> = events
/// .items
/// .iter()
/// .map(|i| ListItem::new(i.as_str()))
/// .collect();
/// // The `List` widget is then built with those items.
/// let list = List::new(items);
/// // Finally the widget is rendered using the associated state. `events.state` is

View File

@@ -127,6 +127,7 @@ impl<'a> BarChart<'a> {
}
/// Surround the [`BarChart`] with a [`Block`].
#[must_use = "method moves the value of self and returns the modified value"]
pub fn block(mut self, block: Block<'a>) -> BarChart<'a> {
self.block = Some(block);
self
@@ -161,6 +162,7 @@ impl<'a> BarChart<'a> {
/// // █ █ █
/// // f b b
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn max(mut self, max: u64) -> BarChart<'a> {
self.max = Some(max);
self
@@ -168,10 +170,14 @@ impl<'a> BarChart<'a> {
/// Set the default style of the bar.
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// 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;
#[must_use = "method moves the value of self and returns the modified value"]
pub fn bar_style<S: Into<Style>>(mut self, style: S) -> BarChart<'a> {
self.bar_style = style.into();
self
}
@@ -182,6 +188,7 @@ impl<'a> BarChart<'a> {
///
/// If not set, this defaults to `1`.
/// The bar label also uses this value as its width.
#[must_use = "method moves the value of self and returns the modified value"]
pub fn bar_width(mut self, width: u16) -> BarChart<'a> {
self.bar_width = width;
self
@@ -205,6 +212,7 @@ impl<'a> BarChart<'a> {
/// // █ █
/// // f b
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn bar_gap(mut self, gap: u16) -> BarChart<'a> {
self.bar_gap = gap;
self
@@ -213,6 +221,7 @@ impl<'a> BarChart<'a> {
/// The [`bar::Set`](crate::symbols::bar::Set) to use for displaying the bars.
///
/// If not set, the default is [`bar::NINE_LEVELS`](crate::symbols::bar::NINE_LEVELS).
#[must_use = "method moves the value of self and returns the modified value"]
pub fn bar_set(mut self, bar_set: symbols::bar::Set) -> BarChart<'a> {
self.bar_set = bar_set;
self
@@ -220,31 +229,40 @@ impl<'a> BarChart<'a> {
/// Set the default value style of the bar.
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// It is also possible to set individually the value style of each [`Bar`].
/// In this case the default value style will be patched by the individual value style
///
/// # See also
///
/// [Bar::value_style] to set the value style individually.
pub fn value_style(mut self, style: Style) -> BarChart<'a> {
self.value_style = style;
#[must_use = "method moves the value of self and returns the modified value"]
pub fn value_style<S: Into<Style>>(mut self, style: S) -> BarChart<'a> {
self.value_style = style.into();
self
}
/// Set the default label style of the groups and bars.
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// It is also possible to set individually the label style of each [`Bar`] or [`BarGroup`].
/// In this case the default label style will be patched by the individual label style
///
/// # See also
///
/// [Bar::label] to set the label style individually.
pub fn label_style(mut self, style: Style) -> BarChart<'a> {
self.label_style = style;
#[must_use = "method moves the value of self and returns the modified value"]
pub fn label_style<S: Into<Style>>(mut self, style: S) -> BarChart<'a> {
self.label_style = style.into();
self
}
/// Set the gap between [`BarGroup`].
#[must_use = "method moves the value of self and returns the modified value"]
pub fn group_gap(mut self, gap: u16) -> BarChart<'a> {
self.group_gap = gap;
self
@@ -252,9 +270,13 @@ impl<'a> BarChart<'a> {
/// Set the style of the entire chart.
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// The style will be applied to everything that isn't styled (borders, bars, labels, ...).
pub fn style(mut self, style: Style) -> BarChart<'a> {
self.style = style;
#[must_use = "method moves the value of self and returns the modified value"]
pub fn style<S: Into<Style>>(mut self, style: S) -> BarChart<'a> {
self.style = style.into();
self
}
@@ -277,6 +299,7 @@ impl<'a> BarChart<'a> {
///
/// █bar██
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn direction(mut self, direction: Direction) -> BarChart<'a> {
self.direction = direction;
self
@@ -586,7 +609,7 @@ impl<'a> Styled for BarChart<'a> {
self.style
}
fn set_style(self, style: Style) -> Self {
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
self.style(style)
}
}

View File

@@ -1,6 +1,6 @@
use unicode_width::UnicodeWidthStr;
use crate::{buffer::Buffer, prelude::Rect, style::Style, text::Line};
use crate::prelude::*;
/// A bar to be shown by the [`BarChart`](crate::widgets::BarChart) widget.
///
@@ -49,6 +49,7 @@ impl<'a> Bar<'a> {
///
/// [`Bar::value_style`] to style the value.
/// [`Bar::text_value`] to set the displayed value.
#[must_use = "method moves the value of self and returns the modified value"]
pub fn value(mut self, value: u64) -> Bar<'a> {
self.value = value;
self
@@ -61,6 +62,7 @@ impl<'a> Bar<'a> {
/// For [`Horizontal`](crate::layout::Direction::Horizontal) bars,
/// display the label **in** the bar.
/// See [`BarChart::direction`](crate::widgets::BarChart::direction) to set the direction.
#[must_use = "method moves the value of self and returns the modified value"]
pub fn label(mut self, label: Line<'a>) -> Bar<'a> {
self.label = Some(label);
self
@@ -68,20 +70,27 @@ impl<'a> Bar<'a> {
/// Set the style of the bar.
///
/// This will apply to every non-styled element.
/// It can be seen and used as a default value.
pub fn style(mut self, style: Style) -> Bar<'a> {
self.style = style;
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// This will apply to every non-styled element. It can be seen and used as a default value.
#[must_use = "method moves the value of self and returns the modified value"]
pub fn style<S: Into<Style>>(mut self, style: S) -> Bar<'a> {
self.style = style.into();
self
}
/// Set the style of the value.
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// # See also
///
/// [`Bar::value`] to set the value.
pub fn value_style(mut self, style: Style) -> Bar<'a> {
self.value_style = style;
#[must_use = "method moves the value of self and returns the modified value"]
pub fn value_style<S: Into<Style>>(mut self, style: S) -> Bar<'a> {
self.value_style = style.into();
self
}
@@ -93,6 +102,7 @@ impl<'a> Bar<'a> {
/// # See also
///
/// [`Bar::value`] to set the value.
#[must_use = "method moves the value of self and returns the modified value"]
pub fn text_value(mut self, text_value: String) -> Bar<'a> {
self.text_value = Some(text_value);
self

View File

@@ -26,12 +26,14 @@ pub struct BarGroup<'a> {
impl<'a> BarGroup<'a> {
/// Set the group label
#[must_use = "method moves the value of self and returns the modified value"]
pub fn label(mut self, label: Line<'a>) -> BarGroup<'a> {
self.label = Some(label);
self
}
/// Set the bars of the group to be shown
#[must_use = "method moves the value of self and returns the modified value"]
pub fn bars(mut self, bars: &[Bar<'a>]) -> BarGroup<'a> {
self.bars = bars.to_vec();
self

View File

@@ -13,9 +13,7 @@ use strum::{Display, EnumString};
pub use self::title::{Position, Title};
use crate::{
buffer::Buffer,
layout::{Alignment, Rect},
style::{Style, Styled},
prelude::*,
symbols::border,
widgets::{Borders, Widget},
};
@@ -220,7 +218,10 @@ impl Padding {
///
/// You may also use multiple titles like in the following:
/// ```
/// use ratatui::{prelude::*, widgets::{*, block::*}};
/// use ratatui::{
/// prelude::*,
/// widgets::{block::*, *},
/// };
///
/// Block::default()
/// .title("Title 1")
@@ -301,7 +302,10 @@ impl<'a> Block<'a> {
/// the leftover space)
/// - Two titles with the same alignment (notice the left titles are separated)
/// ```
/// use ratatui::{prelude::*, widgets::{*, block::*}};
/// use ratatui::{
/// prelude::*,
/// widgets::{block::*, *},
/// };
///
/// Block::default()
/// .title("Title") // By default in the top left corner
@@ -328,9 +332,13 @@ impl<'a> Block<'a> {
/// Applies the style to all titles.
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// If a [`Title`] already has a style, the title's style will add on top of this one.
pub const fn title_style(mut self, style: Style) -> Block<'a> {
self.titles_style = style;
#[must_use = "method moves the value of self and returns the modified value"]
pub fn title_style<S: Into<Style>>(mut self, style: S) -> Block<'a> {
self.titles_style = style.into();
self
}
@@ -343,7 +351,10 @@ impl<'a> Block<'a> {
/// This example aligns all titles in the center except the "right" title which explicitly sets
/// [`Alignment::Right`].
/// ```
/// use ratatui::{prelude::*, widgets::{*, block::*}};
/// use ratatui::{
/// prelude::*,
/// widgets::{block::*, *},
/// };
///
/// Block::default()
/// // This title won't be aligned in the center
@@ -352,6 +363,7 @@ impl<'a> Block<'a> {
/// .title("bar")
/// .title_alignment(Alignment::Center);
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub const fn title_alignment(mut self, alignment: Alignment) -> Block<'a> {
self.titles_alignment = alignment;
self
@@ -372,7 +384,10 @@ impl<'a> Block<'a> {
/// This example positions all titles on the bottom except the "top" title which explicitly sets
/// [`Position::Top`].
/// ```
/// use ratatui::{prelude::*, widgets::{*, block::*}};
/// use ratatui::{
/// prelude::*,
/// widgets::{block::*, *},
/// };
///
/// Block::default()
/// // This title won't be aligned in the center
@@ -381,6 +396,7 @@ impl<'a> Block<'a> {
/// .title("bar")
/// .title_position(Position::Bottom);
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub const fn title_position(mut self, position: Position) -> Block<'a> {
self.titles_position = position;
self
@@ -390,6 +406,9 @@ impl<'a> Block<'a> {
///
/// If a [`Block::style`] is defined, `border_style` will be applied on top of it.
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// # Example
///
/// This example shows a `Block` with blue borders.
@@ -399,8 +418,9 @@ impl<'a> Block<'a> {
/// .borders(Borders::ALL)
/// .border_style(Style::new().blue());
/// ```
pub const fn border_style(mut self, style: Style) -> Block<'a> {
self.border_style = style;
#[must_use = "method moves the value of self and returns the modified value"]
pub fn border_style<S: Into<Style>>(mut self, style: S) -> Block<'a> {
self.border_style = style.into();
self
}
@@ -410,9 +430,13 @@ impl<'a> Block<'a> {
/// more specific style. Elements can be styled further with [`Block::title_style`] and
/// [`Block::border_style`].
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// This will also apply to the widget inside that block, unless the inner widget is styled.
pub const fn style(mut self, style: Style) -> Block<'a> {
self.style = style;
#[must_use = "method moves the value of self and returns the modified value"]
pub fn style<S: Into<Style>>(mut self, style: S) -> Block<'a> {
self.style = style.into();
self
}
@@ -433,6 +457,7 @@ impl<'a> Block<'a> {
/// # use ratatui::{prelude::*, widgets::*};
/// Block::default().borders(Borders::LEFT | Borders::RIGHT);
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub const fn borders(mut self, flag: Borders) -> Block<'a> {
self.borders = flag;
self
@@ -449,12 +474,16 @@ impl<'a> Block<'a> {
///
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// Block::default().title("Block").borders(Borders::ALL).border_type(BorderType::Rounded);
/// Block::default()
/// .title("Block")
/// .borders(Borders::ALL)
/// .border_type(BorderType::Rounded);
/// // Renders
/// // ╭Block╮
/// // │ │
/// // ╰─────╯
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub const fn border_type(mut self, border_type: BorderType) -> Block<'a> {
self.border_set = border_type.to_border_set();
self
@@ -473,6 +502,7 @@ impl<'a> Block<'a> {
/// // ╔Block╗
/// // ║ ║
/// // ╚═════╝
#[must_use = "method moves the value of self and returns the modified value"]
pub const fn border_set(mut self, border_set: border::Set) -> Block<'a> {
self.border_set = border_set;
self
@@ -508,14 +538,15 @@ impl<'a> Block<'a> {
inner.x = inner.x.saturating_add(1).min(inner.right());
inner.width = inner.width.saturating_sub(1);
}
if self.borders.intersects(Borders::TOP) || !self.titles.is_empty() {
if self.borders.intersects(Borders::TOP) || self.have_title_at_position(Position::Top) {
inner.y = inner.y.saturating_add(1).min(inner.bottom());
inner.height = inner.height.saturating_sub(1);
}
if self.borders.intersects(Borders::RIGHT) {
inner.width = inner.width.saturating_sub(1);
}
if self.borders.intersects(Borders::BOTTOM) {
if self.borders.intersects(Borders::BOTTOM) || self.have_title_at_position(Position::Bottom)
{
inner.height = inner.height.saturating_sub(1);
}
@@ -532,6 +563,12 @@ impl<'a> Block<'a> {
inner
}
fn have_title_at_position(&self, position: Position) -> bool {
self.titles
.iter()
.any(|title| title.position.unwrap_or(self.titles_position) == position)
}
/// Defines the padding inside a `Block`.
///
/// See [`Padding`] for more information.
@@ -562,6 +599,7 @@ impl<'a> Block<'a> {
/// // │ content │
/// // └───────────┘
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub const fn padding(mut self, padding: Padding) -> Block<'a> {
self.padding = padding;
self
@@ -766,7 +804,7 @@ impl<'a> Styled for Block<'a> {
self.style
}
fn set_style(self, style: Style) -> Self::Item {
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
self.style(style)
}
}
@@ -939,6 +977,78 @@ mod tests {
);
}
#[test]
fn inner_takes_into_account_border_and_title() {
let test_rect = Rect::new(0, 0, 0, 2);
let top_top = Block::default()
.title(Title::from("Test").position(Position::Top))
.borders(Borders::TOP);
assert_eq!(top_top.inner(test_rect), Rect::new(0, 1, 0, 1));
let top_bot = Block::default()
.title(Title::from("Test").position(Position::Top))
.borders(Borders::BOTTOM);
assert_eq!(top_bot.inner(test_rect), Rect::new(0, 1, 0, 0));
let bot_top = Block::default()
.title(Title::from("Test").position(Position::Bottom))
.borders(Borders::TOP);
assert_eq!(bot_top.inner(test_rect), Rect::new(0, 1, 0, 0));
let bot_bot = Block::default()
.title(Title::from("Test").position(Position::Bottom))
.borders(Borders::BOTTOM);
assert_eq!(bot_bot.inner(test_rect), Rect::new(0, 0, 0, 1));
}
#[test]
fn have_title_at_position_takes_into_account_all_positioning_declarations() {
let block = Block::default();
assert!(!block.have_title_at_position(Position::Top));
assert!(!block.have_title_at_position(Position::Bottom));
let block = Block::default().title(Title::from("Test").position(Position::Top));
assert!(block.have_title_at_position(Position::Top));
assert!(!block.have_title_at_position(Position::Bottom));
let block = Block::default().title(Title::from("Test").position(Position::Bottom));
assert!(!block.have_title_at_position(Position::Top));
assert!(block.have_title_at_position(Position::Bottom));
let block = Block::default()
.title(Title::from("Test").position(Position::Top))
.title_position(Position::Bottom);
assert!(block.have_title_at_position(Position::Top));
assert!(!block.have_title_at_position(Position::Bottom));
let block = Block::default()
.title(Title::from("Test").position(Position::Bottom))
.title_position(Position::Top);
assert!(!block.have_title_at_position(Position::Top));
assert!(block.have_title_at_position(Position::Bottom));
let block = Block::default()
.title(Title::from("Test").position(Position::Top))
.title(Title::from("Test").position(Position::Bottom));
assert!(block.have_title_at_position(Position::Top));
assert!(block.have_title_at_position(Position::Bottom));
let block = Block::default()
.title(Title::from("Test").position(Position::Top))
.title(Title::from("Test"))
.title_position(Position::Bottom);
assert!(block.have_title_at_position(Position::Top));
assert!(block.have_title_at_position(Position::Bottom));
let block = Block::default()
.title(Title::from("Test"))
.title(Title::from("Test").position(Position::Bottom))
.title_position(Position::Top);
assert!(block.have_title_at_position(Position::Top));
assert!(block.have_title_at_position(Position::Bottom));
}
#[test]
fn border_type_can_be_const() {
const _PLAIN: border::Set = BorderType::border_symbols(BorderType::Plain);
@@ -997,15 +1107,60 @@ mod tests {
const _DEFAULT_STYLE: Style = Style::new();
const _DEFAULT_PADDING: Padding = Padding::uniform(1);
const _DEFAULT_BLOCK: Block = Block::new()
.title_style(_DEFAULT_STYLE)
// the following methods are no longer const because they use Into<Style>
// .style(_DEFAULT_STYLE) // no longer const
// .border_style(_DEFAULT_STYLE) // no longer const
// .title_style(_DEFAULT_STYLE) // no longer const
.title_alignment(Alignment::Left)
.title_position(Position::Top)
.borders(Borders::ALL)
.border_style(_DEFAULT_STYLE)
.style(_DEFAULT_STYLE)
.padding(_DEFAULT_PADDING);
}
/// This test ensures that we have some coverage on the Style::from() implementations
#[test]
fn block_style() {
// nominal style
let block = Block::default().style(Style::new().red());
assert_eq!(block.style, Style::new().red());
// auto-convert from Color
let block = Block::default().style(Color::Red);
assert_eq!(block.style, Style::new().red());
// auto-convert from (Color, Color)
let block = Block::default().style((Color::Red, Color::Blue));
assert_eq!(block.style, Style::new().red().on_blue());
// auto-convert from Modifier
let block = Block::default().style(Modifier::BOLD | Modifier::ITALIC);
assert_eq!(block.style, Style::new().bold().italic());
// auto-convert from (Modifier, Modifier)
let block = Block::default().style((Modifier::BOLD | Modifier::ITALIC, Modifier::DIM));
assert_eq!(block.style, Style::new().bold().italic().not_dim());
// auto-convert from (Color, Modifier)
let block = Block::default().style((Color::Red, Modifier::BOLD));
assert_eq!(block.style, Style::new().red().bold());
// auto-convert from (Color, Color, Modifier)
let block = Block::default().style((Color::Red, Color::Blue, Modifier::BOLD));
assert_eq!(block.style, Style::new().red().on_blue().bold());
// auto-convert from (Color, Color, Modifier, Modifier)
let block = Block::default().style((
Color::Red,
Color::Blue,
Modifier::BOLD | Modifier::ITALIC,
Modifier::DIM,
));
assert_eq!(
block.style,
Style::new().red().on_blue().bold().italic().not_dim()
);
}
#[test]
fn can_be_stylized() {
let block = Block::default().black().on_white().bold().not_dim();

View File

@@ -13,18 +13,15 @@ use std::collections::HashMap;
use time::{Date, Duration, OffsetDateTime};
use crate::{
buffer::Buffer,
layout::Rect,
style::Style,
text::Span,
prelude::*,
widgets::{Block, Widget},
};
/// Display a month calendar for the month containing `display_date`
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Monthly<'a, S: DateStyler> {
pub struct Monthly<'a, DS: DateStyler> {
display_date: Date,
events: S,
events: DS,
show_surrounding: Option<Style>,
show_weekday: Option<Style>,
show_month: Option<Style>,
@@ -32,9 +29,9 @@ pub struct Monthly<'a, S: DateStyler> {
block: Option<Block<'a>>,
}
impl<'a, S: DateStyler> Monthly<'a, S> {
impl<'a, DS: DateStyler> Monthly<'a, DS> {
/// Construct a calendar for the `display_date` and highlight the `events`
pub fn new(display_date: Date, events: S) -> Self {
pub fn new(display_date: Date, events: DS) -> Self {
Self {
display_date,
events,
@@ -49,32 +46,44 @@ impl<'a, S: DateStyler> Monthly<'a, S> {
/// Fill the calendar slots for days not in the current month also, this causes each line to be
/// completely filled. If there is an event style for a date, this style will be patched with
/// the event's style
pub fn show_surrounding(mut self, style: Style) -> Self {
self.show_surrounding = Some(style);
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
pub fn show_surrounding<S: Into<Style>>(mut self, style: S) -> Self {
self.show_surrounding = Some(style.into());
self
}
/// Display a header containing weekday abbreviations
pub fn show_weekdays_header(mut self, style: Style) -> Self {
self.show_weekday = Some(style);
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
pub fn show_weekdays_header<S: Into<Style>>(mut self, style: S) -> Self {
self.show_weekday = Some(style.into());
self
}
/// Display a header containing the month and year
pub fn show_month_header(mut self, style: Style) -> Self {
self.show_month = Some(style);
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
pub fn show_month_header<S: Into<Style>>(mut self, style: S) -> Self {
self.show_month = Some(style.into());
self
}
/// How to render otherwise unstyled dates
pub fn default_style(mut self, s: Style) -> Self {
self.default_style = s;
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
pub fn default_style<S: Into<Style>>(mut self, style: S) -> Self {
self.default_style = style.into();
self
}
/// Render the calendar within a [Block]
pub fn block(mut self, b: Block<'a>) -> Self {
self.block = Some(b);
pub fn block(mut self, block: Block<'a>) -> Self {
self.block = Some(block);
self
}
@@ -108,7 +117,7 @@ impl<'a, S: DateStyler> Monthly<'a, S> {
}
}
impl<'a, S: DateStyler> Widget for Monthly<'a, S> {
impl<'a, DS: DateStyler> Widget for Monthly<'a, DS> {
fn render(mut self, area: Rect, buf: &mut Buffer) {
// Block is used for borders and such
// Draw that first, and use the blank area inside the block for our own purposes
@@ -178,16 +187,22 @@ pub struct CalendarEventStore(pub HashMap<Date, Style>);
impl CalendarEventStore {
/// Construct a store that has the current date styled.
pub fn today(style: Style) -> Self {
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
pub fn today<S: Into<Style>>(style: S) -> Self {
let mut res = Self::default();
res.add(OffsetDateTime::now_local().unwrap().date(), style);
res.add(OffsetDateTime::now_local().unwrap().date(), style.into());
res
}
/// Add a date and style to the store
pub fn add(&mut self, date: Date, style: Style) {
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
pub fn add<S: Into<Style>>(&mut self, date: Date, style: S) {
// to simplify style nonsense, last write wins
let _ = self.0.insert(date, style);
let _ = self.0.insert(date, style.into());
}
/// Helper for trait impls

View File

@@ -457,7 +457,13 @@ impl<'a> Context<'a> {
/// ```
/// use ratatui::{prelude::*, widgets::canvas::*};
///
/// let ctx = Context::new(100, 100, [-180.0, 180.0], [-90.0, 90.0], symbols::Marker::Braille);
/// let ctx = Context::new(
/// 100,
/// 100,
/// [-180.0, 180.0],
/// [-90.0, 90.0],
/// symbols::Marker::Braille,
/// );
/// ```
pub fn new(
width: u16,
@@ -554,7 +560,10 @@ impl<'a> Context<'a> {
/// # Examples
///
/// ```
/// use ratatui::{style::Color, widgets::{*, canvas::*}};
/// use ratatui::{
/// style::Color,
/// widgets::{canvas::*, *},
/// };
///
/// Canvas::default()
/// .block(Block::default().title("Canvas").borders(Borders::ALL))
@@ -563,7 +572,7 @@ impl<'a> Context<'a> {
/// .paint(|ctx| {
/// ctx.draw(&Map {
/// resolution: MapResolution::High,
/// color: Color::White
/// color: Color::White,
/// });
/// ctx.layer();
/// ctx.draw(&Line {
@@ -578,7 +587,7 @@ impl<'a> Context<'a> {
/// y: 20.0,
/// width: 10.0,
/// height: 10.0,
/// color: Color::Red
/// color: Color::Red,
/// });
/// });
/// ```
@@ -666,10 +675,18 @@ where
/// ```
/// use ratatui::{prelude::*, widgets::canvas::*};
///
/// Canvas::default().marker(symbols::Marker::Braille).paint(|ctx| {});
/// Canvas::default().marker(symbols::Marker::HalfBlock).paint(|ctx| {});
/// Canvas::default().marker(symbols::Marker::Dot).paint(|ctx| {});
/// Canvas::default().marker(symbols::Marker::Block).paint(|ctx| {});
/// Canvas::default()
/// .marker(symbols::Marker::Braille)
/// .paint(|ctx| {});
/// Canvas::default()
/// .marker(symbols::Marker::HalfBlock)
/// .paint(|ctx| {});
/// Canvas::default()
/// .marker(symbols::Marker::Dot)
/// .paint(|ctx| {});
/// Canvas::default()
/// .marker(symbols::Marker::Block)
/// .paint(|ctx| {});
/// ```
pub fn marker(mut self, marker: symbols::Marker) -> Canvas<'a, F> {
self.marker = marker;

View File

@@ -128,7 +128,7 @@ mod tests {
let mut expected = Buffer::with_lines(expected_lines);
for cell in expected.content.iter_mut() {
if cell.symbol == "" {
if cell.symbol() == "" {
cell.set_style(Style::new().red());
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -66,6 +66,7 @@ impl<'a> Gauge<'a> {
///
/// The gauge is rendered in the inner portion of the block once space for borders and padding
/// is reserved. Styles set on the block do **not** affect the bar itself.
#[must_use = "method moves the value of self and returns the modified value"]
pub fn block(mut self, block: Block<'a>) -> Gauge<'a> {
self.block = Some(block);
self
@@ -80,6 +81,7 @@ impl<'a> Gauge<'a> {
/// # See also
///
/// See [`Gauge::ratio`] to set from a float.
#[must_use = "method moves the value of self and returns the modified value"]
pub fn percent(mut self, percent: u16) -> Gauge<'a> {
assert!(
percent <= 100,
@@ -101,6 +103,7 @@ impl<'a> Gauge<'a> {
/// # See also
///
/// See [`Gauge::percent`] to set from a percentage.
#[must_use = "method moves the value of self and returns the modified value"]
pub fn ratio(mut self, ratio: f64) -> Gauge<'a> {
assert!(
(0.0..=1.0).contains(&ratio),
@@ -114,6 +117,7 @@ impl<'a> Gauge<'a> {
///
/// For a left-aligned label, see [`LineGauge`].
/// If the label is not defined, it is the percentage filled.
#[must_use = "method moves the value of self and returns the modified value"]
pub fn label<T>(mut self, label: T) -> Gauge<'a>
where
T: Into<Span<'a>>,
@@ -124,16 +128,24 @@ impl<'a> Gauge<'a> {
/// Sets the widget style.
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// This will style the block (if any non-styled) and background of the widget (everything
/// except the bar itself). [`Block`] style set with [`Gauge::block`] takes precedence.
pub fn style(mut self, style: Style) -> Gauge<'a> {
self.style = style;
#[must_use = "method moves the value of self and returns the modified value"]
pub fn style<S: Into<Style>>(mut self, style: S) -> Gauge<'a> {
self.style = style.into();
self
}
/// Sets the style of the bar.
pub fn gauge_style(mut self, style: Style) -> Gauge<'a> {
self.gauge_style = style;
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
#[must_use = "method moves the value of self and returns the modified value"]
pub fn gauge_style<S: Into<Style>>(mut self, style: S) -> Gauge<'a> {
self.gauge_style = style.into();
self
}
@@ -142,6 +154,7 @@ impl<'a> Gauge<'a> {
/// This enables the use of
/// [unicode block characters](https://en.wikipedia.org/wiki/Block_Elements).
/// This is useful to display a higher precision bar (8 extra fractional parts per cell).
#[must_use = "method moves the value of self and returns the modified value"]
pub fn use_unicode(mut self, unicode: bool) -> Gauge<'a> {
self.use_unicode = unicode;
self
@@ -265,6 +278,7 @@ pub struct LineGauge<'a> {
impl<'a> LineGauge<'a> {
/// Surrounds the `LineGauge` with a [`Block`].
#[must_use = "method moves the value of self and returns the modified value"]
pub fn block(mut self, block: Block<'a>) -> Self {
self.block = Some(block);
self
@@ -278,6 +292,7 @@ impl<'a> LineGauge<'a> {
/// # Panics
///
/// This method panics if `ratio` is **not** between 0 and 1 inclusively.
#[must_use = "method moves the value of self and returns the modified value"]
pub fn ratio(mut self, ratio: f64) -> Self {
assert!(
(0.0..=1.0).contains(&ratio),
@@ -294,6 +309,7 @@ impl<'a> LineGauge<'a> {
/// See [`symbols::line::Set`] for more information. Predefined sets are also available, see
/// [`NORMAL`](symbols::line::NORMAL), [`DOUBLE`](symbols::line::DOUBLE) and
/// [`THICK`](symbols::line::THICK).
#[must_use = "method moves the value of self and returns the modified value"]
pub fn line_set(mut self, set: symbols::line::Set) -> Self {
self.line_set = set;
self
@@ -313,16 +329,24 @@ impl<'a> LineGauge<'a> {
/// Sets the widget style.
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// This will style everything except the bar itself, so basically the block (if any) and
/// background.
pub fn style(mut self, style: Style) -> Self {
self.style = style;
#[must_use = "method moves the value of self and returns the modified value"]
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = style.into();
self
}
/// Sets the style of the bar.
pub fn gauge_style(mut self, style: Style) -> Self {
self.gauge_style = style;
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
#[must_use = "method moves the value of self and returns the modified value"]
pub fn gauge_style<S: Into<Style>>(mut self, style: S) -> Self {
self.gauge_style = style.into();
self
}
}
@@ -394,7 +418,7 @@ impl<'a> Styled for Gauge<'a> {
self.style
}
fn set_style(self, style: Style) -> Self::Item {
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
self.style(style)
}
}
@@ -406,7 +430,7 @@ impl<'a> Styled for LineGauge<'a> {
self.style
}
fn set_style(self, style: Style) -> Self::Item {
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
self.style(style)
}
}
@@ -419,19 +443,19 @@ mod tests {
#[test]
#[should_panic]
fn gauge_invalid_percentage() {
Gauge::default().percent(110);
let _ = Gauge::default().percent(110);
}
#[test]
#[should_panic]
fn gauge_invalid_ratio_upper_bound() {
Gauge::default().ratio(1.1);
let _ = Gauge::default().ratio(1.1);
}
#[test]
#[should_panic]
fn gauge_invalid_ratio_lower_bound() {
Gauge::default().ratio(-0.5);
let _ = Gauge::default().ratio(-0.5);
}
#[test]

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,10 @@
use unicode_width::UnicodeWidthStr;
use crate::{
buffer::Buffer,
layout::{Alignment, Rect},
style::{Style, Styled},
text::{StyledGrapheme, Text},
prelude::*,
text::StyledGrapheme,
widgets::{
reflow::{LineComposer, LineTruncator, WordWrapper},
reflow::{LineComposer, LineTruncator, WordWrapper, WrappedLine},
Block, Widget,
},
};
@@ -29,16 +27,14 @@ fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Alignment)
/// let text = vec![
/// Line::from(vec![
/// Span::raw("First"),
/// Span::styled("line",Style::new().green().italic()),
/// Span::styled("line", Style::new().green().italic()),
/// ".".into(),
/// ]),
/// Line::from("Second line".red()),
/// "Third line".into(),
/// ];
/// Paragraph::new(text)
/// .block(Block::new()
/// .title("Paragraph")
/// .borders(Borders::ALL))
/// .block(Block::new().title("Paragraph").borders(Borders::ALL))
/// .style(Style::new().white().on_black())
/// .alignment(Alignment::Center)
/// .wrap(Wrap { trim: true });
@@ -66,9 +62,11 @@ pub struct Paragraph<'a> {
/// ```
/// use ratatui::{prelude::*, widgets::*};
///
/// let bullet_points = Text::from(r#"Some indented points:
/// let bullet_points = Text::from(
/// r#"Some indented points:
/// - First thing goes here and is long so that it wraps
/// - Here is another point that is long enough to wrap"#);
/// - Here is another point that is long enough to wrap"#,
/// );
///
/// // With leading spaces trimmed (window width of 30 chars):
/// Paragraph::new(bullet_points.clone()).wrap(Wrap { trim: true });
@@ -108,10 +106,8 @@ impl<'a> Paragraph<'a> {
/// 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()]));
/// 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
@@ -134,10 +130,9 @@ impl<'a> Paragraph<'a> {
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let paragraph = Paragraph::new("Hello, world!")
/// .block(Block::default()
/// .title("Paragraph")
/// .borders(Borders::ALL));
/// .block(Block::default().title("Paragraph").borders(Borders::ALL));
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn block(mut self, block: Block<'a>) -> Paragraph<'a> {
self.block = Some(block);
self
@@ -145,6 +140,9 @@ impl<'a> Paragraph<'a> {
/// Sets the style of the entire widget.
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// 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.
///
@@ -152,11 +150,11 @@ impl<'a> Paragraph<'a> {
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let paragraph = Paragraph::new("Hello, world!")
/// .style(Style::new().red().on_white());
/// let paragraph = Paragraph::new("Hello, world!").style(Style::new().red().on_white());
/// ```
pub fn style(mut self, style: Style) -> Paragraph<'a> {
self.style = style;
#[must_use = "method moves the value of self and returns the modified value"]
pub fn style<S: Into<Style>>(mut self, style: S) -> Paragraph<'a> {
self.style = style.into();
self
}
@@ -168,9 +166,9 @@ impl<'a> Paragraph<'a> {
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let paragraph = Paragraph::new("Hello, world!")
/// .wrap(Wrap { trim: true });
/// let paragraph = Paragraph::new("Hello, world!").wrap(Wrap { trim: true });
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn wrap(mut self, wrap: Wrap) -> Paragraph<'a> {
self.wrap = Some(wrap);
self
@@ -187,6 +185,7 @@ impl<'a> Paragraph<'a> {
///
/// 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.
#[must_use = "method moves the value of self and returns the modified value"]
pub fn scroll(mut self, offset: (Vertical, Horizontal)) -> Paragraph<'a> {
self.scroll = offset;
self
@@ -201,13 +200,84 @@ impl<'a> Paragraph<'a> {
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let paragraph = Paragraph::new("Hello World")
/// .alignment(Alignment::Center);
/// let paragraph = Paragraph::new("Hello World").alignment(Alignment::Center);
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn alignment(mut self, alignment: Alignment) -> Paragraph<'a> {
self.alignment = alignment;
self
}
/// Calculates the number of lines needed to fully render.
///
/// Given a max line width, this method calculates the number of lines that a paragraph will
/// need in order to be fully rendered. For paragraphs that do not use wrapping, this count is
/// simply the number of lines present in the paragraph.
///
/// # Example
///
/// ```ignore
/// # use ratatui::{prelude::*, widgets::*};
/// let paragraph = Paragraph::new("Hello World")
/// .wrap(Wrap { trim: false });
/// assert_eq!(paragraph.line_count(20), 1);
/// assert_eq!(paragraph.line_count(10), 2);
/// ```
#[stability::unstable(
feature = "rendered-line-info",
reason = "The design for text wrapping is not stable and might affect this API.",
issue = "https://github.com/ratatui-org/ratatui/issues/293"
)]
pub fn line_count(&self, width: u16) -> usize {
if width < 1 {
return 0;
}
if let Some(Wrap { trim }) = self.wrap {
let styled = self.text.lines.iter().map(|line| {
let graphemes = line
.spans
.iter()
.flat_map(|span| span.styled_graphemes(self.style));
let alignment = line.alignment.unwrap_or(self.alignment);
(graphemes, alignment)
});
let mut line_composer = WordWrapper::new(styled, width, trim);
let mut count = 0;
while line_composer.next_line().is_some() {
count += 1;
}
count
} else {
self.text.lines.len()
}
}
/// Calculates the shortest line width needed to avoid any word being wrapped or truncated.
///
/// # Example
///
/// ```ignore
/// # use ratatui::{prelude::*, widgets::*};
/// let paragraph = Paragraph::new("Hello World");
/// assert_eq!(paragraph.line_width(), 11);
///
/// let paragraph = Paragraph::new("Hello World\nhi\nHello World!!!");
/// assert_eq!(paragraph.line_width(), 14);
/// ```
#[stability::unstable(
feature = "rendered-line-info",
reason = "The design for text wrapping is not stable and might affect this API.",
issue = "https://github.com/ratatui-org/ratatui/issues/293"
)]
pub fn line_width(&self) -> usize {
self.text
.lines
.iter()
.map(|l| l.width())
.max()
.unwrap_or_default()
}
}
impl<'a> Widget for Paragraph<'a> {
@@ -226,49 +296,53 @@ impl<'a> Widget for Paragraph<'a> {
return;
}
let style = self.style;
let styled = self.text.lines.iter().map(|line| {
(
line.spans
.iter()
.flat_map(|span| span.styled_graphemes(style)),
line.alignment.unwrap_or(self.alignment),
)
let graphemes = line
.spans
.iter()
.flat_map(|span| span.styled_graphemes(self.style));
let alignment = line.alignment.unwrap_or(self.alignment);
(graphemes, alignment)
});
let mut line_composer: Box<dyn LineComposer> = if let Some(Wrap { trim }) = self.wrap {
Box::new(WordWrapper::new(styled, text_area.width, trim))
if let Some(Wrap { trim }) = self.wrap {
let line_composer = WordWrapper::new(styled, text_area.width, trim);
self.render_text(line_composer, text_area, buf);
} else {
let mut line_composer = Box::new(LineTruncator::new(styled, text_area.width));
let mut line_composer = LineTruncator::new(styled, text_area.width);
line_composer.set_horizontal_offset(self.scroll.1);
line_composer
};
self.render_text(line_composer, text_area, buf);
}
}
}
impl<'a> Paragraph<'a> {
fn render_text<C: LineComposer<'a>>(&self, mut composer: C, area: Rect, buf: &mut Buffer) {
let mut y = 0;
while let Some((current_line, current_line_width, current_line_alignment)) =
line_composer.next_line()
while let Some(WrappedLine {
line: current_line,
width: current_line_width,
alignment: current_line_alignment,
}) = composer.next_line()
{
if y >= self.scroll.0 {
let mut x =
get_line_offset(current_line_width, text_area.width, current_line_alignment);
let mut x = get_line_offset(current_line_width, area.width, current_line_alignment);
for StyledGrapheme { symbol, style } in current_line {
let width = symbol.width();
if width == 0 {
continue;
}
buf.get_mut(text_area.left() + x, text_area.top() + y - self.scroll.0)
.set_symbol(if symbol.is_empty() {
// If the symbol is empty, the last char which rendered last time will
// leave on the line. It's a quick fix.
" "
} else {
symbol
})
// If the symbol is empty, the last char which rendered last time will
// leave on the line. It's a quick fix.
let symbol = if symbol.is_empty() { " " } else { symbol };
buf.get_mut(area.left() + x, area.top() + y - self.scroll.0)
.set_symbol(symbol)
.set_style(*style);
x += width as u16;
}
}
y += 1;
if y >= text_area.height + self.scroll.0 {
if y >= area.height + self.scroll.0 {
break;
}
}
@@ -282,7 +356,7 @@ impl<'a> Styled for Paragraph<'a> {
self.style
}
fn set_style(self, style: Style) -> Self::Item {
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
self.style(style)
}
}
@@ -294,7 +368,7 @@ mod test {
backend::TestBackend,
style::{Color, Modifier, Stylize},
text::{Line, Span},
widgets::Borders,
widgets::{block::Position, Borders},
Terminal,
};
@@ -477,6 +551,20 @@ mod test {
);
}
#[test]
fn test_render_paragraph_with_block_with_bottom_title_and_border() {
let block = Block::default()
.title("Title")
.title_position(Position::Bottom)
.borders(Borders::BOTTOM);
let paragraph = Paragraph::new("Hello, world!").block(block);
test_case(
&paragraph,
Buffer::with_lines(vec!["Hello, world! ", "Title──────────"]),
);
}
#[test]
fn test_render_paragraph_with_word_wrap() {
let text = "This is a long line of text that should wrap and contains a superultramegagigalong word.";
@@ -792,4 +880,46 @@ mod test {
.remove_modifier(Modifier::DIM)
)
}
#[test]
fn widgets_paragraph_count_rendered_lines() {
let paragraph = Paragraph::new("Hello World");
assert_eq!(paragraph.line_count(20), 1);
assert_eq!(paragraph.line_count(10), 1);
let paragraph = Paragraph::new("Hello World").wrap(Wrap { trim: false });
assert_eq!(paragraph.line_count(20), 1);
assert_eq!(paragraph.line_count(10), 2);
let paragraph = Paragraph::new("Hello World").wrap(Wrap { trim: true });
assert_eq!(paragraph.line_count(20), 1);
assert_eq!(paragraph.line_count(10), 2);
let text = "Hello World ".repeat(100);
let paragraph = Paragraph::new(text.trim());
assert_eq!(paragraph.line_count(11), 1);
assert_eq!(paragraph.line_count(6), 1);
let paragraph = paragraph.wrap(Wrap { trim: false });
assert_eq!(paragraph.line_count(11), 100);
assert_eq!(paragraph.line_count(6), 200);
let paragraph = paragraph.wrap(Wrap { trim: true });
assert_eq!(paragraph.line_count(11), 100);
assert_eq!(paragraph.line_count(6), 200);
}
#[test]
fn widgets_paragraph_line_width() {
let paragraph = Paragraph::new("Hello World");
assert_eq!(paragraph.line_width(), 11);
let paragraph = Paragraph::new("Hello World").wrap(Wrap { trim: false });
assert_eq!(paragraph.line_width(), 11);
let paragraph = Paragraph::new("Hello World").wrap(Wrap { trim: true });
assert_eq!(paragraph.line_width(), 11);
let text = "Hello World ".repeat(100);
let paragraph = Paragraph::new(text);
assert_eq!(paragraph.line_width(), 1200);
let paragraph = paragraph.wrap(Wrap { trim: false });
assert_eq!(paragraph.line_width(), 1200);
let paragraph = paragraph.wrap(Wrap { trim: true });
assert_eq!(paragraph.line_width(), 1200);
}
}

View File

@@ -11,7 +11,16 @@ const NBSP: &str = "\u{00a0}";
/// Cannot implement it as Iterator since it yields slices of the internal buffer (need streaming
/// iterators for that).
pub trait LineComposer<'a> {
fn next_line(&mut self) -> Option<(&[StyledGrapheme<'a>], u16, Alignment)>;
fn next_line<'lend>(&'lend mut self) -> Option<WrappedLine<'lend, 'a>>;
}
pub struct WrappedLine<'lend, 'text> {
/// One line reflowed to the correct width
pub line: &'lend [StyledGrapheme<'text>],
/// The width of the line
pub width: u16,
/// Whether the line was aligned left or right
pub alignment: Alignment,
}
/// A state machine that wraps lines on word boundaries.
@@ -56,7 +65,7 @@ where
O: Iterator<Item = (I, Alignment)>,
I: Iterator<Item = StyledGrapheme<'a>>,
{
fn next_line(&mut self) -> Option<(&[StyledGrapheme<'a>], u16, Alignment)> {
fn next_line<'lend>(&'lend mut self) -> Option<WrappedLine<'lend, 'a>> {
if self.max_line_width == 0 {
return None;
}
@@ -200,7 +209,11 @@ where
if let Some(line) = current_line {
self.current_line = line;
Some((&self.current_line[..], line_width, self.current_alignment))
Some(WrappedLine {
line: &self.current_line[..],
width: line_width,
alignment: self.current_alignment,
})
} else {
None
}
@@ -249,7 +262,7 @@ where
O: Iterator<Item = (I, Alignment)>,
I: Iterator<Item = StyledGrapheme<'a>>,
{
fn next_line(&mut self) -> Option<(&[StyledGrapheme<'a>], u16, Alignment)> {
fn next_line<'lend>(&'lend mut self) -> Option<WrappedLine<'lend, 'a>> {
if self.max_line_width == 0 {
return None;
}
@@ -296,11 +309,11 @@ where
if lines_exhausted {
None
} else {
Some((
&self.current_line[..],
current_line_width,
current_alignment,
))
Some(WrappedLine {
line: &self.current_line[..],
width: current_line_width,
alignment: current_alignment,
})
}
}
}
@@ -360,7 +373,12 @@ mod test {
let mut lines = vec![];
let mut widths = vec![];
let mut alignments = vec![];
while let Some((styled, width, alignment)) = composer.next_line() {
while let Some(WrappedLine {
line: styled,
width,
alignment,
}) = composer.next_line()
{
let line = styled
.iter()
.map(|StyledGrapheme { symbol, .. }| *symbol)

View File

@@ -2,9 +2,7 @@ use strum::{Display, EnumString};
use super::StatefulWidget;
use crate::{
buffer::Buffer,
layout::Rect,
style::Style,
prelude::*,
symbols::scrollbar::{Set, DOUBLE_HORIZONTAL, DOUBLE_VERTICAL},
};
@@ -62,18 +60,21 @@ impl ScrollbarState {
}
}
/// Sets the scroll position of the scrollbar and returns the modified ScrollbarState.
#[must_use = "method moves the value of self and returns the modified value"]
pub fn position(mut self, position: usize) -> Self {
self.position = position;
self
}
/// Sets the length of the scrollable content and returns the modified ScrollbarState.
#[must_use = "method moves the value of self and returns the modified value"]
pub fn content_length(mut self, content_length: usize) -> Self {
self.content_length = content_length;
self
}
/// Sets the length of the viewport content and returns the modified ScrollbarState.
#[must_use = "method moves the value of self and returns the modified value"]
pub fn viewport_content_length(mut self, viewport_content_length: usize) -> Self {
self.viewport_content_length = viewport_content_length;
self
@@ -147,7 +148,11 @@ pub enum ScrollbarOrientation {
///
/// let vertical_scroll = 0; // from app state
///
/// let items = vec![Line::from("Item 1"), Line::from("Item 2"), Line::from("Item 3")];
/// 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
@@ -160,12 +165,14 @@ pub enum ScrollbarOrientation {
///
/// let area = frame.size();
/// frame.render_widget(paragraph, area);
/// frame.render_stateful_widget(scrollbar,
/// 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);
/// &mut scrollbar_state,
/// );
/// # }
/// ```
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
@@ -204,6 +211,7 @@ impl<'a> Scrollbar<'a> {
/// Sets the orientation of the scrollbar.
/// Resets the symbols to [`DOUBLE_VERTICAL`] or [`DOUBLE_HORIZONTAL`] based on orientation
#[must_use = "method moves the value of self and returns the modified value"]
pub fn orientation(mut self, orientation: ScrollbarOrientation) -> Self {
self.orientation = orientation;
let set = if self.is_vertical() {
@@ -215,56 +223,77 @@ impl<'a> Scrollbar<'a> {
}
/// Sets the orientation and symbols for the scrollbar from a [`Set`].
#[must_use = "method moves the value of self and returns the modified value"]
pub fn orientation_and_symbol(mut self, orientation: ScrollbarOrientation, set: Set) -> Self {
self.orientation = orientation;
self.symbols(set)
}
/// Sets the symbol that represents the thumb of the scrollbar.
#[must_use = "method moves the value of self and returns the modified value"]
pub fn thumb_symbol(mut self, thumb_symbol: &'a str) -> Self {
self.thumb_symbol = thumb_symbol;
self
}
/// Sets the style that represents the thumb of the scrollbar.
pub fn thumb_style(mut self, thumb_style: Style) -> Self {
self.thumb_style = thumb_style;
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
#[must_use = "method moves the value of self and returns the modified value"]
pub fn thumb_style<S: Into<Style>>(mut self, thumb_style: S) -> Self {
self.thumb_style = thumb_style.into();
self
}
/// Sets the symbol that represents the track of the scrollbar.
#[must_use = "method moves the value of self and returns the modified value"]
pub fn track_symbol(mut self, track_symbol: Option<&'a str>) -> Self {
self.track_symbol = track_symbol;
self
}
/// Sets the style that is used for the track of the scrollbar.
pub fn track_style(mut self, track_style: Style) -> Self {
self.track_style = track_style;
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
#[must_use = "method moves the value of self and returns the modified value"]
pub fn track_style<S: Into<Style>>(mut self, track_style: S) -> Self {
self.track_style = track_style.into();
self
}
/// Sets the symbol that represents the beginning of the scrollbar.
#[must_use = "method moves the value of self and returns the modified value"]
pub fn begin_symbol(mut self, begin_symbol: Option<&'a str>) -> Self {
self.begin_symbol = begin_symbol;
self
}
/// Sets the style that is used for the beginning of the scrollbar.
pub fn begin_style(mut self, begin_style: Style) -> Self {
self.begin_style = begin_style;
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
#[must_use = "method moves the value of self and returns the modified value"]
pub fn begin_style<S: Into<Style>>(mut self, begin_style: S) -> Self {
self.begin_style = begin_style.into();
self
}
/// Sets the symbol that represents the end of the scrollbar.
#[must_use = "method moves the value of self and returns the modified value"]
pub fn end_symbol(mut self, end_symbol: Option<&'a str>) -> Self {
self.end_symbol = end_symbol;
self
}
/// Sets the style that is used for the end of the scrollbar.
pub fn end_style(mut self, end_style: Style) -> Self {
self.end_style = end_style;
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
#[must_use = "method moves the value of self and returns the modified value"]
pub fn end_style<S: Into<Style>>(mut self, end_style: S) -> Self {
self.end_style = end_style.into();
self
}
@@ -281,6 +310,7 @@ impl<'a> Scrollbar<'a> {
///
/// Only sets begin_symbol, end_symbol and track_symbol if they already contain a value.
/// If they were set to `None` explicitly, this function will respect that choice.
#[must_use = "method moves the value of self and returns the modified value"]
pub fn symbols(mut self, symbol: Set) -> Self {
self.thumb_symbol = symbol.thumb;
if self.track_symbol.is_some() {
@@ -296,6 +326,10 @@ impl<'a> Scrollbar<'a> {
}
/// Sets the style used for the various parts of the scrollbar from a [`Style`].
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// ```text
/// <--▮------->
/// ^ ^ ^ ^
@@ -304,7 +338,9 @@ impl<'a> Scrollbar<'a> {
/// │ └──────── thumb
/// └─────────── begin
/// ```
pub fn style(mut self, style: Style) -> Self {
#[must_use = "method moves the value of self and returns the modified value"]
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
let style = style.into();
self.track_style = style;
self.thumb_style = style;
self.begin_style = style;

View File

@@ -1,17 +1,27 @@
#![warn(missing_docs)]
use std::cmp::min;
use strum::{Display, EnumString};
use crate::{
buffer::Buffer,
layout::Rect,
style::{Style, Styled},
symbols,
prelude::*,
widgets::{Block, Widget},
};
/// Widget to render a sparkline over one or more lines.
///
/// You can create a `Sparkline` using [`Sparkline::default`].
///
/// `Sparkline` can be styled either using [`Sparkline::style`] or preferably using the methods
/// provided by the [`Stylize`](crate::style::Stylize) trait.
///
/// # Setter methods
///
/// - [`Sparkline::block`] wraps the sparkline in a [`Block`]
/// - [`Sparkline::data`] defines the dataset, you'll almost always want to use it
/// - [`Sparkline::max`] sets the maximum value of bars
/// - [`Sparkline::direction`] sets the render direction
///
/// # Examples
///
/// ```
@@ -21,7 +31,8 @@ use crate::{
/// .block(Block::default().title("Sparkline").borders(Borders::ALL))
/// .data(&[0, 2, 3, 4, 1, 4, 10])
/// .max(5)
/// .style(Style::default().fg(Color::Red).bg(Color::White));
/// .direction(RenderDirection::RightToLeft)
/// .style(Style::default().red().on_white());
/// ```
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Sparkline<'a> {
@@ -40,10 +51,15 @@ pub struct Sparkline<'a> {
direction: RenderDirection,
}
/// Defines the direction in which sparkline will be rendered.
///
/// See [`Sparkline::direction`].
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
pub enum RenderDirection {
/// The first value is on the left, going to the right
#[default]
LeftToRight,
/// The first value is on the right, going to the left
RightToLeft,
}
@@ -61,31 +77,67 @@ impl<'a> Default for Sparkline<'a> {
}
impl<'a> Sparkline<'a> {
/// Wraps the sparkline with the given `block`.
#[must_use = "method moves the value of self and returns the modified value"]
pub fn block(mut self, block: Block<'a>) -> Sparkline<'a> {
self.block = Some(block);
self
}
pub fn style(mut self, style: Style) -> Sparkline<'a> {
self.style = style;
/// Sets the style of the entire widget.
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// The foreground corresponds to the bars while the background is everything else.
#[must_use = "method moves the value of self and returns the modified value"]
pub fn style<S: Into<Style>>(mut self, style: S) -> Sparkline<'a> {
self.style = style.into();
self
}
/// Sets the dataset for the sparkline.
///
/// # Example
///
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// # fn ui(frame: &mut Frame) {
/// # let area = Rect::default();
/// let sparkline = Sparkline::default().data(&[1, 2, 3]);
/// frame.render_widget(sparkline, area);
/// # }
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn data(mut self, data: &'a [u64]) -> Sparkline<'a> {
self.data = data;
self
}
/// Sets the maximum value of bars.
///
/// Every bar will be scaled accordingly. If no max is given, this will be the max in the
/// dataset.
#[must_use = "method moves the value of self and returns the modified value"]
pub fn max(mut self, max: u64) -> Sparkline<'a> {
self.max = Some(max);
self
}
/// Sets the characters used to display the bars.
///
/// Can be [`symbols::bar::THREE_LEVELS`], [`symbols::bar::NINE_LEVELS`] (default) or a custom
/// [`Set`](symbols::bar::Set).
#[must_use = "method moves the value of self and returns the modified value"]
pub fn bar_set(mut self, bar_set: symbols::bar::Set) -> Sparkline<'a> {
self.bar_set = bar_set;
self
}
/// Sets the direction of the sparkline.
///
/// [`RenderDirection::LeftToRight`] by default.
#[must_use = "method moves the value of self and returns the modified value"]
pub fn direction(mut self, direction: RenderDirection) -> Sparkline<'a> {
self.direction = direction;
self
@@ -99,7 +151,7 @@ impl<'a> Styled for Sparkline<'a> {
self.style
}
fn set_style(self, style: Style) -> Self::Item {
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
self.style(style)
}
}

View File

@@ -1,166 +1,17 @@
#![warn(missing_docs)]
use strum::{Display, EnumString};
use unicode_width::UnicodeWidthStr;
use crate::{
buffer::Buffer,
layout::{Alignment, Constraint, Direction, Layout, Rect, SegmentSize},
style::{Style, Styled},
text::Text,
widgets::{Block, StatefulWidget, Widget},
};
mod cell;
mod row;
#[allow(clippy::module_inception)]
mod table;
mod table_state;
/// A [`Cell`] contains the [`Text`] to be displayed in a [`Row`] of a [`Table`].
///
/// It can be created from anything that can be converted to a [`Text`].
/// ```rust
/// use std::borrow::Cow;
/// use ratatui::{prelude::*, widgets::*};
///
/// Cell::from("simple string");
///
/// Cell::from(Span::from("span"));
///
/// Cell::from(Line::from(vec![
/// Span::raw("a vec of "),
/// Span::styled("spans", Style::default().add_modifier(Modifier::BOLD))
/// ]));
///
/// Cell::from(Text::from("a text"));
///
/// Cell::from(Text::from(Cow::Borrowed("hello")));
/// ```
///
/// You can apply a [`Style`] on the entire [`Cell`] using [`Cell::style`] or rely on the styling
/// capabilities of [`Text`].
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Cell<'a> {
content: Text<'a>,
style: Style,
}
impl<'a> Cell<'a> {
/// Set the `Style` of this cell.
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
}
impl<'a, T> From<T> for Cell<'a>
where
T: Into<Text<'a>>,
{
fn from(content: T) -> Cell<'a> {
Cell {
content: content.into(),
style: Style::default(),
}
}
}
impl<'a> Styled for Cell<'a> {
type Item = Cell<'a>;
fn style(&self) -> Style {
self.style
}
fn set_style(self, style: Style) -> Self::Item {
self.style(style)
}
}
/// Holds data to be displayed in a [`Table`] widget.
///
/// A [`Row`] is a collection of cells. It can be created from simple strings:
/// ```rust
/// use ratatui::{prelude::*, widgets::*};
///
/// Row::new(vec!["Cell1", "Cell2", "Cell3"]);
/// ```
///
/// But if you need a bit more control over individual cells, you can explicitly create [`Cell`]s:
/// ```rust
/// use ratatui::{prelude::*, widgets::*};
///
/// Row::new(vec![
/// Cell::from("Cell1"),
/// Cell::from("Cell2").style(Style::default().fg(Color::Yellow)),
/// ]);
/// ```
///
/// You can also construct a row from any type that can be converted into [`Text`]:
/// ```rust
/// use std::borrow::Cow;
/// use ratatui::{prelude::*, widgets::*};
///
/// Row::new(vec![
/// Cow::Borrowed("hello"),
/// Cow::Owned("world".to_uppercase()),
/// ]);
/// ```
///
/// By default, a row has a height of 1 but you can change this using [`Row::height`].
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Row<'a> {
cells: Vec<Cell<'a>>,
height: u16,
style: Style,
bottom_margin: u16,
}
impl<'a> Row<'a> {
/// Creates a new [`Row`] from an iterator where items can be converted to a [`Cell`].
pub fn new<T>(cells: T) -> Self
where
T: IntoIterator,
T::Item: Into<Cell<'a>>,
{
Self {
height: 1,
cells: cells.into_iter().map(Into::into).collect(),
style: Style::default(),
bottom_margin: 0,
}
}
/// Set the fixed height of the [`Row`]. Any [`Cell`] whose content has more lines than this
/// height will see its content truncated.
pub fn height(mut self, height: u16) -> Self {
self.height = height;
self
}
/// Set the [`Style`] of the entire row. This [`Style`] can be overridden by the [`Style`] of a
/// any individual [`Cell`] or event by their [`Text`] content.
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
/// Set the bottom margin. By default, the bottom margin is `0`.
pub fn bottom_margin(mut self, margin: u16) -> Self {
self.bottom_margin = margin;
self
}
/// Returns the total height of the row.
fn total_height(&self) -> u16 {
self.height.saturating_add(self.bottom_margin)
}
}
impl<'a> Styled for Row<'a> {
type Item = Row<'a>;
fn style(&self) -> Style {
self.style
}
fn set_style(self, style: Style) -> Self::Item {
self.style(style)
}
}
pub use cell::Cell;
pub use row::Row;
pub use table::Table;
pub use table_state::TableState;
/// This option allows the user to configure the "highlight symbol" column width spacing
#[derive(Debug, Display, EnumString, PartialEq, Eq, Clone, Default, Hash)]
@@ -170,12 +21,14 @@ pub enum HighlightSpacing {
/// With this variant, the column for the selection symbol will always be allocated, and so the
/// table will never change size, regardless of if a row is selected or not
Always,
/// Only add spacing for the selection symbol column if a row is selected
///
/// With this variant, the column for the selection symbol will only be allocated if there is a
/// selection, causing the table to shift if selected / unselected
#[default]
WhenSelected,
/// Never add spacing to the selection symbol column, regardless of whether something is
/// selected or not
///
@@ -184,659 +37,23 @@ pub enum HighlightSpacing {
}
impl HighlightSpacing {
/// Determine if a selection should be done, based on variant
/// Input "selection_state" should be similar to `state.selected.is_some()`
pub fn should_add(&self, selection_state: bool) -> bool {
/// Determine if a selection column should be displayed
///
/// has_selection: true if a row is selected in the table
///
/// Returns true if a selection column should be displayed
pub(crate) fn should_add(&self, has_selection: bool) -> bool {
match self {
HighlightSpacing::Always => true,
HighlightSpacing::WhenSelected => selection_state,
HighlightSpacing::WhenSelected => has_selection,
HighlightSpacing::Never => false,
}
}
}
/// A widget to display data in formatted columns.
///
/// It is a collection of [`Row`]s, themselves composed of [`Cell`]s:
/// ```rust
/// use ratatui::{prelude::*, widgets::*};
///
/// Table::new(vec![
/// // Row can be created from simple strings.
/// Row::new(vec!["Row11", "Row12", "Row13"]),
/// // You can style the entire row.
/// Row::new(vec!["Row21", "Row22", "Row23"]).style(Style::default().fg(Color::Blue)),
/// // If you need more control over the styling you may need to create Cells directly
/// Row::new(vec![
/// Cell::from("Row31"),
/// Cell::from("Row32").style(Style::default().fg(Color::Yellow)),
/// Cell::from(Line::from(vec![
/// Span::raw("Row"),
/// Span::styled("33", Style::default().fg(Color::Green))
/// ])),
/// ]),
/// // If a Row need to display some content over multiple lines, you just have to change
/// // its height.
/// Row::new(vec![
/// Cell::from("Row\n41"),
/// Cell::from("Row\n42"),
/// Cell::from("Row\n43"),
/// ]).height(2),
/// ])
/// // You can set the style of the entire Table.
/// .style(Style::default().fg(Color::White))
/// // It has an optional header, which is simply a Row always visible at the top.
/// .header(
/// Row::new(vec!["Col1", "Col2", "Col3"])
/// .style(Style::default().fg(Color::Yellow))
/// // If you want some space between the header and the rest of the rows, you can always
/// // specify some margin at the bottom.
/// .bottom_margin(1)
/// )
/// // As any other widget, a Table can be wrapped in a Block.
/// .block(Block::default().title("Table"))
/// // Columns widths are constrained in the same way as Layout...
/// .widths(&[Constraint::Length(5), Constraint::Length(5), Constraint::Length(10)])
/// // ...and they can be separated by a fixed spacing.
/// .column_spacing(1)
/// // If you wish to highlight a row in any specific way when it is selected...
/// .highlight_style(Style::default().add_modifier(Modifier::BOLD))
/// // ...and potentially show a symbol in front of the selection.
/// .highlight_symbol(">>");
/// ```
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Table<'a> {
/// A block to wrap the widget in
block: Option<Block<'a>>,
/// Base style for the widget
style: Style,
/// Width constraints for each column
widths: &'a [Constraint],
/// Space between each column
column_spacing: u16,
/// Style used to render the selected row
highlight_style: Style,
/// Symbol in front of the selected rom
highlight_symbol: Option<&'a str>,
/// Optional header
header: Option<Row<'a>>,
/// Data to display in each row
rows: Vec<Row<'a>>,
/// Decides when to allocate spacing for the row selection
highlight_spacing: HighlightSpacing,
}
impl<'a> Table<'a> {
/// Creates a new [`Table`] widget with the given rows.
///
/// The `rows` parameter is a Vector of [`Row`], this holds the data to be displayed by the
/// table
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let table = Table::new(vec![
/// Row::new(vec![
/// Cell::from("Cell1"),
/// Cell::from("Cell2")
/// ]),
/// Row::new(vec![
/// Cell::from("Cell3"),
/// Cell::from("Cell4")
/// ]),
/// ]);
/// ```
pub fn new<T>(rows: T) -> Self
where
T: IntoIterator<Item = Row<'a>>,
{
Self {
block: None,
style: Style::default(),
widths: &[],
column_spacing: 1,
highlight_style: Style::default(),
highlight_symbol: None,
header: None,
rows: rows.into_iter().collect(),
highlight_spacing: HighlightSpacing::default(),
}
}
/// Creates a custom block around a [`Table`] widget.
///
/// The `block` parameter is of type [`Block`]. This holds the specified block to be
/// created around the [`Table`]
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let table = Table::new(vec![
/// Row::new(vec![
/// Cell::from("Cell1"),
/// Cell::from("Cell2")
/// ]),
/// Row::new(vec![
/// Cell::from("Cell3"),
/// Cell::from("Cell4")
/// ]),
/// ]).block(Block::default().title("Table"));
/// ```
pub fn block(mut self, block: Block<'a>) -> Self {
self.block = Some(block);
self
}
/// Creates a header for a [`Table`] widget.
///
/// The `header` parameter is of type [`Row`] and this holds the cells to be displayed at the
/// top of the [`Table`]
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let table = Table::new(vec![
/// Row::new(vec![
/// Cell::from("Cell1"),
/// Cell::from("Cell2")
/// ])
/// ]).header(
/// Row::new(vec![
/// Cell::from("Header Cell 1"),
/// Cell::from("Header Cell 2")
/// ])
/// );
/// ```
pub fn header(mut self, header: Row<'a>) -> Self {
self.header = Some(header);
self
}
pub fn widths(mut self, widths: &'a [Constraint]) -> Self {
let between_0_and_100 = |&w| match w {
Constraint::Percentage(p) => p <= 100,
_ => true,
};
assert!(
widths.iter().all(between_0_and_100),
"Percentages should be between 0 and 100 inclusively."
);
self.widths = widths;
self
}
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
pub fn highlight_symbol(mut self, highlight_symbol: &'a str) -> Self {
self.highlight_symbol = Some(highlight_symbol);
self
}
pub fn highlight_style(mut self, highlight_style: Style) -> Self {
self.highlight_style = highlight_style;
self
}
/// Set when to show the highlight spacing
///
/// See [`HighlightSpacing`] about which variant affects spacing in which way
pub fn highlight_spacing(mut self, value: HighlightSpacing) -> Self {
self.highlight_spacing = value;
self
}
pub fn column_spacing(mut self, spacing: u16) -> Self {
self.column_spacing = spacing;
self
}
/// Get all offsets and widths of all user specified columns
/// Returns (x, width)
fn get_columns_widths(&self, max_width: u16, selection_width: u16) -> Vec<(u16, u16)> {
let mut constraints = Vec::with_capacity(self.widths.len() * 2 + 1);
constraints.push(Constraint::Length(selection_width));
for constraint in self.widths {
constraints.push(*constraint);
constraints.push(Constraint::Length(self.column_spacing));
}
if !self.widths.is_empty() {
constraints.pop();
}
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints(constraints)
.segment_size(SegmentSize::None)
.split(Rect {
x: 0,
y: 0,
width: max_width,
height: 1,
});
chunks
.iter()
.skip(1)
.step_by(2)
.map(|c| (c.x, c.width))
.collect()
}
fn get_row_bounds(
&self,
selected: Option<usize>,
offset: usize,
max_height: u16,
) -> (usize, usize) {
let offset = offset.min(self.rows.len().saturating_sub(1));
let mut start = offset;
let mut end = offset;
let mut height = 0;
for item in self.rows.iter().skip(offset) {
if height + item.height > max_height {
break;
}
height += item.total_height();
end += 1;
}
let selected = selected.unwrap_or(0).min(self.rows.len() - 1);
while selected >= end {
height = height.saturating_add(self.rows[end].total_height());
end += 1;
while height > max_height {
height = height.saturating_sub(self.rows[start].total_height());
start += 1;
}
}
while selected < start {
start -= 1;
height = height.saturating_add(self.rows[start].total_height());
while height > max_height {
end -= 1;
height = height.saturating_sub(self.rows[end].total_height());
}
}
(start, end)
}
}
impl<'a> Styled for Table<'a> {
type Item = Table<'a>;
fn style(&self) -> Style {
self.style
}
fn set_style(self, style: Style) -> Self::Item {
self.style(style)
}
}
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct TableState {
offset: usize,
selected: Option<usize>,
}
impl TableState {
pub fn offset(&self) -> usize {
self.offset
}
pub fn offset_mut(&mut self) -> &mut usize {
&mut self.offset
}
pub fn with_selected(mut self, selected: Option<usize>) -> Self {
self.selected = selected;
self
}
pub fn with_offset(mut self, offset: usize) -> Self {
self.offset = offset;
self
}
pub fn selected(&self) -> Option<usize> {
self.selected
}
pub fn select(&mut self, index: Option<usize>) {
self.selected = index;
if index.is_none() {
self.offset = 0;
}
}
}
impl<'a> StatefulWidget for Table<'a> {
type State = TableState;
fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
if area.area() == 0 {
return;
}
buf.set_style(area, self.style);
let table_area = match self.block.take() {
Some(b) => {
let inner_area = b.inner(area);
b.render(area, buf);
inner_area
}
None => area,
};
let selection_width = if self.highlight_spacing.should_add(state.selected.is_some()) {
self.highlight_symbol.map_or(0, |s| s.width() as u16)
} else {
0
};
let columns_widths = self.get_columns_widths(table_area.width, selection_width);
let highlight_symbol = self.highlight_symbol.unwrap_or("");
let mut current_height = 0;
let mut rows_height = table_area.height;
// Draw header
if let Some(ref header) = self.header {
let max_header_height = table_area.height.min(header.total_height());
buf.set_style(
Rect {
x: table_area.left(),
y: table_area.top(),
width: table_area.width,
height: table_area.height.min(header.height),
},
header.style,
);
let inner_offset = table_area.left();
for ((x, width), cell) in columns_widths.iter().zip(header.cells.iter()) {
render_cell(
buf,
cell,
Rect {
x: inner_offset + x,
y: table_area.top(),
width: *width,
height: max_header_height,
},
);
}
current_height += max_header_height;
rows_height = rows_height.saturating_sub(max_header_height);
}
// Draw rows
if self.rows.is_empty() {
return;
}
let (start, end) = self.get_row_bounds(state.selected, state.offset, rows_height);
state.offset = start;
for (i, table_row) in self
.rows
.iter_mut()
.enumerate()
.skip(state.offset)
.take(end - start)
{
let (row, inner_offset) = (table_area.top() + current_height, table_area.left());
current_height += table_row.total_height();
let table_row_area = Rect {
x: inner_offset,
y: row,
width: table_area.width,
height: table_row.height,
};
buf.set_style(table_row_area, table_row.style);
let is_selected = state.selected.map_or(false, |s| s == i);
if selection_width > 0 && is_selected {
// this should in normal cases be safe, because "get_columns_widths" allocates
// "highlight_symbol.width()" space but "get_columns_widths"
// currently does not bind it to max table.width()
buf.set_stringn(
inner_offset,
row,
highlight_symbol,
table_area.width as usize,
table_row.style,
);
};
for ((x, width), cell) in columns_widths.iter().zip(table_row.cells.iter()) {
render_cell(
buf,
cell,
Rect {
x: inner_offset + x,
y: row,
width: *width,
height: table_row.height,
},
);
}
if is_selected {
buf.set_style(table_row_area, self.highlight_style);
}
}
}
}
fn render_cell(buf: &mut Buffer, cell: &Cell, area: Rect) {
buf.set_style(area, cell.style);
for (i, line) in cell.content.lines.iter().enumerate() {
if i as u16 >= area.height {
break;
}
let x_offset = match line.alignment {
Some(Alignment::Center) => (area.width / 2).saturating_sub(line.width() as u16 / 2),
Some(Alignment::Right) => area.width.saturating_sub(line.width() as u16),
_ => 0,
};
buf.set_line(area.x + x_offset, area.y + i as u16, line, area.width);
}
}
impl<'a> Widget for Table<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
let mut state = TableState::default();
StatefulWidget::render(self, area, buf, &mut state);
}
}
#[cfg(test)]
mod tests {
use std::vec;
use super::*;
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!(
Cell::from("").black().on_white().bold().not_dim().style,
Style::default()
.fg(Color::Black)
.bg(Color::White)
.add_modifier(Modifier::BOLD)
.remove_modifier(Modifier::DIM)
)
}
#[test]
fn row_can_be_stylized() {
assert_eq!(
Row::new(vec![Cell::from("")])
.black()
.on_white()
.bold()
.not_italic()
.style,
Style::default()
.fg(Color::Black)
.bg(Color::White)
.add_modifier(Modifier::BOLD)
.remove_modifier(Modifier::ITALIC)
)
}
#[test]
fn table_can_be_stylized() {
assert_eq!(
Table::new(vec![Row::new(vec![Cell::from("")])])
.black()
.on_white()
.bold()
.not_crossed_out()
.style,
Style::default()
.fg(Color::Black)
.bg(Color::White)
.add_modifier(Modifier::BOLD)
.remove_modifier(Modifier::CROSSED_OUT)
)
}
#[test]
fn highlight_spacing_to_string() {

215
src/widgets/table/cell.rs Normal file
View File

@@ -0,0 +1,215 @@
use crate::prelude::*;
/// A [`Cell`] contains the [`Text`] to be displayed in a [`Row`] of a [`Table`].
///
/// You can apply a [`Style`] to the [`Cell`] using [`Cell::style`]. This will set the style for the
/// entire area of the cell. Any [`Style`] set on the [`Text`] content will be combined with the
/// [`Style`] of the [`Cell`] by adding the [`Style`] of the [`Text`] content to the [`Style`] of
/// the [`Cell`]. Styles set on the text content will only affect the content.
///
/// # Examples
///
/// You can create a `Cell` from anything that can be converted to a [`Text`].
///
/// ```rust
/// use std::borrow::Cow;
///
/// use ratatui::{prelude::*, widgets::*};
///
/// Cell::from("simple string");
/// Cell::from(Span::from("span"));
/// Cell::from(Line::from(vec![
/// Span::raw("a vec of "),
/// Span::styled("spans", Style::default().add_modifier(Modifier::BOLD)),
/// ]));
/// Cell::from(Text::from("a text"));
/// Cell::from(Text::from(Cow::Borrowed("hello")));
/// ```
///
/// `Cell` implements [`Styled`] which means you can use style shorthands from the [`Stylize`] trait
/// to set the style of the cell concisely.
///
/// ```rust
/// use ratatui::{prelude::*, widgets::*};
/// Cell::new("Cell 1").red().italic();
/// ```
///
/// [`Row`]: super::Row
/// [`Table`]: super::Table
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Cell<'a> {
content: Text<'a>,
style: Style,
}
impl<'a> Cell<'a> {
/// Creates a new [`Cell`]
///
/// The `content` parameter accepts any value that can be converted into a [`Text`].
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// Cell::new("simple string");
/// Cell::new(Span::from("span"));
/// Cell::new(Line::from(vec![
/// Span::raw("a vec of "),
/// Span::styled("spans", Style::default().add_modifier(Modifier::BOLD)),
/// ]));
/// Cell::new(Text::from("a text"));
/// ```
pub fn new<T>(content: T) -> Self
where
T: Into<Text<'a>>,
{
Self {
content: content.into(),
style: Style::default(),
}
}
/// Set the content of the [`Cell`]
///
/// The `content` parameter accepts any value that can be converted into a [`Text`].
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// Cell::default().content("simple string");
/// Cell::default().content(Span::from("span"));
/// Cell::default().content(Line::from(vec![
/// Span::raw("a vec of "),
/// Span::styled("spans", Style::new().bold()),
/// ]));
/// Cell::default().content(Text::from("a text"));
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn content<T>(mut self, content: T) -> Self
where
T: Into<Text<'a>>,
{
self.content = content.into();
self
}
/// Set the `Style` of this cell
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// This `Style` will override the `Style` of the [`Row`] and can be overridden by the `Style`
/// of the [`Text`] content.
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// Cell::new("Cell 1").style(Style::new().red().italic());
/// ```
///
/// `Cell` also implements the [`Styled`] trait, which means you can use style shorthands from
/// the [`Stylize`] trait to set the style of the widget more concisely.
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// Cell::new("Cell 1").red().italic();
/// ```
///
/// [`Row`]: super::Row
#[must_use = "method moves the value of self and returns the modified value"]
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = style.into();
self
}
}
impl Cell<'_> {
pub(crate) fn render(&self, area: Rect, buf: &mut Buffer) {
buf.set_style(area, self.style);
for (i, line) in self.content.lines.iter().enumerate() {
if i as u16 >= area.height {
break;
}
let x_offset = match line.alignment {
Some(Alignment::Center) => (area.width / 2).saturating_sub(line.width() as u16 / 2),
Some(Alignment::Right) => area.width.saturating_sub(line.width() as u16),
_ => 0,
};
let x = area.x + x_offset;
if x >= area.right() {
continue;
}
buf.set_line(x, area.y + i as u16, line, area.width);
}
}
}
impl<'a, T> From<T> for Cell<'a>
where
T: Into<Text<'a>>,
{
fn from(content: T) -> Cell<'a> {
Cell {
content: content.into(),
style: Style::default(),
}
}
}
impl<'a> Styled for Cell<'a> {
type Item = Cell<'a>;
fn style(&self) -> Style {
self.style
}
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
self.style(style)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::style::{Color, Modifier, Style, Stylize};
#[test]
fn new() {
let cell = Cell::new("");
assert_eq!(cell.content, Text::from(""));
}
#[test]
fn content() {
let cell = Cell::default().content("");
assert_eq!(cell.content, Text::from(""));
}
#[test]
fn style() {
let style = Style::default().red().italic();
let cell = Cell::default().style(style);
assert_eq!(cell.style, style);
}
#[test]
fn stylize() {
assert_eq!(
Cell::from("").black().on_white().bold().not_dim().style,
Style::default()
.fg(Color::Black)
.bg(Color::White)
.add_modifier(Modifier::BOLD)
.remove_modifier(Modifier::DIM)
)
}
}

300
src/widgets/table/row.rs Normal file
View File

@@ -0,0 +1,300 @@
use super::*;
use crate::prelude::*;
/// A single row of data to be displayed in a [`Table`] widget.
///
/// A `Row` is a collection of [`Cell`]s.
///
/// By default, a row has a height of 1 but you can change this using [`Row::height`].
///
/// You can set the style of the entire row using [`Row::style`]. This [`Style`] will be combined
/// with the [`Style`] of each individual [`Cell`] by adding the [`Style`] of the [`Cell`] to the
/// [`Style`] of the [`Row`].
///
/// # Examples
///
/// You can create `Row`s from simple strings.
///
/// ```rust
/// use ratatui::{prelude::*, widgets::*};
///
/// Row::new(vec!["Cell1", "Cell2", "Cell3"]);
/// ```
///
/// If you need a bit more control over individual cells, you can explicitly create [`Cell`]s:
///
/// ```rust
/// use ratatui::{prelude::*, widgets::*};
///
/// Row::new(vec![
/// Cell::from("Cell1"),
/// Cell::from("Cell2").style(Style::default().fg(Color::Yellow)),
/// ]);
/// ```
///
/// You can also construct a row from any type that can be converted into [`Text`]:
///
/// ```rust
/// use std::borrow::Cow;
///
/// use ratatui::{prelude::*, widgets::*};
///
/// Row::new(vec![
/// Cow::Borrowed("hello"),
/// Cow::Owned("world".to_uppercase()),
/// ]);
/// ```
///
/// `Row` implements [`Styled`] which means you can use style shorthands from the [`Stylize`] trait
/// to set the style of the row concisely.
///
/// ```rust
/// use ratatui::{prelude::*, widgets::*};
/// let cells = vec!["Cell1", "Cell2", "Cell3"];
/// Row::new(cells).red().italic();
/// ```
///
/// [`Table`]: super::Table
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Row<'a> {
pub(crate) cells: Vec<Cell<'a>>,
pub(crate) height: u16,
pub(crate) top_margin: u16,
pub(crate) bottom_margin: u16,
pub(crate) style: Style,
}
impl<'a> Row<'a> {
/// Creates a new [`Row`]
///
/// The `cells` parameter accepts any value that can be converted into an iterator of anything
/// that can be converted into a [`Cell`] (e.g. `Vec<&str>`, `&[Cell<'a>]`, `Vec<String>`, etc.)
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let row = Row::new(vec!["Cell 1", "Cell 2", "Cell 3"]);
/// let row = Row::new(vec![
/// Cell::new("Cell 1"),
/// Cell::new("Cell 2"),
/// Cell::new("Cell 3"),
/// ]);
/// ```
pub fn new<T>(cells: T) -> Self
where
T: IntoIterator,
T::Item: Into<Cell<'a>>,
{
Self {
cells: cells.into_iter().map(Into::into).collect(),
height: 1,
..Default::default()
}
}
/// Set the cells of the [`Row`]
///
/// The `cells` parameter accepts any value that can be converted into an iterator of anything
/// that can be converted into a [`Cell`] (e.g. `Vec<&str>`, `&[Cell<'a>]`, `Vec<String>`, etc.)
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let row = Row::default().cells(vec!["Cell 1", "Cell 2", "Cell 3"]);
/// let row = Row::default().cells(vec![
/// Cell::new("Cell 1"),
/// Cell::new("Cell 2"),
/// Cell::new("Cell 3"),
/// ]);
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn cells<T>(mut self, cells: T) -> Self
where
T: IntoIterator,
T::Item: Into<Cell<'a>>,
{
self.cells = cells.into_iter().map(Into::into).collect();
self
}
/// Set the fixed height of the [`Row`]
///
/// Any [`Cell`] whose content has more lines than this height will see its content truncated.
///
/// By default, the height is `1`.
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let cells = vec!["Cell 1\nline 2", "Cell 2", "Cell 3"];
/// let row = Row::new(cells).height(2);
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn height(mut self, height: u16) -> Self {
self.height = height;
self
}
/// Set the top margin. By default, the top margin is `0`.
///
/// The top margin is the number of blank lines to be displayed before the row.
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # let cells = vec!["Cell 1", "Cell 2", "Cell 3"];
/// let row = Row::default().top_margin(1);
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn top_margin(mut self, margin: u16) -> Self {
self.top_margin = margin;
self
}
/// Set the bottom margin. By default, the bottom margin is `0`.
///
/// The bottom margin is the number of blank lines to be displayed after the row.
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # let cells = vec!["Cell 1", "Cell 2", "Cell 3"];
/// let row = Row::default().bottom_margin(1);
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn bottom_margin(mut self, margin: u16) -> Self {
self.bottom_margin = margin;
self
}
/// Set the [`Style`] of the entire row
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// This [`Style`] can be overridden by the [`Style`] of a any individual [`Cell`] or by their
/// [`Text`] content.
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let cells = vec!["Cell 1", "Cell 2", "Cell 3"];
/// let row = Row::new(cells).style(Style::new().red().italic());
/// ```
///
/// `Row` also implements the [`Styled`] trait, which means you can use style shorthands from
/// the [`Stylize`] trait to set the style of the widget more concisely.
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let cells = vec!["Cell 1", "Cell 2", "Cell 3"];
/// let row = Row::new(cells).red().italic();
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = style.into();
self
}
}
// private methods for rendering
impl Row<'_> {
/// Returns the total height of the row.
pub(crate) fn height_with_margin(&self) -> u16 {
self.height
.saturating_add(self.top_margin)
.saturating_add(self.bottom_margin)
}
}
impl<'a> Styled for Row<'a> {
type Item = Row<'a>;
fn style(&self) -> Style {
self.style
}
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
self.style(style)
}
}
#[cfg(test)]
mod tests {
use std::vec;
use super::*;
use crate::style::{Color, Modifier, Style, Stylize};
#[test]
fn new() {
let cells = vec![Cell::from("")];
let row = Row::new(cells.clone());
assert_eq!(row.cells, cells);
}
#[test]
fn cells() {
let cells = vec![Cell::from("")];
let row = Row::default().cells(cells.clone());
assert_eq!(row.cells, cells);
}
#[test]
fn height() {
let row = Row::default().height(2);
assert_eq!(row.height, 2);
}
#[test]
fn top_margin() {
let row = Row::default().top_margin(1);
assert_eq!(row.top_margin, 1);
}
#[test]
fn bottom_margin() {
let row = Row::default().bottom_margin(1);
assert_eq!(row.bottom_margin, 1);
}
#[test]
fn style() {
let style = Style::default().red().italic();
let row = Row::default().style(style);
assert_eq!(row.style, style);
}
#[test]
fn stylize() {
assert_eq!(
Row::new(vec![Cell::from("")])
.black()
.on_white()
.bold()
.not_italic()
.style,
Style::default()
.fg(Color::Black)
.bg(Color::White)
.add_modifier(Modifier::BOLD)
.remove_modifier(Modifier::ITALIC)
)
}
}

1423
src/widgets/table/table.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,238 @@
/// State of a [`Table`] widget
///
/// This state can be used to scroll through the rows and select one of them. When the table is
/// rendered as a stateful widget, the selected row will be highlighted and the table will be
/// shifted to ensure that the selected row is visible. This will modify the [`TableState`] object
/// passed to the [`Frame::render_stateful_widget`] method.
///
/// The state consists of two fields:
/// - [`offset`]: the index of the first row to be displayed
/// - [`selected`]: the index of the selected row, which can be `None` if no row is selected
///
/// [`offset`]: TableState::offset()
/// [`selected`]: TableState::selected()
///
/// See the [table example] and the recipe and traceroute tabs in the [demo2 example] for a more in
/// depth example of the various configuration options and for how to handle state.
///
/// [table example]: https://github.com/ratatui-org/ratatui/blob/master/examples/table.rs
/// [demo2 example]: https://github.com/ratatui-org/ratatui/blob/master/examples/demo2/
///
/// # Example
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # fn ui(frame: &mut Frame) {
/// # let area = Rect::default();
/// # let rows = [Row::new(vec!["Cell1", "Cell2"])];
/// # let widths = [Constraint::Length(5), Constraint::Length(5)];
/// let table = Table::new(rows, widths).widths(widths);
///
/// // Note: TableState should be stored in your application state (not constructed in your render
/// // method) so that the selected row is preserved across renders
/// let mut table_state = TableState::default();
/// *table_state.offset_mut() = 1; // display the second row and onwards
/// table_state.select(Some(3)); // select the forth row (0-indexed)
///
/// frame.render_stateful_widget(table, area, &mut table_state);
/// # }
/// ```
///
/// Note that if [`Table::widths`] is not called before rendering, the rendered columns will have
/// equal width.
///
/// [`Table`]: crate::widgets::Table
/// [`Table::widths`]: crate::widgets::Table::widths
/// [`Frame::render_stateful_widget`]: crate::Frame::render_stateful_widget
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct TableState {
pub(crate) offset: usize,
pub(crate) selected: Option<usize>,
}
impl TableState {
/// Creates a new [`TableState`]
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let state = TableState::new();
/// ```
pub fn new() -> Self {
Self::default()
}
/// Sets the index of the first row to be displayed
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let state = TableState::new().with_offset(1);
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn with_offset(mut self, offset: usize) -> Self {
self.offset = offset;
self
}
/// Sets the index of the selected row
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let state = TableState::new().with_selected(Some(1));
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn with_selected<T>(mut self, selected: T) -> Self
where
T: Into<Option<usize>>,
{
self.selected = selected.into();
self
}
/// Index of the first row to be displayed
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let state = TableState::new();
/// assert_eq!(state.offset(), 0);
/// ```
pub fn offset(&self) -> usize {
self.offset
}
/// Mutable reference to the index of the first row to be displayed
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let mut state = TableState::default();
/// *state.offset_mut() = 1;
/// ```
pub fn offset_mut(&mut self) -> &mut usize {
&mut self.offset
}
/// Index of the selected row
///
/// Returns `None` if no row is selected
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let state = TableState::new();
/// assert_eq!(state.selected(), None);
/// ```
pub fn selected(&self) -> Option<usize> {
self.selected
}
/// Mutable reference to the index of the selected row
///
/// Returns `None` if no row is selected
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let mut state = TableState::default();
/// *state.selected_mut() = Some(1);
/// ```
pub fn selected_mut(&mut self) -> &mut Option<usize> {
&mut self.selected
}
/// Sets the index of the selected row
///
/// Set to `None` if no row is selected. This will also reset the offset to `0`.
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let mut state = TableState::default();
/// state.select(Some(1));
/// ```
pub fn select(&mut self, index: Option<usize>) {
self.selected = index;
if index.is_none() {
self.offset = 0;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new() {
let state = TableState::new();
assert_eq!(state.offset, 0);
assert_eq!(state.selected, None);
}
#[test]
fn with_offset() {
let state = TableState::new().with_offset(1);
assert_eq!(state.offset, 1);
}
#[test]
fn with_selected() {
let state = TableState::new().with_selected(Some(1));
assert_eq!(state.selected, Some(1));
}
#[test]
fn offset() {
let state = TableState::new();
assert_eq!(state.offset(), 0);
}
#[test]
fn offset_mut() {
let mut state = TableState::new();
*state.offset_mut() = 1;
assert_eq!(state.offset, 1);
}
#[test]
fn selected() {
let state = TableState::new();
assert_eq!(state.selected(), None);
}
#[test]
fn selected_mut() {
let mut state = TableState::new();
*state.selected_mut() = Some(1);
assert_eq!(state.selected, Some(1));
}
#[test]
fn select() {
let mut state = TableState::new();
state.select(Some(1));
assert_eq!(state.selected, Some(1));
}
#[test]
fn select_none() {
let mut state = TableState::new().with_selected(Some(1));
state.select(None);
assert_eq!(state.selected, None);
}
}

View File

@@ -1,18 +1,19 @@
#![deny(missing_docs)]
use crate::{
buffer::Buffer,
layout::Rect,
style::{Style, Styled},
symbols,
text::{Line, Span},
prelude::*,
widgets::{Block, Widget},
};
const DEFAULT_HIGHLIGHT_STYLE: Style = Style::new().add_modifier(Modifier::REVERSED);
/// A widget that displays a horizontal set of Tabs with a single tab selected.
///
/// Each tab title is stored as a [`Line`] which can be individually styled. The selected tab is set
/// using [`Tabs::select`] and styled using [`Tabs::highlight_style`]. The divider can be customized
/// with [`Tabs::divider`].
/// with [`Tabs::divider`]. Padding can be set with [`Tabs::padding`] or [`Tabs::padding_left`] and
/// [`Tabs::padding_right`].
///
/// The divider defaults to |, and padding defaults to a singular space on each side.
///
/// # Example
///
@@ -24,7 +25,8 @@ use crate::{
/// .style(Style::default().white())
/// .highlight_style(Style::default().yellow())
/// .select(2)
/// .divider(symbols::DOT);
/// .divider(symbols::DOT)
/// .padding("->", "<-");
/// ```
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Tabs<'a> {
@@ -40,6 +42,10 @@ pub struct Tabs<'a> {
highlight_style: Style,
/// Tab divider
divider: Span<'a>,
/// Tab Left Padding
padding_left: Line<'a>,
/// Tab Right Padding
padding_right: Line<'a>,
}
impl<'a> Tabs<'a> {
@@ -48,6 +54,18 @@ impl<'a> Tabs<'a> {
/// `titles` can be a [`Vec`] of [`&str`], [`String`] or anything that can be converted into
/// [`Line`]. As such, titles can be styled independently.
///
/// The selected tab can be set with [`Tabs::select`]. The first tab has index 0 (this is also
/// the default index).
///
/// The selected tab can have a different style with [`Tabs::highlight_style`]. This defaults to
/// a style with the [`Modifier::REVERSED`] modifier added.
///
/// The default divider is a pipe (`|`), but it can be customized with [`Tabs::divider`].
///
/// The entire widget can be styled with [`Tabs::style`].
///
/// The widget can be wrapped in a [`Block`] using [`Tabs::block`].
///
/// # Examples
///
/// Basic titles.
@@ -70,12 +88,15 @@ impl<'a> Tabs<'a> {
titles: titles.into_iter().map(Into::into).collect(),
selected: 0,
style: Style::default(),
highlight_style: Style::default(),
highlight_style: DEFAULT_HIGHLIGHT_STYLE,
divider: Span::raw(symbols::line::VERTICAL),
padding_left: Line::from(" "),
padding_right: Line::from(" "),
}
}
/// Surrounds the `Tabs` with a [`Block`].
#[must_use = "method moves the value of self and returns the modified value"]
pub fn block(mut self, block: Block<'a>) -> Tabs<'a> {
self.block = Some(block);
self
@@ -83,8 +104,9 @@ impl<'a> Tabs<'a> {
/// Sets the selected tab.
///
/// The first tab has index 0 (this is also the default index).
/// The first tab has index 0 (this is also the default index).
/// The selected tab can have a different style with [`Tabs::highlight_style`].
#[must_use = "method moves the value of self and returns the modified value"]
pub fn select(mut self, selected: usize) -> Tabs<'a> {
self.selected = selected;
self
@@ -92,19 +114,27 @@ impl<'a> Tabs<'a> {
/// Sets the style of the tabs.
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// This will set the given style on the entire render area.
/// More precise style can be applied to the titles by styling the ones given to [`Tabs::new`].
/// The selected tab can be styled differently using [`Tabs::highlight_style`].
pub fn style(mut self, style: Style) -> Tabs<'a> {
self.style = style;
#[must_use = "method moves the value of self and returns the modified value"]
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = style.into();
self
}
/// Sets the style for the highlighted tab.
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// Highlighted tab can be selected with [`Tabs::select`].
pub fn highlight_style(mut self, style: Style) -> Tabs<'a> {
self.highlight_style = style;
#[must_use = "method moves the value of self and returns the modified value"]
pub fn highlight_style<S: Into<Style>>(mut self, style: S) -> Tabs<'a> {
self.highlight_style = style.into();
self
}
@@ -121,7 +151,7 @@ impl<'a> Tabs<'a> {
/// ```
/// Use dash (`-`) as separator.
/// ```
/// # use ratatui::{prelude::*, widgets::Tabs, symbols};
/// # use ratatui::{prelude::*, widgets::Tabs};
/// let tabs = Tabs::new(vec!["Tab 1", "Tab 2"]).divider("-");
/// ```
pub fn divider<T>(mut self, divider: T) -> Tabs<'a>
@@ -131,6 +161,70 @@ impl<'a> Tabs<'a> {
self.divider = divider.into();
self
}
/// Sets the padding between tabs.
///
/// Both default to space.
///
/// # Examples
///
/// A space on either side of the tabs.
/// ```
/// # use ratatui::{prelude::*, widgets::Tabs};
/// let tabs = Tabs::new(vec!["Tab 1", "Tab 2"]).padding(" ", " ");
/// ```
/// Nothing on either side of the tabs.
/// ```
/// # use ratatui::{prelude::*, widgets::Tabs};
/// let tabs = Tabs::new(vec!["Tab 1", "Tab 2"]).padding("", "");
/// ```
pub fn padding<T, U>(mut self, left: T, right: U) -> Tabs<'a>
where
T: Into<Line<'a>>,
U: Into<Line<'a>>,
{
self.padding_left = left.into();
self.padding_right = right.into();
self
}
/// Sets the left side padding between tabs.
///
/// Defaults to a space.
///
/// # Example
///
/// An arrow on the left of tabs.
/// ```
/// # use ratatui::{prelude::*, widgets::Tabs};
/// let tabs = Tabs::new(vec!["Tab 1", "Tab 2"]).padding_left("->");
/// ```
pub fn padding_left<T>(mut self, padding: T) -> Tabs<'a>
where
T: Into<Line<'a>>,
{
self.padding_left = padding.into();
self
}
/// Sets the right side padding between tabs.
///
/// Defaults to a space.
///
/// # Example
///
/// An arrow on the right of tabs.
/// ```
/// # use ratatui::{prelude::*, widgets::Tabs};
/// let tabs = Tabs::new(vec!["Tab 1", "Tab 2"]).padding_right("<-");
/// ```
pub fn padding_right<T>(mut self, padding: T) -> Tabs<'a>
where
T: Into<Line<'a>>,
{
self.padding_left = padding.into();
self
}
}
impl<'a> Styled for Tabs<'a> {
@@ -140,7 +234,7 @@ impl<'a> Styled for Tabs<'a> {
self.style
}
fn set_style(self, style: Style) -> Self::Item {
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
self.style(style)
}
}
@@ -165,11 +259,21 @@ impl<'a> Widget for Tabs<'a> {
let titles_length = self.titles.len();
for (i, title) in self.titles.into_iter().enumerate() {
let last_title = titles_length - 1 == i;
x = x.saturating_add(1);
let remaining_width = tabs_area.right().saturating_sub(x);
if remaining_width == 0 {
break;
}
// Left Padding
let pos = buf.set_line(x, tabs_area.top(), &self.padding_left, remaining_width);
x = pos.0;
let remaining_width = tabs_area.right().saturating_sub(x);
if remaining_width == 0 {
break;
}
// Title
let pos = buf.set_line(x, tabs_area.top(), &title, remaining_width);
if i == self.selected {
buf.set_style(
@@ -182,11 +286,20 @@ impl<'a> Widget for Tabs<'a> {
self.highlight_style,
);
}
x = pos.0.saturating_add(1);
x = pos.0;
let remaining_width = tabs_area.right().saturating_sub(x);
if remaining_width == 0 {
break;
}
// Right Padding
let pos = buf.set_line(x, tabs_area.top(), &self.padding_right, remaining_width);
x = pos.0;
let remaining_width = tabs_area.right().saturating_sub(x);
if remaining_width == 0 || last_title {
break;
}
let pos = buf.set_span(x, tabs_area.top(), &self.divider, remaining_width);
x = pos.0;
}
@@ -196,7 +309,7 @@ impl<'a> Widget for Tabs<'a> {
#[cfg(test)]
mod tests {
use super::*;
use crate::{assert_buffer_eq, prelude::*, widgets::Borders};
use crate::{assert_buffer_eq, widgets::Borders};
#[test]
fn new() {
@@ -214,8 +327,10 @@ mod tests {
],
selected: 0,
style: Style::default(),
highlight_style: Style::default(),
highlight_style: DEFAULT_HIGHLIGHT_STYLE,
divider: Span::raw(symbols::line::VERTICAL),
padding_right: Line::from(" "),
padding_left: Line::from(" "),
}
);
}
@@ -229,40 +344,56 @@ mod tests {
#[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 "])
);
let mut expected = Buffer::with_lines(vec![" Tab1 │ Tab2 │ Tab3 │ Tab4 "]);
// first tab selected
expected.set_style(Rect::new(1, 0, 4, 1), DEFAULT_HIGHLIGHT_STYLE);
assert_buffer_eq!(render(tabs, Rect::new(0, 0, 30, 1)), expected);
}
#[test]
fn render_no_padding() {
let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"]).padding("", "");
let mut expected = Buffer::with_lines(vec!["Tab1│Tab2│Tab3│Tab4 "]);
// first tab selected
expected.set_style(Rect::new(0, 0, 4, 1), DEFAULT_HIGHLIGHT_STYLE);
assert_buffer_eq!(render(tabs, Rect::new(0, 0, 30, 1)), expected);
}
#[test]
fn render_more_padding() {
let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"]).padding("---", "++");
let mut expected = Buffer::with_lines(vec!["---Tab1++│---Tab2++│---Tab3++│"]);
// first tab selected
expected.set_style(Rect::new(3, 0, 4, 1), DEFAULT_HIGHLIGHT_STYLE);
assert_buffer_eq!(render(tabs, Rect::new(0, 0, 30, 1)), expected);
}
#[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 │",
"└────────────────────────────┘",
])
);
let mut expected = Buffer::with_lines(vec![
"┌Tabs────────────────────────┐",
"│ Tab1 │ Tab2 │ Tab3 │ Tab4 │",
"────────────────────────────┘",
]);
// first tab selected
expected.set_style(Rect::new(2, 1, 4, 1), DEFAULT_HIGHLIGHT_STYLE);
assert_buffer_eq!(render(tabs, Rect::new(0, 0, 30, 3)), expected);
}
#[test]
fn render_style() {
let tabs =
Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"]).style(Style::default().fg(Color::Red));
assert_buffer_eq!(
render(tabs, Rect::new(0, 0, 30, 1)),
Buffer::with_lines(vec![" Tab1 │ Tab2 │ Tab3 │ Tab4 ".red()])
);
let mut expected = Buffer::with_lines(vec![" Tab1 │ Tab2 │ Tab3 │ Tab4 ".red()]);
expected.set_style(Rect::new(1, 0, 4, 1), DEFAULT_HIGHLIGHT_STYLE.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());
let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"]);
// first tab selected
assert_buffer_eq!(
@@ -305,13 +436,13 @@ mod tests {
fn render_style_and_selected() {
let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"])
.style(Style::new().red())
.highlight_style(Style::new().reversed())
.highlight_style(Style::new().underlined())
.select(0);
assert_buffer_eq!(
render(tabs, Rect::new(0, 0, 30, 1)),
Buffer::with_lines(vec![Line::from(vec![
" ".red(),
"Tab1".red().reversed(),
"Tab1".red().underlined(),
" │ Tab2 │ Tab3 │ Tab4 ".red(),
])])
);
@@ -320,10 +451,10 @@ mod tests {
#[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 ",])
);
let mut expected = Buffer::with_lines(vec![" Tab1 -- Tab2 -- Tab3 -- Tab4 "]);
// first tab selected
expected.set_style(Rect::new(1, 0, 4, 1), DEFAULT_HIGHLIGHT_STYLE);
assert_buffer_eq!(render(tabs, Rect::new(0, 0, 30, 1)), expected);
}
#[test]

View File

@@ -1,10 +1,12 @@
use std::error::Error;
use ratatui::{
assert_buffer_eq,
backend::{Backend, TestBackend},
layout::Rect,
widgets::Paragraph,
Terminal,
prelude::Buffer,
widgets::{Paragraph, Widget},
Terminal, TerminalOptions, Viewport,
};
#[test]
@@ -23,9 +25,9 @@ fn swap_buffer_clears_prev_buffer() {
terminal
.current_buffer_mut()
.set_string(0, 0, "Hello", ratatui::style::Style::reset());
assert_eq!(terminal.current_buffer_mut().content()[0].symbol, "H");
assert_eq!(terminal.current_buffer_mut().content()[0].symbol(), "H");
terminal.swap_buffers();
assert_eq!(terminal.current_buffer_mut().content()[0].symbol, " ");
assert_eq!(terminal.current_buffer_mut().content()[0].symbol(), " ");
}
#[test]
@@ -36,14 +38,158 @@ fn terminal_draw_returns_the_completed_frame() -> Result<(), Box<dyn Error>> {
let paragraph = Paragraph::new("Test");
f.render_widget(paragraph, f.size());
})?;
assert_eq!(frame.buffer.get(0, 0).symbol, "T");
assert_eq!(frame.buffer.get(0, 0).symbol(), "T");
assert_eq!(frame.area, Rect::new(0, 0, 10, 10));
terminal.backend_mut().resize(8, 8);
let frame = terminal.draw(|f| {
let paragraph = Paragraph::new("test");
f.render_widget(paragraph, f.size());
})?;
assert_eq!(frame.buffer.get(0, 0).symbol, "t");
assert_eq!(frame.buffer.get(0, 0).symbol(), "t");
assert_eq!(frame.area, Rect::new(0, 0, 8, 8));
Ok(())
}
#[test]
fn terminal_insert_before_moves_viewport() -> Result<(), Box<dyn Error>> {
// When we have a terminal with 5 lines, and a single line viewport, if we insert a
// number of lines less than the `terminal height - viewport height` it should move
// viewport down to accommodate the new lines.
let backend = TestBackend::new(20, 5);
let mut terminal = Terminal::with_options(
backend,
TerminalOptions {
viewport: Viewport::Inline(1),
},
)?;
// insert_before cannot guarantee the contents of the viewport remain unharmed
// by potential scrolling as such it is necessary to call draw afterwards to
// redraw the contents of the viewport over the newly designated area.
terminal.insert_before(2, |buf| {
Paragraph::new(vec![
"------ Line 1 ------".into(),
"------ Line 2 ------".into(),
])
.render(buf.area, buf);
})?;
terminal.draw(|f| {
let paragraph = Paragraph::new("[---- Viewport ----]");
f.render_widget(paragraph, f.size());
})?;
assert_buffer_eq!(
terminal.backend().buffer().clone(),
Buffer::with_lines(vec![
"------ Line 1 ------",
"------ Line 2 ------",
"[---- Viewport ----]",
" ",
" ",
])
);
Ok(())
}
#[test]
fn terminal_insert_before_scrolls_on_large_input() -> Result<(), Box<dyn Error>> {
// When we have a terminal with 5 lines, and a single line viewport, if we insert many
// lines before the viewport (greater than `terminal height - viewport height`) it should
// move the viewport down to the bottom of the terminal and scroll all lines above the viewport
// until all have been added to the buffer.
let backend = TestBackend::new(20, 5);
let mut terminal = Terminal::with_options(
backend,
TerminalOptions {
viewport: Viewport::Inline(1),
},
)?;
terminal.insert_before(5, |buf| {
Paragraph::new(vec![
"------ Line 1 ------".into(),
"------ Line 2 ------".into(),
"------ Line 3 ------".into(),
"------ Line 4 ------".into(),
"------ Line 5 ------".into(),
])
.render(buf.area, buf);
})?;
terminal.draw(|f| {
let paragraph = Paragraph::new("[---- Viewport ----]");
f.render_widget(paragraph, f.size());
})?;
assert_buffer_eq!(
terminal.backend().buffer().clone(),
Buffer::with_lines(vec![
"------ Line 2 ------",
"------ Line 3 ------",
"------ Line 4 ------",
"------ Line 5 ------",
"[---- Viewport ----]",
])
);
Ok(())
}
#[test]
fn terminal_insert_before_scrolls_on_many_inserts() -> Result<(), Box<dyn Error>> {
// This test ensures similar behaviour to `terminal_insert_before_scrolls_on_large_input`
// but covers a bug previously present whereby multiple small insertions
// (less than `terminal height - viewport height`) would have disparate behaviour to one large
// insertion. This was caused by an undocumented cap on the height to be inserted, which has now
// been removed.
let backend = TestBackend::new(20, 5);
let mut terminal = Terminal::with_options(
backend,
TerminalOptions {
viewport: Viewport::Inline(1),
},
)?;
terminal.insert_before(1, |buf| {
Paragraph::new(vec!["------ Line 1 ------".into()]).render(buf.area, buf);
})?;
terminal.insert_before(1, |buf| {
Paragraph::new(vec!["------ Line 2 ------".into()]).render(buf.area, buf);
})?;
terminal.insert_before(1, |buf| {
Paragraph::new(vec!["------ Line 3 ------".into()]).render(buf.area, buf);
})?;
terminal.insert_before(1, |buf| {
Paragraph::new(vec!["------ Line 4 ------".into()]).render(buf.area, buf);
})?;
terminal.insert_before(1, |buf| {
Paragraph::new(vec!["------ Line 5 ------".into()]).render(buf.area, buf);
})?;
terminal.draw(|f| {
let paragraph = Paragraph::new("[---- Viewport ----]");
f.render_widget(paragraph, f.size());
})?;
assert_buffer_eq!(
terminal.backend().buffer().clone(),
Buffer::with_lines(vec![
"------ Line 2 ------",
"------ Line 3 ------",
"------ Line 4 ------",
"------ Line 5 ------",
"[---- Viewport ----]",
])
);
Ok(())
}

View File

@@ -20,7 +20,7 @@ fn list_should_shows_the_length() {
assert_eq!(list.len(), 3);
assert!(!list.is_empty());
let empty_list = List::new(vec![]);
let empty_list = List::default();
assert_eq!(empty_list.len(), 0);
assert!(empty_list.is_empty());
}

View File

@@ -19,19 +19,21 @@ fn widgets_table_column_spacing_can_be_changed() {
terminal
.draw(|f| {
let size = f.size();
let table = Table::new(vec![
Row::new(vec!["Row11", "Row12", "Row13"]),
Row::new(vec!["Row21", "Row22", "Row23"]),
Row::new(vec!["Row31", "Row32", "Row33"]),
Row::new(vec!["Row41", "Row42", "Row43"]),
])
let table = Table::new(
vec![
Row::new(vec!["Row11", "Row12", "Row13"]),
Row::new(vec!["Row21", "Row22", "Row23"]),
Row::new(vec!["Row31", "Row32", "Row33"]),
Row::new(vec!["Row41", "Row42", "Row43"]),
],
[
Constraint::Length(5),
Constraint::Length(5),
Constraint::Length(5),
],
)
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
.block(Block::default().borders(Borders::ALL))
.widths(&[
Constraint::Length(5),
Constraint::Length(5),
Constraint::Length(5),
])
.column_spacing(column_spacing);
f.render_widget(table, size);
})
@@ -117,15 +119,17 @@ fn widgets_table_columns_widths_can_use_fixed_length_constraints() {
terminal
.draw(|f| {
let size = f.size();
let table = Table::new(vec![
Row::new(vec!["Row11", "Row12", "Row13"]),
Row::new(vec!["Row21", "Row22", "Row23"]),
Row::new(vec!["Row31", "Row32", "Row33"]),
Row::new(vec!["Row41", "Row42", "Row43"]),
])
let table = Table::new(
vec![
Row::new(vec!["Row11", "Row12", "Row13"]),
Row::new(vec!["Row21", "Row22", "Row23"]),
Row::new(vec!["Row31", "Row32", "Row33"]),
Row::new(vec!["Row41", "Row42", "Row43"]),
],
widths,
)
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
.block(Block::default().borders(Borders::ALL))
.widths(widths);
.block(Block::default().borders(Borders::ALL));
f.render_widget(table, size);
})
.unwrap();
@@ -198,28 +202,31 @@ fn widgets_table_columns_widths_can_use_fixed_length_constraints() {
#[test]
fn widgets_table_columns_widths_can_use_percentage_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();
terminal
.draw(|f| {
let size = f.size();
let table = Table::new(vec![
Row::new(vec!["Row11", "Row12", "Row13"]),
Row::new(vec!["Row21", "Row22", "Row23"]),
Row::new(vec!["Row31", "Row32", "Row33"]),
Row::new(vec!["Row41", "Row42", "Row43"]),
])
let table = Table::new(
vec![
Row::new(vec!["Row11", "Row12", "Row13"]),
Row::new(vec!["Row21", "Row22", "Row23"]),
Row::new(vec!["Row31", "Row32", "Row33"]),
Row::new(vec!["Row41", "Row42", "Row43"]),
],
widths,
)
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
.block(Block::default().borders(Borders::ALL))
.widths(widths)
.column_spacing(0);
f.render_widget(table, size);
})
.unwrap();
terminal.backend().assert_buffer(&expected);
};
}
// columns of zero width show nothing
test_case(
@@ -312,15 +319,17 @@ fn widgets_table_columns_widths_can_use_mixed_constraints() {
terminal
.draw(|f| {
let size = f.size();
let table = Table::new(vec![
Row::new(vec!["Row11", "Row12", "Row13"]),
Row::new(vec!["Row21", "Row22", "Row23"]),
Row::new(vec!["Row31", "Row32", "Row33"]),
Row::new(vec!["Row41", "Row42", "Row43"]),
])
let table = Table::new(
vec![
Row::new(vec!["Row11", "Row12", "Row13"]),
Row::new(vec!["Row21", "Row22", "Row23"]),
Row::new(vec!["Row31", "Row32", "Row33"]),
Row::new(vec!["Row41", "Row42", "Row43"]),
],
widths,
)
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
.block(Block::default().borders(Borders::ALL))
.widths(widths);
.block(Block::default().borders(Borders::ALL));
f.render_widget(table, size);
})
.unwrap();
@@ -422,15 +431,17 @@ fn widgets_table_columns_widths_can_use_ratio_constraints() {
terminal
.draw(|f| {
let size = f.size();
let table = Table::new(vec![
Row::new(vec!["Row11", "Row12", "Row13"]),
Row::new(vec!["Row21", "Row22", "Row23"]),
Row::new(vec!["Row31", "Row32", "Row33"]),
Row::new(vec!["Row41", "Row42", "Row43"]),
])
let table = Table::new(
vec![
Row::new(vec!["Row11", "Row12", "Row13"]),
Row::new(vec!["Row21", "Row22", "Row23"]),
Row::new(vec!["Row31", "Row32", "Row33"]),
Row::new(vec!["Row41", "Row42", "Row43"]),
],
widths,
)
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
.block(Block::default().borders(Borders::ALL))
.widths(widths)
.column_spacing(0);
f.render_widget(table, size);
})
@@ -527,20 +538,22 @@ fn widgets_table_can_have_rows_with_multi_lines() {
terminal
.draw(|f| {
let size = f.size();
let table = Table::new(vec![
Row::new(vec!["Row11", "Row12", "Row13"]),
Row::new(vec!["Row21", "Row22", "Row23"]).height(2),
Row::new(vec!["Row31", "Row32", "Row33"]),
Row::new(vec!["Row41", "Row42", "Row43"]).height(2),
])
let table = Table::new(
vec![
Row::new(vec!["Row11", "Row12", "Row13"]),
Row::new(vec!["Row21", "Row22", "Row23"]).height(2),
Row::new(vec!["Row31", "Row32", "Row33"]),
Row::new(vec!["Row41", "Row42", "Row43"]).height(2),
],
[
Constraint::Length(5),
Constraint::Length(5),
Constraint::Length(5),
],
)
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
.block(Block::default().borders(Borders::ALL))
.highlight_symbol(">> ")
.widths(&[
Constraint::Length(5),
Constraint::Length(5),
Constraint::Length(5),
])
.column_spacing(1);
f.render_stateful_widget(table, size, state);
})
@@ -621,21 +634,23 @@ fn widgets_table_enable_always_highlight_spacing() {
terminal
.draw(|f| {
let size = f.size();
let table = Table::new(vec![
Row::new(vec!["Row11", "Row12", "Row13"]),
Row::new(vec!["Row21", "Row22", "Row23"]).height(2),
Row::new(vec!["Row31", "Row32", "Row33"]),
Row::new(vec!["Row41", "Row42", "Row43"]).height(2),
])
let table = Table::new(
vec![
Row::new(vec!["Row11", "Row12", "Row13"]),
Row::new(vec!["Row21", "Row22", "Row23"]).height(2),
Row::new(vec!["Row31", "Row32", "Row33"]),
Row::new(vec!["Row41", "Row42", "Row43"]).height(2),
],
[
Constraint::Length(5),
Constraint::Length(5),
Constraint::Length(5),
],
)
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
.block(Block::default().borders(Borders::ALL))
.highlight_symbol(">> ")
.highlight_spacing(space)
.widths(&[
Constraint::Length(5),
Constraint::Length(5),
Constraint::Length(5),
])
.column_spacing(1);
f.render_stateful_widget(table, size, state);
})
@@ -755,28 +770,31 @@ fn widgets_table_can_have_elements_styled_individually() {
terminal
.draw(|f| {
let size = f.size();
let table = Table::new(vec![
Row::new(vec!["Row11", "Row12", "Row13"]).style(Style::default().fg(Color::Green)),
Row::new(vec![
Cell::from("Row21"),
Cell::from("Row22").style(Style::default().fg(Color::Yellow)),
Cell::from(Line::from(vec![
Span::raw("Row"),
Span::styled("23", Style::default().fg(Color::Blue)),
]))
.style(Style::default().fg(Color::Red)),
])
.style(Style::default().fg(Color::LightGreen)),
])
let table = Table::new(
vec![
Row::new(vec!["Row11", "Row12", "Row13"])
.style(Style::default().fg(Color::Green)),
Row::new(vec![
Cell::from("Row21"),
Cell::from("Row22").style(Style::default().fg(Color::Yellow)),
Cell::from(Line::from(vec![
Span::raw("Row"),
Span::styled("23", Style::default().fg(Color::Blue)),
]))
.style(Style::default().fg(Color::Red)),
])
.style(Style::default().fg(Color::LightGreen)),
],
[
Constraint::Length(6),
Constraint::Length(6),
Constraint::Length(6),
],
)
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
.block(Block::default().borders(Borders::LEFT | Borders::RIGHT))
.highlight_symbol(">> ")
.highlight_style(Style::default().add_modifier(Modifier::BOLD))
.widths(&[
Constraint::Length(6),
Constraint::Length(6),
Constraint::Length(6),
])
.column_spacing(1);
f.render_stateful_widget(table, size, &mut state);
})
@@ -830,15 +848,17 @@ fn widgets_table_should_render_even_if_empty() {
terminal
.draw(|f| {
let size = f.size();
let table = Table::new(vec![])
.header(Row::new(vec!["Head1", "Head2", "Head3"]))
.block(Block::default().borders(Borders::LEFT | Borders::RIGHT))
.widths(&[
let table = Table::new(
vec![],
[
Constraint::Length(6),
Constraint::Length(6),
Constraint::Length(6),
])
.column_spacing(1);
],
)
.header(Row::new(vec!["Head1", "Head2", "Head3"]))
.block(Block::default().borders(Borders::LEFT | Borders::RIGHT))
.column_spacing(1);
f.render_widget(table, size);
})
.unwrap();
@@ -868,17 +888,19 @@ fn widgets_table_columns_dont_panic() {
// based on https://github.com/fdehau/tui-rs/issues/470#issuecomment-852562848
let table1_width = 98;
let table1 = Table::new(vec![Row::new(vec!["r1", "r2", "r3", "r4"])])
.header(Row::new(vec!["h1", "h2", "h3", "h4"]))
.block(Block::default().borders(Borders::ALL))
.highlight_symbol(">> ")
.column_spacing(1)
.widths(&[
let table1 = Table::new(
vec![Row::new(vec!["r1", "r2", "r3", "r4"])],
[
Constraint::Percentage(15),
Constraint::Percentage(15),
Constraint::Percentage(25),
Constraint::Percentage(45),
]);
],
)
.header(Row::new(vec!["h1", "h2", "h3", "h4"]))
.block(Block::default().borders(Borders::ALL))
.highlight_symbol(">> ")
.column_spacing(1);
let mut state = TableState::default();
@@ -898,21 +920,23 @@ fn widgets_table_should_clamp_offset_if_rows_are_removed() {
terminal
.draw(|f| {
let size = f.size();
let table = Table::new(vec![
Row::new(vec!["Row01", "Row02", "Row03"]),
Row::new(vec!["Row11", "Row12", "Row13"]),
Row::new(vec!["Row21", "Row22", "Row23"]),
Row::new(vec!["Row31", "Row32", "Row33"]),
Row::new(vec!["Row41", "Row42", "Row43"]),
Row::new(vec!["Row51", "Row52", "Row53"]),
])
let table = Table::new(
vec![
Row::new(vec!["Row01", "Row02", "Row03"]),
Row::new(vec!["Row11", "Row12", "Row13"]),
Row::new(vec!["Row21", "Row22", "Row23"]),
Row::new(vec!["Row31", "Row32", "Row33"]),
Row::new(vec!["Row41", "Row42", "Row43"]),
Row::new(vec!["Row51", "Row52", "Row53"]),
],
[
Constraint::Length(5),
Constraint::Length(5),
Constraint::Length(5),
],
)
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
.block(Block::default().borders(Borders::ALL))
.widths(&[
Constraint::Length(5),
Constraint::Length(5),
Constraint::Length(5),
])
.column_spacing(1);
f.render_stateful_widget(table, size, &mut state);
})
@@ -934,15 +958,17 @@ fn widgets_table_should_clamp_offset_if_rows_are_removed() {
terminal
.draw(|f| {
let size = f.size();
let table = Table::new(vec![Row::new(vec!["Row31", "Row32", "Row33"])])
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
.block(Block::default().borders(Borders::ALL))
.widths(&[
let table = Table::new(
vec![Row::new(vec!["Row31", "Row32", "Row33"])],
[
Constraint::Length(5),
Constraint::Length(5),
Constraint::Length(5),
])
.column_spacing(1);
],
)
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
.block(Block::default().borders(Borders::ALL))
.column_spacing(1);
f.render_stateful_widget(table, size, &mut state);
})
.unwrap();

View File

@@ -1,5 +1,11 @@
use ratatui::{
backend::TestBackend, buffer::Buffer, layout::Rect, symbols, text::Line, widgets::Tabs,
backend::TestBackend,
buffer::Buffer,
layout::Rect,
style::{Style, Stylize},
symbols,
text::Line,
widgets::Tabs,
Terminal,
};
@@ -43,6 +49,7 @@ fn widgets_tabs_should_truncate_the_last_item() {
);
})
.unwrap();
let expected = Buffer::with_lines(vec![format!(" Tab1 {} T ", symbols::line::VERTICAL)]);
let mut expected = Buffer::with_lines(vec![format!(" Tab1 {} T ", symbols::line::VERTICAL)]);
expected.set_style(Rect::new(1, 0, 4, 1), Style::new().reversed());
terminal.backend().assert_buffer(&expected);
}