Compare commits

..

101 Commits

Author SHA1 Message Date
multisn8
fe06f0c7b0 feat(serde): support TableState, ListState, and ScrollbarState (#723)
TableState, ListState, and ScrollbarState can now be serialized and deserialized
using serde.

```rust
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
struct AppState {
    list_state: ListState,
    table_state: TableState,
    scrollbar_state: ScrollbarState,
}

let app_state = AppState::default();
let serialized = serde_json::to_string(app_state);

let app_state = serde_json::from_str(serialized);
```
2024-01-12 16:13:35 -08:00
Christian Stefanescu
43b2b57191 docs: fix typo in Table widget description (#797) 2024-01-12 21:11:56 +01:00
Valentin271
50b81c9d4e fix(examples/scrollbar): title wasn't displayed because of background reset (#795) 2024-01-12 17:53:35 +01:00
Dheepak Krishnamurthy
2b4aa46a6a docs: GitHub admonition syntax for examples README.md (#791)
* docs: GitHub admonition syntax for examples README.md

* docs: Add link to stable release
2024-01-11 21:08:54 -05:00
Josh McKinney
e1e85aa7af feat(style): add material design color palette (#786)
The `ratatui::style::palette::material` module contains the Google 2014
Material Design palette.

See https://m2.material.io/design/color/the-color-system.html#tools-for-picking-colors
for more information.

```rust
use ratatui::style::palette::material::BLUE_GRAY;
Line::styled("Hello", BLUE_GRAY.c500);
```
2024-01-11 19:22:57 +01:00
Josh McKinney
bf67850739 feat(style): add tailwind color palette (#787)
The `ratatui::style::palette::tailwind` module contains the default
Tailwind color palette. This is useful for styling components with
colors that match the Tailwind color palette.

See https://tailwindcss.com/docs/customizing-colors for more information
on Tailwind.

```rust
use ratatui::style::palette::tailwind::SLATE;
Line::styled("Hello", SLATE.c500);
```
2024-01-11 10:07:53 -08:00
Josh McKinney
1561d64c80 feat(layout): add Rect -> Size conversion methods (#789)
- add Size::new() constructor
- add Rect::as_size()
- impl From<Rect> for Size
- document and add tests for Size
2024-01-11 17:39:53 +01:00
Josh McKinney
ffd5fc79fc feat(color): add Color::from_u32 constructor (#785)
Convert a u32 in the format 0x00RRGGBB to a Color.

```rust
let white = Color::from_u32(0x00FFFFFF);
let black = Color::from_u32(0x00000000);
```
2024-01-11 04:23:16 -08:00
Josh McKinney
34648941d4 docs(examples): add warning about examples matching the main branch (#778) 2024-01-11 08:44:23 +01:00
Eric Lunderberg
c69ca47922 feat(table)!: Collect iterator of Row into Table (#774)
Any iterator whose item is convertible into `Row` can now be
collected into a `Table`.

Where previously, `Table::new` accepted `IntoIterator<Item = Row>`, it
now accepts `IntoIterator<Item: Into<Row>>`.

BREAKING CHANGE:
The compiler can no longer infer the element type of the container
passed to `Table::new()`.  For example, `Table::new(vec![], widths)`
will no longer compile, as the type of `vec![]` can no longer be
inferred.
2024-01-10 17:32:58 -08:00
Eric Lunderberg
f29c73fb1c feat(tabs): accept Iterators of Line in constructors (#776)
Any iterator whose item is convertible into `Line` can now be
collected into `Tabs`.

In addition, where previously `Tabs::new` required a `Vec`, it can now
accept any object that implements `IntoIterator` with an item type
implementing `Into<Line>`.

BREAKING CHANGE:

Calls to `Tabs::new()` whose argument is collected from an iterator
will no longer compile.  For example,
`Tabs::new(["a","b"].into_iter().collect())` will no longer compile,
because the return type of `.collect()` can no longer be inferred to
be a `Vec<_>`.
2024-01-10 17:16:44 -08:00
Dheepak Krishnamurthy
f2eab71ccf fix: broken tests in table.rs (#784)
* fix: broken tests in table.rs

* fix: Use default instead of raw
2024-01-10 19:57:38 +01:00
Josh McKinney
6645d2e058 fix(table)!: ensure that default and new() match (#751)
In https://github.com/ratatui-org/ratatui/pull/660 we introduced the
segment_size field to the Table struct. However, we forgot to update
the default() implementation to match the new() implementation. This
meant that the default() implementation picked up SegmentSize::default()
instead of SegmentSize::None.

Additionally the introduction of Table::default() in an earlier PR,
https://github.com/ratatui-org/ratatui/pull/339, was also missing the
default for the column_spacing field (1).

This commit fixes the default() implementation to match the new()
implementation of these two fields by implementing the Default trait
manually.

BREAKING CHANGE: The default() implementation of Table now sets the
column_spacing field to 1 and the segment_size field to
SegmentSize::None. This will affect the rendering of a small amount of
apps.
2024-01-10 17:16:03 +01:00
Josh McKinney
2faa879658 feat(table): accept Text for highlight_symbol (#781)
This allows for multi-line symbols to be used as the highlight symbol.

```rust
let table = Table::new(rows, widths)
    .highlight_symbol(Text::from(vec![
        "".into(),
        " █ ".into(),
        " █ ".into(),
        "".into(),
    ]));
```
2024-01-10 17:11:37 +01:00
Eric Lunderberg
eb79256cee feat(widgets): Collect iterator of ListItem into List (#775)
Any iterator whose item is convertible into `ListItem` can now be
collected into a `List`.

```rust
let list: List = (0..3).map(|i| format!("Item{i}")).collect();
```
2024-01-10 00:06:03 -08:00
Josh McKinney
388aa467f1 docs: update crate, lib and readme links (#771)
Link to the contributing, changelog, and breaking changes docs at the
top of the page instead of just in in the main part of the doc. This
makes it easier to find them.

Rearrange the links to be in a more logical order.

Use link refs for all the links

Fix up the CI link to point to the right workflow
2024-01-08 23:52:47 -08:00
Valentin271
8dd177a051 fix: fix PR write permission to upload unsigned commit comment (#770) 2024-01-08 16:26:15 +01:00
Prisacaru Bogdan-Paul
bbf2f906fb feat(rect.rs): implement Rows and Columns iterators in Rect (#765)
This enables iterating over rows and columns of a Rect. In tern being able to use that with other iterators and simplify looping over cells.
2024-01-08 15:51:19 +01:00
Valentin271
c24216cf30 chore: add comment on PRs with unsigned commits (#768) 2024-01-08 04:30:06 -08:00
Valentin271
5db895dcbf chore(deps): update termion to v3.0 (#767)
See Termion changelog at https://gitlab.redox-os.org/redox-os/termion#200-to-300-guide
2024-01-08 11:38:41 +01:00
Dheepak Krishnamurthy
c50ff08a63 feat: Add frame count (#766) 2024-01-08 03:51:53 -05:00
Valentin271
06141900b4 fix(cd): fix grepping the last release (#762) 2024-01-07 16:14:16 +01:00
Valentin271
fab943b61a chore(contributing): add deprecation notice guideline (#761) 2024-01-07 14:44:51 +01:00
Valentin271
a67815e138 fix(chart): exclude unnamed datasets from legend (#753)
A dataset with no name won't display an empty line anymore in the legend.
If no dataset have name, then no legend is ever displayed.
2024-01-07 13:21:38 +01:00
Valentin271
bd6b91c958 refactor!: make patch_style & reset_style chainable (#754)
Previously, `patch_style` and `reset_style` in `Text`, `Line` and `Span`
 were using a mutable reference to `Self`. To be more consistent with
 the rest of `ratatui`, which is using fluent setters, these now take
 ownership of `Self` and return it.
2024-01-07 12:58:13 +01:00
Josh McKinney
5aba988fac refactor(terminal): extract types to files (#760)
Fields on Frame that were private are now pub(crate).
2024-01-07 12:53:16 +01:00
Eric Lunderberg
e64e194b6b feat(table): Implement FromIterator for widgets::Row (#755)
The `Row::new` constructor accepts a single argument that implements
`IntoIterator`.  This commit adds an implementation of `FromIterator`,
as a thin wrapper around `Row::new`.  This allows `.collect::<Row>()`
to be used at the end of an iterator chain, rather than wrapping the
entire iterator chain in `Row::new`.
2024-01-06 20:23:38 -08:00
Valentin271
bc274e2bd9 refactor(block)!: remove deprecated title_on_bottom (#757)
`Block::title_on_bottom` was deprecated in v0.22. Use `Block::title` and `Title::position` instead.
2024-01-06 14:26:02 -08:00
Josh McKinney
f13fd73d9e feat(layout): add Rect::clamp() method (#749)
* feat(layout): add a Rect::clamp() method

This ensures a rectangle does not end up outside an area. This is useful
when you want to be able to dynamically move a rectangle around, but
keep it constrained to a certain area.

For example, this can be used to implement a draggable window that can
be moved around, but not outside the terminal window.

```rust
let window_area = Rect::new(state.x, state.y, 20, 20).clamp(area);
state.x = rect.x;
state.y = rect.y;
```

* refactor: use rstest to simplify clamp test

* fix: use rstest description instead of string

test layout::rect::tests::clamp::case_01_inside ... ok
test layout::rect::tests::clamp::case_02_up_left ... ok
test layout::rect::tests::clamp::case_04_up_right ... ok
test layout::rect::tests::clamp::case_05_left ... ok
test layout::rect::tests::clamp::case_03_up ... ok
test layout::rect::tests::clamp::case_06_right ... ok
test layout::rect::tests::clamp::case_07_down_left ... ok
test layout::rect::tests::clamp::case_08_down ... ok
test layout::rect::tests::clamp::case_09_down_right ... ok
test layout::rect::tests::clamp::case_10_too_wide ... ok
test layout::rect::tests::clamp::case_11_too_tall ... ok
test layout::rect::tests::clamp::case_12_too_large ... ok

* fix: less ambiguous docs for this / other rect

* fix: move rstest to dev deps
2024-01-05 20:38:30 +01:00
Josh McKinney
fe84141119 docs(layout): document the difference in the split methods (#750)
* docs(layout): document the difference in the split methods

* fix: doc suggestion
2024-01-05 20:19:53 +01:00
Josh McKinney
fb93db0730 docs(examples): simplify docs using new layout methods (#731)
Use the new `Layout::horizontal` and `vertical` constructors and
`Rect::split_array` through all the examples.
2024-01-05 07:45:14 -08:00
Valentin271
23f6938498 feat(block): add Block::bordered (#736)
This avoid creating a block with no borders and then settings Borders::ALL. i.e.

```diff
- Block::default().borders(Borders::ALL);
+ Block::bordered();
```
2024-01-05 12:19:49 +01:00
Josh McKinney
98bcf1c0a5 feat(layout): add Rect::split method (#729)
This method splits a Rect and returns a fixed-size array of the
resulting Rects. This allows the caller to use array destructuring
to get the individual Rects.

```rust
use Constraint::*;
let layout = &Layout::vertical([Length(1), Min(0)]);
let [top, main] = area.split(&layout);
```
2024-01-05 03:13:57 -08:00
Josh McKinney
803a72df27 feat(table): accept Into<Constraint> for widths (#745)
This allows Table constructors to accept any type that implements
Into<Constraint> instead of just AsRef<Constraint>. This is useful when
you want to specify a fixed size for a table columns, but don't want to
explicitly create a Constraint::Length yourself.

```rust
Table::new(rows, [1,2,3])
Table::default().widths([1,2,3])
```
2024-01-04 22:41:48 -08:00
Josh McKinney
0494ee52f1 feat(layout): accept Into<Constraint> for constructors (#744)
This allows Layout constructors to accept any type that implements
Into<Constraint> instead of just AsRef<Constraint>. This is useful when
you want to specify a fixed size for a layout, but don't want to
explicitly create a Constraint::Length yourself.

```rust
Layout::new(Direction::Vertical, [1, 2, 3]);
Layout::horizontal([1, 2, 3]);
Layout::vertical([1, 2, 3]);
Layout::default().constraints([1, 2, 3]);
```
2024-01-04 22:36:37 -08:00
Josh McKinney
6d15b2570f refactor(layout): move the remaining types (#743)
- alignment -> layout/alignment.rs
- corner -> layout/corner.rs
- direction -> layout/direction.rs
- size -> layout/size.rs
2024-01-04 22:35:12 -08:00
Josh McKinney
659460e19c refactor(layout): move SegmentSize to layout/segment_size.rs (#742) 2024-01-04 20:53:57 -08:00
Josh McKinney
ba036cd579 refactor(layout): move Layout to layout/layout.rs (#741) 2024-01-04 20:43:00 -08:00
Josh McKinney
8724aeb9e7 refactor(layout): move Margin to margin.rs (#740) 2024-01-04 20:34:42 -08:00
Josh McKinney
da6c299804 refactor: extract layout::Constraint to file (#739) 2024-01-04 20:13:11 -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
110 changed files with 14410 additions and 5658 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 -P "v0+\.\d+\.\d+$" | tail -1)"
echo "🐭 Last release: ${last_release}"
# detect breaking changes
if [ -n "$(git log --oneline ${last_release}..HEAD | grep '!:')" ]; 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

@@ -44,15 +44,37 @@ jobs:
header: pr-title-lint-error
delete: true
check-signed:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
# Check commit signature and add comment if needed
- name: Check signed commits in PR
uses: 1Password/check-signed-commits-action@v1
with:
comment: |
Thank you for opening this pull request!
We require commits to be signed and it looks like this PR contains unsigned commits.
Get help in the [CONTRIBUTING.md](https://github.com/ratatui-org/ratatui/blob/main/CONTRIBUTING.md#sign-your-commits)
or on [Github doc](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits).
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

View File

@@ -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,10 +10,20 @@ github with a [breaking change] label.
This is a quick summary of the sections below:
- Unreleased (0.24.1)
-`Table::widths()` now accepts `AsRef<[Constraint]>`
- [v0.26.0 (unreleased)](#v0260-unreleased)
- `patch_style` & `reset_style` now consume and return `Self`
- Removed deprecated `Block::title_on_bottom`
- `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
- `Tabs::new()` now accepts `IntoIterator<Item: Into<Line<'a>>>`
- `Table::new` now accepts `IntoIterator<Item: Into<Row<'a>>>`.
- [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`
@@ -36,11 +46,185 @@ This is a quick summary of the sections below:
- MSRV is now 1.63.0
- `List` no longer ignores empty strings
## Unreleased (v0.24.1)
## v0.26.0 (unreleased)
### `Table::widths()` now accepts `AsRef<[Constraint]>` ([#628])
### `Table::new()` now accepts `IntoIterator<Item: Into<Row<'a>>>` ([#774])
[#628]: https://github.com/ratatui-org/ratatui/pull/628
[#774]: https://github.com/ratatui-org/ratatui/pull/774
Previously, `Table::new()` accepted `IntoIterator<Item=Row<'a>>`. The argument change to
`IntoIterator<Item: Into<Row<'a>>>`, This allows more flexible types from calling scopes, though it
can some break type inference in the calling scope for empty containers.
This can be resolved either by providing an explicit type (e.g. `Vec::<Row>::new()`), or by using
`Table::default()`.
```diff
- let table = Table::new(vec![], widths);
// becomes
+ let table = Table::default().widths(widths);
```
### `Tabs::new()` now accepts `IntoIterator<Item: Into<Line<'a>>>` ([#776])
[#776]: https://github.com/ratatui-org/ratatui/pull/776
Previously, `Tabs::new()` accepted `Vec<T>` where `T: Into<Line<'a>>`. This allows more flexible
types from calling scopes, though it can break some type inference in the calling scope.
This typically occurs when collecting an iterator prior to calling `Tabs::new`, and can be resolved
by removing the call to `.collect()`.
```diff
- let tabs = Tabs::new((0.3).map(|i| format!("{i}")).collect());
// becomes
+ let tabs = Tabs::new((0.3).map(|i| format!("{i}")));
```
### Table::default() now sets segment_size to None and column_spacing to ([#751])
[#751]: https://github.com/ratatui-org/ratatui/pull/751
The default() implementation of Table now sets the column_spacing field to 1 and the segment_size
field to SegmentSize::None. This will affect the rendering of a small amount of apps.
To use the previous default values, call `table.segment_size(Default::default())` and
`table.column_spacing(0)`.
### `patch_style` & `reset_style` now consumes and returns `Self` ([#754])
[#754]: https://github.com/ratatui-org/ratatui/pull/754
Previously, `patch_style` and `reset_style` in `Text`, `Line` and `Span` were using a mutable
reference to `Self`. To be more consistent with the rest of `ratatui`, which is using fluent
setters, these now take ownership of `Self` and return it.
The following example shows how to migrate for `Line`, but the same applies for `Text` and `Span`.
```diff
- let mut line = Line::from("foobar");
- line.patch_style(style);
// becomes
+ let line = Line::new("foobar").patch_style(style);
```
### Remove deprecated `Block::title_on_bottom` ([#757])
[#757]: https://github.com/ratatui-org/ratatui/pull/757
`Block::title_on_bottom` was deprecated in v0.22. Use `Block::title` and `Title::position` instead.
```diff
- block.title("foobar").title_on_bottom();
+ block.title(Title::from("foobar").position(Position::Bottom));
```
### `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
@@ -48,10 +232,10 @@ the `&`.
E.g.
```rust
let table = Table::new(rows).widths(&[Constraint::Length(1)]);
```diff
- let table = Table::new(rows).widths(&[Constraint::Length(1)]);
// becomes
let table = Table::new(rows).widths([Constraint::Length(1)]);
+ let table = Table::new(rows, [Constraint::Length(1)]);
```
### Layout::new() now accepts direction and constraint parameters ([#557])
@@ -90,10 +274,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])
@@ -104,10 +288,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])
@@ -119,13 +303,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])
@@ -135,12 +319,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)
@@ -151,10 +335,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])
@@ -165,10 +349,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])
@@ -189,7 +373,7 @@ changelog](https://github.com/bitflags/bitflags/blob/main/CHANGELOG.md#200-rc2).
## [v0.21.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.21.0)
### MSRV is 1.65.0 ([#171])
### MSRV is 1.65.0 ([#171])
[#171]: https://github.com/ratatui-org/ratatui/issues/171
@@ -200,15 +384,15 @@ The minimum supported rust version is now 1.65.0.
[#114]: https://github.com/ratatui-org/ratatui/issues/114
In order to support inline viewports, the unstable method `Terminal::with_options()` was stabilized
and `ViewPort` was changed from a struct to an enum.
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),
});
```
@@ -217,13 +401,13 @@ let terminal = Terminal::with_options(backend, TerminalOptions {
[#168]: https://github.com/ratatui-org/ratatui/issues/168
A new type `Masked` was introduced that implements `From<Text<'a>>`. This causes any code that did
previously did not need to use type annotations to fail to compile. To fix this, annotate or call
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])
@@ -234,10 +418,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

@@ -171,6 +171,12 @@ struct Foo {}
- Code items should be between backticks
i.e. ``[`Block`]``, **NOT** ``[Block]``
### Deprecation notice
We generally want to wait at least two versions before removing deprecated items so users have
time to update. However, if a deprecation is blocking for us to implement a new feature we may
*consider* removing it in a one version notice.
### Use of unsafe for optimization purposes
We don't currently use any unsafe code in Ratatui, and would like to keep it that way. However there

View File

@@ -1,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/"
@@ -24,7 +24,7 @@ rust-version = "1.70.0"
[dependencies]
crossterm = { version = "0.27", optional = true }
termion = { version = "2.0", optional = true }
termion = { version = "3.0", optional = true }
termwiz = { version = "0.20.0", optional = true }
serde = { version = "1", optional = true, features = ["derive"] }
@@ -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,14 @@ 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"
rstest = "0.18.2"
serde_json = "1.0.109"
[features]
#! The crate provides a set of optional features that can be enabled in your `cargo.toml` file.
@@ -89,6 +93,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
@@ -216,6 +235,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"]
@@ -245,3 +269,7 @@ doc-scrape-examples = true
name = "inline"
required-features = ["crossterm"]
doc-scrape-examples = true
[[test]]
name = "state_serde"
required-features = ["serde"]

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

121
README.md
View File

@@ -25,27 +25,22 @@
<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)
· [Send a Pull Request](https://github.com/ratatui-org/ratatui/compare)
[![Crate Badge]][Crate] [![Docs Badge]][API Docs] [![CI Badge]][CI Workflow] [![License
Badge]](./LICENSE)<br>
[![Codecov Badge]][Codecov] [![Deps.rs Badge]][Deps.rs] [![Discord Badge]][Discord Server]
[![Matrix Badge]][Matrix]<br>
[Ratatui Website] · [API Docs] · [Examples]<br>
[Contributing] · [Changelog] · [Breaking Changes]<br>
[Report a bug] · [Request a Feature] · [Create a Pull Request]
</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][Ratatui Website] 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 +51,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,16 +59,16 @@ 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
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
- [API Docs] - the full API documentation for the library on docs.rs.
- [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].
- [Contributing] - Please read this if you are interested in contributing to the project.
- [Changelog] - generated by [git-cliff] utilizing [Conventional Commits].
- [Breaking Changes] - a list of breaking changes in the library.
## Quickstart
@@ -81,12 +76,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
[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:
@@ -110,30 +104,31 @@ 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
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
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::*};
@@ -159,7 +154,7 @@ fn handle_events() -> io::Result<bool> {
if key.kind == event::KeyEventKind::Press && key.code == KeyCode::Char('q') {
return Ok(true);
}
}
}
}
Ok(false)
}
@@ -182,7 +177,7 @@ 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::*};
@@ -194,7 +189,7 @@ fn ui(frame: &mut Frame) {
Constraint::Length(1),
Constraint::Min(0),
Constraint::Length(1),
]
],
)
.split(frame.size());
frame.render_widget(
@@ -208,7 +203,7 @@ fn ui(frame: &mut Frame) {
let inner_layout = Layout::new(
Direction::Horizontal,
[Constraint::Percentage(50), Constraint::Percentage(50)]
[Constraint::Percentage(50), Constraint::Percentage(50)],
)
.split(main_layout[1]);
frame.render_widget(
@@ -236,7 +231,7 @@ 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::*};
@@ -250,7 +245,7 @@ fn ui(frame: &mut Frame) {
Constraint::Length(1),
Constraint::Length(1),
Constraint::Min(0),
]
],
)
.split(frame.size());
@@ -288,23 +283,25 @@ 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/concepts/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
[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
[Create a Pull Request]: https://github.com/ratatui-org/ratatui/compare
[git-cliff]: https://git-cliff.org
[Conventional Commits]: https://www.conventionalcommits.org
[API Documentation]: https://docs.rs/ratatui
[API Docs]: https://docs.rs/ratatui
[Changelog]: https://github.com/ratatui-org/ratatui/blob/main/CHANGELOG.md
[Contributing]: https://github.com/ratatui-org/ratatui/blob/main/CONTRIBUTING.md
[Breaking Changes]: https://github.com/ratatui-org/ratatui/blob/main/BREAKING-CHANGES.md
@@ -324,24 +321,29 @@ Running this example produces the following output:
[`Backend`]: backend::Backend
[`backend` module]: backend
[`crossterm::event`]: https://docs.rs/crossterm/latest/crossterm/event/index.html
[Ratatui]: https://ratatui.rs
[Crate]: https://crates.io/crates/ratatui
[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
[License Badge]: https://img.shields.io/crates/l/ratatui?style=flat-square
[CI Badge]:
https://img.shields.io/github/actions/workflow/status/ratatui-org/ratatui/ci.yml?style=flat-square&logo=github
[CI Workflow]: https://github.com/ratatui-org/ratatui/actions/workflows/ci.yml
[Codecov Badge]:
https://img.shields.io/codecov/c/github/ratatui-org/ratatui?logo=codecov&style=flat-square&token=BAQ8SOKEST
[Dependencies Badge]: https://deps.rs/repo/github/ratatui-org/ratatui/status.svg?style=flat-square
[Codecov]: https://app.codecov.io/gh/ratatui-org/ratatui
[Deps.rs Badge]: https://deps.rs/repo/github/ratatui-org/ratatui/status.svg?style=flat-square
[Deps.rs]: https://deps.rs/repo/github/ratatui-org/ratatui
[Discord Badge]:
https://img.shields.io/discord/1070692720437383208?label=discord&logo=discord&style=flat-square
[Discord Server]: https://discord.gg/pMCEU9hNEj
[Docs Badge]: https://img.shields.io/docsrs/ratatui?logo=rust&style=flat-square
[License Badge]: https://img.shields.io/crates/l/ratatui?style=flat-square
[Matrix Badge]:
https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix
[Matrix]: https://matrix.to/#/#ratatui:matrix.org
<!-- cargo-rdme end -->
@@ -405,7 +407,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
@@ -428,8 +429,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,5 +1,19 @@
# Examples
> [!WARNING]
> The examples in this folder run against the `main` branch. There may be backwards
> incompatible changes compared to the [latest stable version](https://github.com/ratatui-org/ratatui/releases/latest).
>
> If you find that code copied from an example fails to run in your own application, please check
> the tag of the release that your project is using.
>
> To view examples that match the version of Ratatui that you are using checkout the matching tag in
> your local git repository, or select the tag in the "Switch branches/tags" button if you're
> viewing this file on GitHub.
>
> To use incompatible example code as is, you can also use the most recent alpha release of Ratatui
> (we release these weekly).
These gifs were created using [VHS](https://github.com/charmbracelet/vhs). Each example has a
corresponding `.tape` file that holds instructions for how to generate the images. Note that the
images themselves are stored in a separate git branch to avoid bloating the main repository.
@@ -117,7 +131,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 +240,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 +300,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 +326,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 +338,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

@@ -134,11 +134,11 @@ fn run_app<B: Backend>(
}
}
fn ui(f: &mut Frame, app: &App) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)])
.split(f.size());
fn ui(frame: &mut Frame, app: &App) {
let vertical = Layout::vertical([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)]);
let horizontal = Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]);
let [top, bottom] = frame.size().split(&vertical);
let [left, right] = bottom.split(&horizontal);
let barchart = BarChart::default()
.block(Block::default().title("Data1").borders(Borders::ALL))
@@ -146,15 +146,10 @@ fn ui(f: &mut Frame, app: &App) {
.bar_width(9)
.bar_style(Style::default().fg(Color::Yellow))
.value_style(Style::default().fg(Color::Black).bg(Color::Yellow));
f.render_widget(barchart, chunks[0]);
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(chunks[1]);
draw_bar_with_group_labels(f, app, chunks[0]);
draw_horizontal_bars(f, app, chunks[1]);
frame.render_widget(barchart, top);
draw_bar_with_group_labels(frame, app, left);
draw_horizontal_bars(frame, app, right);
}
fn create_groups<'a>(app: &'a App, combine_values_and_labels: bool) -> Vec<BarGroup<'a>> {

View File

@@ -103,20 +103,14 @@ fn ui(frame: &mut Frame) {
///
/// Returns a tuple of the title area and the main areas.
fn calculate_layout(area: Rect) -> (Rect, Vec<Vec<Rect>>) {
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(1), Constraint::Min(0)])
.split(area);
let title_area = layout[0];
let main_areas = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Max(4); 9])
.split(layout[1])
let main_layout = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
let block_layout = &Layout::vertical([Constraint::Max(4); 9]);
let [title_area, main_area] = area.split(&main_layout);
let main_areas = block_layout
.split(main_area)
.iter()
.map(|&area| {
Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(area)
.to_vec()
})

View File

@@ -1,4 +1,4 @@
use std::{error::Error, io, rc::Rc};
use std::{error::Error, io};
use crossterm::{
event::{self, Event, KeyCode},
@@ -55,43 +55,19 @@ fn draw(f: &mut Frame) {
let list = make_dates(start.year());
for chunk in split_rows(&calarea)
.iter()
.flat_map(|row| split_cols(row).to_vec())
{
let rows = Layout::vertical([Constraint::Ratio(1, 3); 3]).split(calarea);
let cols = rows.iter().flat_map(|row| {
Layout::horizontal([Constraint::Ratio(1, 4); 4])
.split(*row)
.to_vec()
});
for col in cols {
let cal = cals::get_cal(start.month(), start.year(), &list);
f.render_widget(cal, chunk);
f.render_widget(cal, col);
start = start.replace_month(start.month().next()).unwrap();
}
}
fn split_rows(area: &Rect) -> Rc<[Rect]> {
let list_layout = Layout::default()
.direction(Direction::Vertical)
.margin(0)
.constraints([
Constraint::Percentage(33),
Constraint::Percentage(33),
Constraint::Percentage(33),
]);
list_layout.split(*area)
}
fn split_cols(area: &Rect) -> Rc<[Rect]> {
let list_layout = Layout::default()
.direction(Direction::Horizontal)
.margin(0)
.constraints([
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
]);
list_layout.split(*area)
}
fn make_dates(current_year: i32) -> CalendarEventStore {
let mut list = CalendarEventStore::today(
Style::default()
@@ -175,7 +151,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 +164,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 +174,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 +185,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 +201,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 +217,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 +231,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,
_ => {}
}
}
@@ -107,19 +107,15 @@ impl App {
}
fn ui(&self, frame: &mut Frame) {
let main_layout = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(frame.size());
let horizontal =
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]);
let vertical = Layout::vertical([Constraint::Percentage(50), Constraint::Percentage(50)]);
let [map, right] = frame.size().split(&horizontal);
let [pong, boxes] = right.split(&vertical);
let right_layout = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(main_layout[1]);
frame.render_widget(self.map_canvas(), main_layout[0]);
frame.render_widget(self.pong_canvas(), right_layout[0]);
frame.render_widget(self.boxes_canvas(right_layout[1]), right_layout[1]);
frame.render_widget(self.map_canvas(), map);
frame.render_widget(self.pong_canvas(), pong);
frame.render_widget(self.boxes_canvas(boxes), boxes);
}
fn map_canvas(&self) -> impl Widget + '_ {

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 {
@@ -140,16 +132,20 @@ 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);
fn ui(frame: &mut Frame, app: &App) {
let area = frame.size();
let vertical = Layout::vertical([Constraint::Percentage(40), Constraint::Percentage(60)]);
let horizontal = Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]);
let [chart1, bottom] = area.split(&vertical);
let [line_chart, scatter] = bottom.split(&horizontal);
render_chart1(frame, chart1, app);
render_line_chart(frame, line_chart);
render_scatter(frame, scatter);
}
fn render_chart1(f: &mut Frame, area: Rect, app: &App) {
let x_labels = vec![
Span::styled(
format!("{}", app.window[0]),
@@ -194,61 +190,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".italic())
.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".underlined())
.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

@@ -42,14 +42,12 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
}
fn ui(frame: &mut Frame) {
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(30),
Constraint::Length(17),
Constraint::Length(2),
])
.split(frame.size());
let layout = Layout::vertical([
Constraint::Length(30),
Constraint::Length(17),
Constraint::Length(2),
])
.split(frame.size());
render_named_colors(frame, layout[0]);
render_indexed_colors(frame, layout[1]);
@@ -76,10 +74,7 @@ const NAMED_COLORS: [Color; 16] = [
];
fn render_named_colors(frame: &mut Frame, area: Rect) {
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(3); 10])
.split(area);
let layout = Layout::vertical([Constraint::Length(3); 10]).split(area);
render_fg_named_colors(frame, Color::Reset, layout[0]);
render_fg_named_colors(frame, Color::Black, layout[1]);
@@ -99,15 +94,11 @@ fn render_fg_named_colors(frame: &mut Frame, bg: Color, area: Rect) {
let inner = block.inner(area);
frame.render_widget(block, area);
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(1); 2])
let layout = Layout::vertical([Constraint::Length(1); 2])
.split(inner)
.iter()
.flat_map(|area| {
Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Ratio(1, 8); 8])
Layout::horizontal([Constraint::Ratio(1, 8); 8])
.split(*area)
.to_vec()
})
@@ -124,15 +115,11 @@ fn render_bg_named_colors(frame: &mut Frame, fg: Color, area: Rect) {
let inner = block.inner(area);
frame.render_widget(block, area);
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(1); 2])
let layout = Layout::vertical([Constraint::Length(1); 2])
.split(inner)
.iter()
.flat_map(|area| {
Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Ratio(1, 8); 8])
Layout::horizontal([Constraint::Ratio(1, 8); 8])
.split(*area)
.to_vec()
})
@@ -149,23 +136,18 @@ fn render_indexed_colors(frame: &mut Frame, area: Rect) {
let inner = block.inner(area);
frame.render_widget(block, area);
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1), // 0 - 15
Constraint::Length(1), // blank
Constraint::Min(6), // 16 - 123
Constraint::Length(1), // blank
Constraint::Min(6), // 124 - 231
Constraint::Length(1), // blank
])
.split(inner);
let layout = Layout::vertical([
Constraint::Length(1), // 0 - 15
Constraint::Length(1), // blank
Constraint::Min(6), // 16 - 123
Constraint::Length(1), // blank
Constraint::Min(6), // 124 - 231
Constraint::Length(1), // blank
])
.split(inner);
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
let color_layout = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Length(5); 16])
.split(layout[0]);
let color_layout = Layout::horizontal([Constraint::Length(5); 16]).split(layout[0]);
for i in 0..16 {
let color = Color::Indexed(i);
let color_index = format!("{i:0>2}");
@@ -196,25 +178,19 @@ fn render_indexed_colors(frame: &mut Frame, area: Rect) {
.iter()
// two rows of 3 columns
.flat_map(|area| {
Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Length(27); 3])
Layout::horizontal([Constraint::Length(27); 3])
.split(*area)
.to_vec()
})
// each with 6 rows
.flat_map(|area| {
Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(1); 6])
Layout::vertical([Constraint::Length(1); 6])
.split(area)
.to_vec()
})
// each with 6 columns
.flat_map(|area| {
Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Min(4); 6])
Layout::horizontal([Constraint::Min(4); 6])
.split(area)
.to_vec()
})
@@ -244,22 +220,18 @@ fn title_block(title: String) -> Block<'static> {
}
fn render_indexed_grayscale(frame: &mut Frame, area: Rect) {
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1), // 232 - 243
Constraint::Length(1), // 244 - 255
])
.split(area)
.iter()
.flat_map(|area| {
Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Length(6); 12])
.split(*area)
.to_vec()
})
.collect_vec();
let layout = Layout::vertical([
Constraint::Length(1), // 232 - 243
Constraint::Length(1), // 244 - 255
])
.split(area)
.iter()
.flat_map(|area| {
Layout::horizontal([Constraint::Length(6); 12])
.split(*area)
.to_vec()
})
.collect_vec();
for i in 232..=255 {
let color = Color::Indexed(i);

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,136 @@ 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);
let vertical = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
let horizontal = Layout::horizontal([Constraint::Min(0), Constraint::Length(8)]);
let [top, colors] = area.split(&vertical);
let [title, fps] = top.split(&horizontal);
self.title.render(title, buf);
self.fps_widget.render(fps, buf);
self.rgb_colors_widget.render(colors, buf);
}
}
impl RgbColors {
fn layout(area: Rect) -> Rc<[Rect]> {
Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(1), Constraint::Min(0)])
.split(area)
}
fn render_title(area: Rect, buf: &mut Buffer) {
Paragraph::new("colors_rgb example. Press q to quit")
.dark_gray()
.alignment(Alignment::Center)
.render(area, buf);
}
/// Render a colored grid of half block characters (`"▀"`) each with a different RGB color.
fn render_colors(area: Rect, buf: &mut Buffer) {
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

@@ -171,42 +171,34 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
}
fn ui(frame: &mut Frame, states: &[State; 3]) {
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1),
Constraint::Max(3),
Constraint::Length(1),
Constraint::Min(0), // ignore remaining space
])
.split(frame.size());
let vertical = Layout::vertical([
Constraint::Length(1),
Constraint::Max(3),
Constraint::Length(1),
Constraint::Min(0), // ignore remaining space
]);
let [title, buttons, help, _] = frame.size().split(&vertical);
frame.render_widget(
Paragraph::new("Custom Widget Example (mouse enabled)"),
layout[0],
);
render_buttons(frame, layout[1], states);
frame.render_widget(
Paragraph::new("←/→: select, Space: toggle, q: quit"),
layout[2],
title,
);
render_buttons(frame, buttons, states);
frame.render_widget(Paragraph::new("←/→: select, Space: toggle, q: quit"), help);
}
fn render_buttons(frame: &mut Frame<'_>, area: Rect, states: &[State; 3]) {
let layout = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Length(15),
Constraint::Length(15),
Constraint::Length(15),
Constraint::Min(0), // ignore remaining space
])
.split(area);
frame.render_widget(Button::new("Red").theme(RED).state(states[0]), layout[0]);
frame.render_widget(
Button::new("Green").theme(GREEN).state(states[1]),
layout[1],
);
frame.render_widget(Button::new("Blue").theme(BLUE).state(states[2]), layout[2]);
let horizontal = Layout::horizontal([
Constraint::Length(15),
Constraint::Length(15),
Constraint::Length(15),
Constraint::Min(0), // ignore remaining space
]);
let [red, green, blue, _] = area.split(&horizontal);
frame.render_widget(Button::new("Red").theme(RED).state(states[0]), red);
frame.render_widget(Button::new("Green").theme(GREEN).state(states[1]), green);
frame.render_widget(Button::new("Blue").theme(BLUE).state(states[2]), blue);
}
fn handle_key_event(
@@ -216,12 +208,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

@@ -6,16 +6,13 @@ use ratatui::{
use crate::app::App;
pub fn draw(f: &mut Frame, app: &mut App) {
let chunks = Layout::default()
.constraints([Constraint::Length(3), Constraint::Min(0)])
.split(f.size());
let titles = app
let chunks = Layout::vertical([Constraint::Length(3), Constraint::Min(0)]).split(f.size());
let tabs = app
.tabs
.titles
.iter()
.map(|t| text::Line::from(Span::styled(*t, Style::default().fg(Color::Green))))
.collect();
let tabs = Tabs::new(titles)
.collect::<Tabs>()
.block(Block::default().borders(Borders::ALL).title(app.title))
.highlight_style(Style::default().fg(Color::Yellow))
.select(app.tabs.index);
@@ -29,27 +26,25 @@ pub fn draw(f: &mut Frame, app: &mut App) {
}
fn draw_first_tab(f: &mut Frame, app: &mut App, area: Rect) {
let chunks = Layout::default()
.constraints([
Constraint::Length(9),
Constraint::Min(8),
Constraint::Length(7),
])
.split(area);
let chunks = Layout::vertical([
Constraint::Length(9),
Constraint::Min(8),
Constraint::Length(7),
])
.split(area);
draw_gauges(f, app, chunks[0]);
draw_charts(f, app, chunks[1]);
draw_text(f, chunks[2]);
}
fn draw_gauges(f: &mut Frame, app: &mut App, area: Rect) {
let chunks = Layout::default()
.constraints([
Constraint::Length(2),
Constraint::Length(3),
Constraint::Length(1),
])
.margin(1)
.split(area);
let chunks = Layout::vertical([
Constraint::Length(2),
Constraint::Length(3),
Constraint::Length(1),
])
.margin(1)
.split(area);
let block = Block::default().borders(Borders::ALL).title("Graphs");
f.render_widget(block, area);
@@ -96,19 +91,14 @@ fn draw_charts(f: &mut Frame, app: &mut App, area: Rect) {
} else {
vec![Constraint::Percentage(100)]
};
let chunks = Layout::default()
.constraints(constraints)
.direction(Direction::Horizontal)
.split(area);
let chunks = Layout::horizontal(constraints).split(area);
{
let chunks = Layout::default()
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
let chunks = Layout::vertical([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(chunks[0]);
{
let chunks = Layout::default()
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.direction(Direction::Horizontal)
.split(chunks[0]);
let chunks =
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(chunks[0]);
// Draw tasks
let tasks: Vec<ListItem> = app
@@ -273,10 +263,8 @@ fn draw_text(f: &mut Frame, area: Rect) {
}
fn draw_second_tab(f: &mut Frame, app: &mut App, area: Rect) {
let chunks = Layout::default()
.constraints([Constraint::Percentage(30), Constraint::Percentage(70)])
.direction(Direction::Horizontal)
.split(area);
let chunks =
Layout::horizontal([Constraint::Percentage(30), Constraint::Percentage(70)]).split(area);
let up_style = Style::default().fg(Color::Green);
let failure_style = Style::default()
.fg(Color::Red)
@@ -289,18 +277,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()
@@ -359,10 +349,7 @@ fn draw_second_tab(f: &mut Frame, app: &mut App, area: Rect) {
}
fn draw_third_tab(f: &mut Frame, _app: &mut App, area: Rect) {
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)])
.split(area);
let chunks = Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]).split(area);
let colors = [
Color::Reset,
Color::Black,
@@ -393,12 +380,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

@@ -1,5 +1,3 @@
use std::rc::Rc;
use itertools::Itertools;
use ratatui::{prelude::*, widgets::*};
@@ -18,25 +16,31 @@ impl<'a> Root<'a> {
impl Widget for Root<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
Block::new().style(THEME.root).render(area, buf);
let area = layout(area, Direction::Vertical, vec![1, 0, 1]);
self.render_title_bar(area[0], buf);
self.render_selected_tab(area[1], buf);
self.render_bottom_bar(area[2], buf);
let vertical = Layout::vertical([
Constraint::Length(1),
Constraint::Min(0),
Constraint::Length(1),
]);
let [title_bar, tab, bottom_bar] = area.split(&vertical);
self.render_title_bar(title_bar, buf);
self.render_selected_tab(tab, buf);
self.render_bottom_bar(bottom_bar, buf);
}
}
impl Root<'_> {
fn render_title_bar(&self, area: Rect, buf: &mut Buffer) {
let area = layout(area, Direction::Horizontal, vec![0, 45]);
let horizontal = Layout::horizontal([Constraint::Min(0), Constraint::Length(45)]);
let [title, tabs] = area.split(&horizontal);
Paragraph::new(Span::styled("Ratatui", THEME.app_title)).render(area[0], buf);
Paragraph::new(Span::styled("Ratatui", THEME.app_title)).render(title, buf);
let titles = vec!["", " Recipe ", " Email ", " Traceroute ", " Weather "];
Tabs::new(titles)
.style(THEME.tabs)
.highlight_style(THEME.tabs_selected)
.select(self.context.tab_index)
.divider("")
.render(area[1], buf);
.render(tabs, buf);
}
fn render_selected_tab(&self, area: Rect, buf: &mut Buffer) {
@@ -73,21 +77,3 @@ impl Root<'_> {
.render(area, buf);
}
}
/// simple helper method to split an area into multiple sub-areas
pub fn layout(area: Rect, direction: Direction, heights: Vec<u16>) -> Rc<[Rect]> {
let constraints = heights
.iter()
.map(|&h| {
if h > 0 {
Constraint::Length(h)
} else {
Constraint::Min(0)
}
})
.collect_vec();
Layout::default()
.direction(direction)
.constraints(constraints)
.split(area)
}

View File

@@ -1,7 +1,7 @@
use itertools::Itertools;
use ratatui::{prelude::*, widgets::*};
use crate::{layout, RgbSwatch, THEME};
use crate::{RgbSwatch, THEME};
const RATATUI_LOGO: [&str; 32] = [
" ███ ",
@@ -51,9 +51,10 @@ impl AboutTab {
impl Widget for AboutTab {
fn render(self, area: Rect, buf: &mut Buffer) {
RgbSwatch.render(area, buf);
let area = layout(area, Direction::Horizontal, vec![34, 0]);
render_crate_description(area[1], buf);
render_logo(self.selected_row, area[0], buf);
let horizontal = Layout::horizontal([Constraint::Length(34), Constraint::Min(0)]);
let [description, logo] = area.split(&horizontal);
render_crate_description(description, buf);
render_logo(self.selected_row, logo, buf);
}
}

View File

@@ -2,7 +2,7 @@ use itertools::Itertools;
use ratatui::{prelude::*, widgets::*};
use unicode_width::UnicodeWidthStr;
use crate::{layout, RgbSwatch, THEME};
use crate::{RgbSwatch, THEME};
#[derive(Debug, Default)]
pub struct Email {
@@ -60,20 +60,22 @@ impl Widget for EmailTab {
horizontal: 2,
});
Clear.render(area, buf);
let area = layout(area, Direction::Vertical, vec![5, 0]);
render_inbox(self.selected_index, area[0], buf);
render_email(self.selected_index, area[1], buf);
let vertical = Layout::vertical([Constraint::Length(5), Constraint::Min(0)]);
let [inbox, email] = area.split(&vertical);
render_inbox(self.selected_index, inbox, buf);
render_email(self.selected_index, email, buf);
}
}
fn render_inbox(selected_index: usize, area: Rect, buf: &mut Buffer) {
let area = layout(area, Direction::Vertical, vec![1, 0]);
let vertical = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
let [tabs, inbox] = area.split(&vertical);
let theme = THEME.email;
Tabs::new(vec![" Inbox ", " Sent ", " Drafts "])
.style(theme.tabs)
.highlight_style(theme.tabs_selected)
.select(0)
.divider("")
.render(area[0], buf);
.render(tabs, buf);
let highlight_symbol = ">>";
let from_width = EMAILS
@@ -94,7 +96,7 @@ fn render_inbox(selected_index: usize, area: Rect, buf: &mut Buffer) {
.style(theme.inbox)
.highlight_style(theme.selected_item)
.highlight_symbol(highlight_symbol),
area[1],
inbox,
buf,
&mut state,
);
@@ -106,7 +108,7 @@ fn render_inbox(selected_index: usize, area: Rect, buf: &mut Buffer) {
.end_symbol(None)
.track_symbol(None)
.thumb_symbol("")
.render(area[1], buf, &mut scrollbar_state);
.render(inbox, buf, &mut scrollbar_state);
}
fn render_email(selected_index: usize, area: Rect, buf: &mut Buffer) {
@@ -120,7 +122,8 @@ fn render_email(selected_index: usize, area: Rect, buf: &mut Buffer) {
let inner = block.inner(area);
block.render(area, buf);
if let Some(email) = email {
let area = layout(inner, Direction::Vertical, vec![3, 0]);
let vertical = Layout::vertical([Constraint::Length(3), Constraint::Min(0)]);
let [headers_area, body_area] = inner.split(&vertical);
let headers = vec![
Line::from(vec![
"From: ".set_style(theme.header),
@@ -134,9 +137,11 @@ fn render_email(selected_index: usize, area: Rect, buf: &mut Buffer) {
];
Paragraph::new(headers)
.style(theme.body)
.render(area[0], buf);
.render(headers_area, buf);
let body = email.body.lines().map(Line::from).collect_vec();
Paragraph::new(body).style(theme.body).render(area[1], buf);
Paragraph::new(body)
.style(theme.body)
.render(body_area, buf);
} else {
Paragraph::new("No email selected").render(inner, buf);
}

View File

@@ -1,7 +1,7 @@
use itertools::Itertools;
use ratatui::{prelude::*, widgets::*};
use crate::{layout, RgbSwatch, THEME};
use crate::{RgbSwatch, THEME};
#[derive(Debug, Default, Clone, Copy)]
struct Ingredient {
@@ -123,10 +123,13 @@ impl Widget for RecipeTab {
horizontal: 2,
vertical: 1,
});
let area = layout(area, Direction::Horizontal, vec![44, 0]);
let [recipe, ingredients] = area.split(&Layout::horizontal([
Constraint::Length(44),
Constraint::Min(0),
]));
render_recipe(area[0], buf);
render_ingredients(self.selected_row, area[1], buf);
render_recipe(recipe, buf);
render_ingredients(self.selected_row, ingredients, buf);
}
}
@@ -143,13 +146,12 @@ fn render_recipe(area: Rect, buf: &mut Buffer) {
fn render_ingredients(selected_row: usize, area: Rect, buf: &mut Buffer) {
let mut state = TableState::default().with_selected(Some(selected_row));
let rows = INGREDIENTS.iter().map(|&i| i.into()).collect_vec();
let rows = INGREDIENTS.iter().cloned();
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

@@ -4,7 +4,7 @@ use ratatui::{
widgets::{canvas::*, *},
};
use crate::{layout, RgbSwatch, THEME};
use crate::{RgbSwatch, THEME};
#[derive(Debug)]
pub struct TracerouteTab {
@@ -28,14 +28,14 @@ impl Widget for TracerouteTab {
});
Clear.render(area, buf);
Block::new().style(THEME.content).render(area, buf);
let area = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)])
.split(area);
let left_area = layout(area[0], Direction::Vertical, vec![0, 3]);
render_hops(self.selected_row, left_area[0], buf);
render_ping(self.selected_row, left_area[1], buf);
render_map(self.selected_row, area[1], buf);
let horizontal = Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]);
let vertical = Layout::vertical([Constraint::Min(0), Constraint::Length(3)]);
let [left, map] = area.split(&horizontal);
let [hops, pings] = left.split(&vertical);
render_hops(self.selected_row, hops, buf);
render_ping(self.selected_row, pings, buf);
render_map(self.selected_row, map, buf);
}
}
@@ -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

@@ -6,7 +6,7 @@ use ratatui::{
};
use time::OffsetDateTime;
use crate::{color_from_oklab, layout, RgbSwatch, THEME};
use crate::{color_from_oklab, RgbSwatch, THEME};
pub struct WeatherTab {
pub selected_row: usize,
@@ -32,14 +32,24 @@ impl Widget for WeatherTab {
horizontal: 2,
vertical: 1,
});
let area = layout(area, Direction::Vertical, vec![0, 1, 1]);
render_gauges(self.selected_row, area[2], buf);
let [main, _, gauges] = area.split(&Layout::vertical([
Constraint::Min(0),
Constraint::Length(1),
Constraint::Length(1),
]));
let [calendar, charts] = main.split(&Layout::horizontal([
Constraint::Length(23),
Constraint::Min(0),
]));
let [simple, horizontal] = charts.split(&Layout::vertical([
Constraint::Length(29),
Constraint::Min(0),
]));
let area = layout(area[0], Direction::Horizontal, vec![23, 0]);
render_calendar(area[0], buf);
let area = layout(area[1], Direction::Horizontal, vec![29, 0]);
render_simple_barchart(area[0], buf);
render_horizontal_barchart(area[1], buf);
render_calendar(calendar, buf);
render_simple_barchart(simple, buf);
render_horizontal_barchart(horizontal, buf);
render_gauge(self.selected_row, gauges, buf);
}
}
@@ -114,7 +124,7 @@ fn render_horizontal_barchart(area: Rect, buf: &mut Buffer) {
.render(area, buf);
}
pub fn render_gauges(progress: usize, area: Rect, buf: &mut Buffer) {
pub fn render_gauge(progress: usize, area: Rect, buf: &mut Buffer) {
let percent = (progress * 3).min(100) as f64;
render_line_gauge(percent, area, buf);

View File

@@ -53,48 +53,36 @@ fn handle_events() -> io::Result<bool> {
}
fn layout(frame: &mut Frame) {
let main_layout = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1),
Constraint::Min(0),
Constraint::Length(1),
])
.split(frame.size());
let vertical = Layout::vertical([
Constraint::Length(1),
Constraint::Min(0),
Constraint::Length(1),
]);
let horizontal = Layout::horizontal([Constraint::Ratio(1, 2); 2]);
let [title_bar, main_area, status_bar] = frame.size().split(&vertical);
let [left, right] = main_area.split(&horizontal);
frame.render_widget(
Block::new().borders(Borders::TOP).title("Title Bar"),
main_layout[0],
title_bar,
);
frame.render_widget(
Block::new().borders(Borders::TOP).title("Status Bar"),
main_layout[2],
);
let inner_layout = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(main_layout[1]);
frame.render_widget(
Block::default().borders(Borders::ALL).title("Left"),
inner_layout[0],
);
frame.render_widget(
Block::default().borders(Borders::ALL).title("Right"),
inner_layout[1],
status_bar,
);
frame.render_widget(Block::default().borders(Borders::ALL).title("Left"), left);
frame.render_widget(Block::default().borders(Borders::ALL).title("Right"), right);
}
fn styling(frame: &mut Frame) {
let areas = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
Constraint::Min(0),
])
.split(frame.size());
let areas = Layout::vertical([
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
Constraint::Min(0),
])
.split(frame.size());
let span1 = Span::raw("Hello ");
let span2 = Span::styled(

View File

@@ -1,6 +1,5 @@
use std::{
io::{self, stdout, Stdout},
rc::Rc,
time::Duration,
};
@@ -75,7 +74,7 @@ impl App {
fn draw(&mut self) -> Result<()> {
self.term.draw(|frame| {
let state = self.state;
let layout = Self::equal_layout(frame);
let layout = Layout::vertical([Constraint::Ratio(1, 4); 4]).split(frame.size());
Self::render_gauge1(state.progress1, frame, layout[0]);
Self::render_gauge2(state.progress2, frame, layout[1]);
Self::render_gauge3(state.progress3, frame, layout[2]);
@@ -97,18 +96,6 @@ impl App {
Ok(())
}
fn equal_layout(frame: &Frame) -> Rc<[Rect]> {
Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
])
.split(frame.size())
}
fn render_gauge1(progress: u16, frame: &mut Frame, area: Rect) {
let title = Self::title_block("Gauge with percentage progress");
let gauge = Gauge::default()

View File

@@ -215,15 +215,15 @@ fn run_app<B: Backend>(
}
fn ui(f: &mut Frame, downloads: &Downloads) {
let size = f.size();
let area = f.size();
let block = Block::default().title(block::Title::from("Progress").alignment(Alignment::Center));
f.render_widget(block, size);
f.render_widget(block, area);
let chunks = Layout::default()
.constraints([Constraint::Length(2), Constraint::Length(4)])
.margin(1)
.split(size);
let vertical = Layout::vertical([Constraint::Length(2), Constraint::Length(4)]).margin(1);
let horizontal = Layout::horizontal([Constraint::Percentage(20), Constraint::Percentage(80)]);
let [progress_area, main] = area.split(&vertical);
let [list_area, gauge_area] = main.split(&horizontal);
// total progress
let done = NUM_DOWNLOADS - downloads.pending.len() - downloads.in_progress.len();
@@ -231,12 +231,7 @@ fn ui(f: &mut Frame, downloads: &Downloads) {
.gauge_style(Style::default().fg(Color::Blue))
.label(format!("{done}/{NUM_DOWNLOADS}"))
.ratio(done as f64 / NUM_DOWNLOADS as f64);
f.render_widget(progress, chunks[0]);
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(20), Constraint::Percentage(80)])
.split(chunks[1]);
f.render_widget(progress, progress_area);
// in progress downloads
let items: Vec<ListItem> = downloads
@@ -259,21 +254,21 @@ fn ui(f: &mut Frame, downloads: &Downloads) {
})
.collect();
let list = List::new(items);
f.render_widget(list, chunks[0]);
f.render_widget(list, list_area);
for (i, (_, download)) in downloads.in_progress.iter().enumerate() {
let gauge = Gauge::default()
.gauge_style(Style::default().fg(Color::Yellow))
.ratio(download.progress / 100.0);
if chunks[1].top().saturating_add(i as u16) > size.bottom() {
if gauge_area.top().saturating_add(i as u16) > area.bottom() {
continue;
}
f.render_widget(
gauge,
Rect {
x: chunks[1].left(),
y: chunks[1].top().saturating_add(i as u16),
width: chunks[1].width,
x: gauge_area.left(),
y: gauge_area.top().saturating_add(i as u16),
width: gauge_area.width,
height: 1,
},
);

View File

@@ -48,14 +48,12 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
}
fn ui(frame: &mut Frame) {
let main_layout = Layout::default()
.direction(Direction::Vertical)
.constraints([
Length(4), // text
Length(50), // examples
Min(0), // fills remaining space
])
.split(frame.size());
let vertical = Layout::vertical([
Length(4), // text
Length(50), // examples
Min(0), // fills remaining space
]);
let [text_area, examples_area, _] = frame.size().split(&vertical);
// title
frame.render_widget(
@@ -66,38 +64,34 @@ fn ui(frame: &mut Frame) {
Line::from("E.g. the second line of the Len/Min box is [Length(2), Min(2), Min(0)]"),
Line::from("Note: constraint labels that don't fit are truncated"),
]),
main_layout[0],
text_area,
);
let example_rows = Layout::default()
.direction(Direction::Vertical)
.constraints([
Length(9),
Length(9),
Length(9),
Length(9),
Length(9),
Min(0), // fills remaining space
])
.split(main_layout[1]);
let example_rows = Layout::vertical([
Length(9),
Length(9),
Length(9),
Length(9),
Length(9),
Min(0), // fills remaining space
])
.split(examples_area);
let example_areas = example_rows
.iter()
.flat_map(|area| {
Layout::default()
.direction(Direction::Horizontal)
.constraints([
Length(14),
Length(14),
Length(14),
Length(14),
Length(14),
Min(0), // fills remaining space
])
.split(*area)
.iter()
.copied()
.take(5) // ignore Min(0)
.collect_vec()
Layout::horizontal([
Constraint::Length(14),
Constraint::Length(14),
Constraint::Length(14),
Constraint::Length(14),
Constraint::Length(14),
Constraint::Min(0), // fills remaining space
])
.split(*area)
.iter()
.copied()
.take(5) // ignore Min(0)
.collect_vec()
})
.collect_vec();
@@ -182,10 +176,7 @@ fn render_example_combination(
.border_style(Style::default().fg(Color::DarkGray));
let inner = block.inner(area);
frame.render_widget(block, area);
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![Length(1); constraints.len() + 1])
.split(inner);
let layout = Layout::vertical(vec![Length(1); constraints.len() + 1]).split(inner);
for (i, (a, b)) in constraints.iter().enumerate() {
render_single_example(frame, layout[i], vec![*a, *b, Min(0)]);
}
@@ -199,13 +190,11 @@ fn render_single_example(frame: &mut Frame, area: Rect, constraints: Vec<Constra
let red = Paragraph::new(constraint_label(constraints[0])).on_red();
let blue = Paragraph::new(constraint_label(constraints[1])).on_blue();
let green = Paragraph::new("·".repeat(12)).on_green();
let layout = Layout::default()
.direction(Direction::Horizontal)
.constraints(constraints)
.split(area);
frame.render_widget(red, layout[0]);
frame.render_widget(blue, layout[1]);
frame.render_widget(green, layout[2]);
let horizontal = Layout::horizontal(constraints);
let [r, b, g] = area.split(&horizontal);
frame.render_widget(red, r);
frame.render_widget(blue, b);
frame.render_widget(green, g);
}
fn constraint_label(constraint: Constraint) -> String {

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(),
_ => {}
}
}
@@ -198,10 +198,8 @@ fn run_app<B: Backend>(
fn ui(f: &mut Frame, app: &mut App) {
// Create two chunks with equal horizontal screen space
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(f.size());
let horizontal = Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]);
let [item_list_area, event_list_area] = f.size().split(&horizontal);
// Iterate through all elements in the `items` app and append some debug text to it.
let items: Vec<ListItem> = app
@@ -232,7 +230,7 @@ fn ui(f: &mut Frame, app: &mut App) {
.highlight_symbol(">> ");
// We can now render the item list
f.render_stateful_widget(items, chunks[0], &mut app.items.state);
f.render_stateful_widget(items, item_list_area, &mut app.items.state);
// Let's do the same for the events.
// The event list doesn't have any state and only displays the current state of the list.
@@ -264,7 +262,7 @@ fn ui(f: &mut Frame, app: &mut App) {
// 3. Add a spacer line
// 4. Add the actual event
ListItem::new(vec![
Line::from("-".repeat(chunks[1].width as usize)),
Line::from("-".repeat(event_list_area.width as usize)),
header,
Line::from(""),
log,
@@ -273,6 +271,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);
f.render_widget(events_list, chunks[1]);
.direction(ListDirection::BottomToTop);
f.render_widget(events_list, event_list_area);
}

View File

@@ -44,24 +44,18 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
}
fn ui(frame: &mut Frame) {
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(1), Constraint::Min(0)])
.split(frame.size());
let vertical = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
let [text_area, main_area] = frame.size().split(&vertical);
frame.render_widget(
Paragraph::new("Note: not all terminals support all modifiers")
.style(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
layout[0],
text_area,
);
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(1); 50])
.split(layout[1])
let layout = Layout::vertical([Constraint::Length(1); 50])
.split(main_area)
.iter()
.flat_map(|area| {
Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(20); 5])
Layout::horizontal([Constraint::Percentage(20); 5])
.split(*area)
.to_vec()
})

View File

@@ -90,15 +90,7 @@ fn ui(f: &mut Frame, app: &App) {
let block = Block::default().black();
f.render_widget(block, size);
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
])
.split(size);
let layout = Layout::vertical([Constraint::Ratio(1, 4); 4]).split(size);
let text = vec![
Line::from("This is a line "),
@@ -129,20 +121,20 @@ fn ui(f: &mut Frame, app: &App) {
let paragraph = Paragraph::new(text.clone())
.style(Style::default().fg(Color::Gray))
.block(create_block("Default alignment (Left), no wrap"));
f.render_widget(paragraph, chunks[0]);
f.render_widget(paragraph, layout[0]);
let paragraph = Paragraph::new(text.clone())
.style(Style::default().fg(Color::Gray))
.block(create_block("Default alignment (Left), with wrap"))
.wrap(Wrap { trim: true });
f.render_widget(paragraph, chunks[1]);
f.render_widget(paragraph, layout[1]);
let paragraph = Paragraph::new(text.clone())
.style(Style::default().fg(Color::Gray))
.block(create_block("Right alignment, with wrap"))
.alignment(Alignment::Right)
.wrap(Wrap { trim: true });
f.render_widget(paragraph, chunks[2]);
f.render_widget(paragraph, layout[2]);
let paragraph = Paragraph::new(text)
.style(Style::default().fg(Color::Gray))
@@ -150,5 +142,5 @@ fn ui(f: &mut Frame, app: &App) {
.alignment(Alignment::Center)
.wrap(Wrap { trim: true })
.scroll((app.scroll, 0));
f.render_widget(paragraph, chunks[3]);
f.render_widget(paragraph, layout[3]);
}

View File

@@ -62,11 +62,10 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
}
fn ui(f: &mut Frame, app: &App) {
let size = f.size();
let area = f.size();
let chunks = Layout::default()
.constraints([Constraint::Percentage(20), Constraint::Percentage(80)])
.split(size);
let vertical = Layout::vertical([Constraint::Percentage(20), Constraint::Percentage(80)]);
let [instructions, content] = area.split(&vertical);
let text = if app.show_popup {
"Press p to close the popup"
@@ -76,17 +75,17 @@ fn ui(f: &mut Frame, app: &App) {
let paragraph = Paragraph::new(text.slow_blink())
.alignment(Alignment::Center)
.wrap(Wrap { trim: true });
f.render_widget(paragraph, chunks[0]);
f.render_widget(paragraph, instructions);
let block = Block::default()
.title("Content")
.borders(Borders::ALL)
.on_blue();
f.render_widget(block, chunks[1]);
f.render_widget(block, content);
if app.show_popup {
let block = Block::default().title("Popup").borders(Borders::ALL);
let area = centered_rect(60, 20, size);
let area = centered_rect(60, 20, area);
f.render_widget(Clear, area); //this clears out the background
f.render_widget(block, area);
}
@@ -94,21 +93,17 @@ fn ui(f: &mut Frame, app: &App) {
/// helper function to create a centered rect using up certain percentage of the available rect `r`
fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
let popup_layout = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Percentage((100 - percent_y) / 2),
Constraint::Percentage(percent_y),
Constraint::Percentage((100 - percent_y) / 2),
])
.split(r);
let popup_layout = Layout::vertical([
Constraint::Percentage((100 - percent_y) / 2),
Constraint::Percentage(percent_y),
Constraint::Percentage((100 - percent_y) / 2),
])
.split(r);
Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage((100 - percent_x) / 2),
Constraint::Percentage(percent_x),
Constraint::Percentage((100 - percent_x) / 2),
])
.split(popup_layout[1])[1]
Layout::horizontal([
Constraint::Percentage((100 - percent_x) / 2),
Constraint::Percentage(percent_x),
Constraint::Percentage((100 - percent_x) / 2),
])
.split(popup_layout[1])[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

@@ -100,19 +100,14 @@ fn ui(f: &mut Frame, app: &mut App) {
let mut long_line = s.repeat(usize::from(size.width) / s.len() + 4);
long_line.push('\n');
let block = Block::default().black();
f.render_widget(block, size);
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Min(1),
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
])
.split(size);
let chunks = Layout::vertical([
Constraint::Min(1),
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
])
.split(size);
let text = vec![
Line::from("This is a line "),
@@ -145,18 +140,10 @@ fn ui(f: &mut Frame, app: &mut App) {
app.vertical_scroll_state = app.vertical_scroll_state.content_length(text.len());
app.horizontal_scroll_state = app.horizontal_scroll_state.content_length(long_line.len());
let create_block = |title| {
Block::default()
.borders(Borders::ALL)
.gray()
.title(Span::styled(
title,
Style::default().add_modifier(Modifier::BOLD),
))
};
let create_block = |title: &'static str| Block::bordered().gray().title(title.bold());
let title = Block::default()
.title("Use h j k l to scroll ◄ ▲ ▼ ►")
.title("Use h j k l or ◄ ▲ ▼ ► to scroll ".bold())
.title_alignment(Alignment::Center);
f.render_widget(title, chunks[0]);

View File

@@ -125,14 +125,12 @@ fn run_app<B: Backend>(
}
fn ui(f: &mut Frame, app: &App) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3),
Constraint::Length(3),
Constraint::Min(0),
])
.split(f.size());
let chunks = Layout::vertical([
Constraint::Length(3),
Constraint::Length(3),
Constraint::Min(0),
])
.split(f.size());
let sparkline = Sparkline::default()
.block(
Block::default()

View File

@@ -1,42 +1,23 @@
use std::{error::Error, io};
use std::{error::Error, io, str::FromStr};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use itertools::Itertools;
use ratatui::{prelude::*, widgets::*};
struct App<'a> {
struct App {
state: TableState,
items: Vec<Vec<&'a str>>,
items: Vec<Vec<String>>,
}
impl<'a> App<'a> {
fn new() -> App<'a> {
impl App {
fn new() -> App {
App {
state: TableState::default(),
items: vec![
vec!["Row11", "Row12", "Row13"],
vec!["Row21", "Row22", "Row23"],
vec!["Row31", "Row32", "Row33"],
vec!["Row41", "Row42", "Row43"],
vec!["Row51", "Row52", "Row53"],
vec!["Row61", "Row62\nTest", "Row63"],
vec!["Row71", "Row72", "Row73"],
vec!["Row81", "Row82", "Row83"],
vec!["Row91", "Row92", "Row93"],
vec!["Row101", "Row102", "Row103"],
vec!["Row111", "Row112", "Row113"],
vec!["Row121", "Row122", "Row123"],
vec!["Row131", "Row132", "Row133"],
vec!["Row141", "Row142", "Row143"],
vec!["Row151", "Row152", "Row153"],
vec!["Row161", "Row162", "Row163"],
vec!["Row171", "Row172", "Row173"],
vec!["Row181", "Row182", "Row183"],
vec!["Row191", "Row192", "Row193"],
],
state: TableState::default().with_selected(0),
items: generate_fake_names(),
}
}
pub fn next(&mut self) {
@@ -68,6 +49,26 @@ impl<'a> App<'a> {
}
}
fn generate_fake_names() -> Vec<Vec<String>> {
use fakeit::{address, contact, name};
(0..20)
.map(|_| {
let name = name::full();
let address = format!(
"{}\n{}, {} {}",
address::street(),
address::city(),
address::state(),
address::zip()
);
let email = contact::email();
vec![name, address, email]
})
.sorted_by(|a, b| a[0].cmp(&b[0]))
.collect_vec()
}
fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
enable_raw_mode()?;
@@ -104,8 +105,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(),
_ => {}
}
}
@@ -114,38 +115,52 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
}
fn ui(f: &mut Frame, app: &mut App) {
let rects = Layout::default()
.constraints([Constraint::Percentage(100)])
.split(f.size());
let rects = Layout::vertical([Constraint::Percentage(100)]).split(f.size());
// colors from https://tailwindcss.com/docs/customizing-colors
let header_bg = Color::from_str("#1e3a8a").unwrap();
let header_fg = Color::from_str("#eff6ff").unwrap();
let header_style = Style::default().fg(header_fg).bg(header_bg);
let normal_row_color = Color::from_str("#1e293b").unwrap();
let alt_row_color = Color::from_str("#0f172a").unwrap();
let selected_style = Style::default().add_modifier(Modifier::REVERSED);
let normal_style = Style::default().bg(Color::Blue);
let header_cells = ["Header1", "Header2", "Header3"]
let header = ["Name", "Address", "Email"]
.iter()
.map(|h| Cell::from(*h).style(Style::default().fg(Color::Red)));
let header = Row::new(header_cells)
.style(normal_style)
.height(1)
.bottom_margin(1);
let rows = app.items.iter().map(|item| {
let height = item
.iter()
.map(|content| content.chars().filter(|c| *c == '\n').count())
.max()
.unwrap_or(0)
+ 1;
let cells = item.iter().map(|c| Cell::from(*c));
Row::new(cells).height(height as u16).bottom_margin(1)
.cloned()
.map(Cell::from)
.collect::<Row>()
.style(header_style)
.height(1);
let rows = app.items.iter().enumerate().map(|(i, item)| {
let color = match i % 2 {
0 => normal_row_color,
_ => alt_row_color,
};
item.iter()
.cloned()
.map(|content| Cell::from(Text::from(format!("\n{}\n", content))))
.collect::<Row>()
.style(Style::new().bg(color))
.height(4)
});
let t = Table::new(rows)
.header(header)
.block(Block::default().borders(Borders::ALL).title("Table"))
.highlight_style(selected_style)
.highlight_symbol(">> ")
.widths([
Constraint::Percentage(50),
Constraint::Max(30),
Constraint::Min(10),
]);
let bar = "";
let t = Table::new(
rows,
[
Constraint::Length(15),
Constraint::Min(30),
Constraint::Min(25),
],
)
.header(header)
.highlight_style(selected_style)
.highlight_symbol(Text::from(vec![
"".into(),
bar.into(),
bar.into(),
"".into(),
]))
.highlight_spacing(HighlightSpacing::Always);
f.render_stateful_widget(t, rects[0], &mut app.state);
}

View File

@@ -3,14 +3,17 @@
Output "target/table.gif"
Set Theme "Aardvark Blue"
Set Width 1200
Set Height 600
Set Height 800
Hide
Type "cargo run --example=table --features=crossterm"
Enter
Sleep 1s
Show
Down@1s 4
Up@1s 2
Down@1s 8
Up@1s 12
Sleep 5s
Sleep 2s
Set TypingSpeed 0.5s
Down 2
Up 2
Down 8
Up 12
Down 4
Sleep 2s

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(),
_ => {}
}
}
@@ -79,28 +79,25 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
}
fn ui(f: &mut Frame, app: &App) {
let size = f.size();
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(3), Constraint::Min(0)])
.split(size);
let area = f.size();
let vertical = Layout::vertical([Constraint::Length(3), Constraint::Min(0)]);
let [tabs_area, inner_area] = area.split(&vertical);
let block = Block::default().on_white().black();
f.render_widget(block, size);
let titles = app
f.render_widget(block, area);
let tabs = app
.titles
.iter()
.map(|t| {
let (first, rest) = t.split_at(1);
Line::from(vec![first.yellow(), rest.green()])
})
.collect();
let tabs = Tabs::new(titles)
.collect::<Tabs>()
.block(Block::default().borders(Borders::ALL).title("Tabs"))
.select(app.index)
.style(Style::default().cyan().on_gray())
.highlight_style(Style::default().bold().on_black());
f.render_widget(tabs, chunks[0]);
f.render_widget(tabs, tabs_area);
let inner = match app.index {
0 => Block::default().title("Inner 0").borders(Borders::ALL),
1 => Block::default().title("Inner 1").borders(Borders::ALL),
@@ -108,5 +105,5 @@ fn ui(f: &mut Frame, app: &App) {
3 => Block::default().title("Inner 3").borders(Borders::ALL),
_ => unreachable!(),
};
f.render_widget(inner, chunks[1]);
f.render_widget(inner, inner_area);
}

View File

@@ -172,14 +172,12 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
}
fn ui(f: &mut Frame, app: &App) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1),
Constraint::Length(3),
Constraint::Min(1),
])
.split(f.size());
let vertical = Layout::vertical([
Constraint::Length(1),
Constraint::Length(3),
Constraint::Min(1),
]);
let [help_area, input_area, messages_area] = f.size().split(&vertical);
let (msg, style) = match app.input_mode {
InputMode::Normal => (
@@ -203,10 +201,9 @@ fn ui(f: &mut Frame, app: &App) {
Style::default(),
),
};
let mut text = Text::from(Line::from(msg));
text.patch_style(style);
let text = Text::from(Line::from(msg)).patch_style(style);
let help_message = Paragraph::new(text);
f.render_widget(help_message, chunks[0]);
f.render_widget(help_message, help_area);
let input = Paragraph::new(app.input.as_str())
.style(match app.input_mode {
@@ -214,7 +211,7 @@ fn ui(f: &mut Frame, app: &App) {
InputMode::Editing => Style::default().fg(Color::Yellow),
})
.block(Block::default().borders(Borders::ALL).title("Input"));
f.render_widget(input, chunks[1]);
f.render_widget(input, input_area);
match app.input_mode {
InputMode::Normal =>
// Hide the cursor. `Frame` does this by default, so we don't need to do anything here
@@ -226,9 +223,9 @@ fn ui(f: &mut Frame, app: &App) {
f.set_cursor(
// Draw the cursor at the current position in the input field.
// This position is can be controlled via the left and right arrow key
chunks[1].x + app.cursor_position as u16 + 1,
input_area.x + app.cursor_position as u16 + 1,
// Move one line down, from the border to the input line
chunks[1].y + 1,
input_area.y + 1,
)
}
}
@@ -244,5 +241,5 @@ fn ui(f: &mut Frame, app: &App) {
.collect();
let messages =
List::new(messages).block(Block::default().borders(Borders::ALL).title("Messages"));
f.render_widget(messages, chunks[2]);
f.render_widget(messages, messages_area);
}

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

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

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

31
src/layout/alignment.rs Normal file
View File

@@ -0,0 +1,31 @@
use strum::{Display, EnumString};
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
pub enum Alignment {
#[default]
Left,
Center,
Right,
}
#[cfg(test)]
mod tests {
use strum::ParseError;
use super::*;
#[test]
fn alignment_to_string() {
assert_eq!(Alignment::Left.to_string(), "Left");
assert_eq!(Alignment::Center.to_string(), "Center");
assert_eq!(Alignment::Right.to_string(), "Right");
}
#[test]
fn alignment_from_str() {
assert_eq!("Left".parse::<Alignment>(), Ok(Alignment::Left));
assert_eq!("Center".parse::<Alignment>(), Ok(Alignment::Center));
assert_eq!("Right".parse::<Alignment>(), Ok(Alignment::Right));
assert_eq!("".parse::<Alignment>(), Err(ParseError::VariantNotFound));
}
}

362
src/layout/constraint.rs Normal file
View File

@@ -0,0 +1,362 @@
use std::fmt::{self, Display};
use itertools::Itertools;
/// A constraint that can be applied to a layout
///
/// Constraints are used to define the size of a layout. They can be used to define a fixed size, a
/// percentage of the available space, a ratio of the available space, or a minimum or maximum size.
///
/// Relative constraints (percentage, ratio) are calculated relative to the entire space being
/// split, not the space available after applying the more fixed constraints (min, max, length).
///
/// # Examples
///
/// `Constraint` has some helper methods to create lists of constraints from anything that can be
/// converted into an iterator of u16s ((u16, u16) for ratios).
///
/// ```rust
/// # use ratatui::prelude::*;
/// // 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 layout specifying maximum sizes of the columns
/// let constraints = Constraint::from_maxes([30, 170]);
/// ```
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub enum Constraint {
/// Apply a percentage to a given amount
///
/// Converts the given percentage to a f32, and then converts it back, trimming off the decimal
/// point (effectively rounding down)
/// ```
/// # use ratatui::prelude::*;
/// assert_eq!(0, Constraint::Percentage(50).apply(0));
/// assert_eq!(2, Constraint::Percentage(50).apply(4));
/// assert_eq!(5, Constraint::Percentage(50).apply(10));
/// assert_eq!(5, Constraint::Percentage(50).apply(11));
/// ```
Percentage(u16),
/// Apply a ratio
///
/// Converts the given numbers to a f32, and then converts it back, trimming off the decimal
/// point (effectively rounding down)
/// ```
/// # use ratatui::prelude::*;
/// assert_eq!(0, Constraint::Ratio(4, 3).apply(0));
/// assert_eq!(4, Constraint::Ratio(4, 3).apply(4));
/// assert_eq!(10, Constraint::Ratio(4, 3).apply(10));
/// assert_eq!(100, Constraint::Ratio(4, 3).apply(100));
///
/// assert_eq!(0, Constraint::Ratio(3, 4).apply(0));
/// assert_eq!(3, Constraint::Ratio(3, 4).apply(4));
/// assert_eq!(7, Constraint::Ratio(3, 4).apply(10));
/// assert_eq!(75, Constraint::Ratio(3, 4).apply(100));
/// ```
Ratio(u32, u32),
/// Apply no more than the given amount (currently roughly equal to [Constraint::Max], but less
/// consistent)
/// ```
/// # use ratatui::prelude::*;
/// assert_eq!(0, Constraint::Length(4).apply(0));
/// assert_eq!(4, Constraint::Length(4).apply(4));
/// assert_eq!(4, Constraint::Length(4).apply(10));
/// ```
Length(u16),
/// Apply at most the given amount
///
/// also see [std::cmp::min]
/// ```
/// # use ratatui::prelude::*;
/// assert_eq!(0, Constraint::Max(4).apply(0));
/// assert_eq!(4, Constraint::Max(4).apply(4));
/// assert_eq!(4, Constraint::Max(4).apply(10));
/// ```
Max(u16),
/// Apply at least the given amount
///
/// also see [std::cmp::max]
/// ```
/// # use ratatui::prelude::*;
/// assert_eq!(4, Constraint::Min(4).apply(0));
/// assert_eq!(4, Constraint::Min(4).apply(4));
/// assert_eq!(10, Constraint::Min(4).apply(10));
/// ```
Min(u16),
}
impl Constraint {
pub fn apply(&self, length: u16) -> u16 {
match *self {
Constraint::Percentage(p) => {
let p = p as f32 / 100.0;
let length = length as f32;
(p * length).min(length) as u16
}
Constraint::Ratio(numerator, denominator) => {
// avoid division by zero by using 1 when denominator is 0
// this results in 0/0 -> 0 and x/0 -> x for x != 0
let percentage = numerator as f32 / denominator.max(1) as f32;
let length = length as f32;
(percentage * length).min(length) as u16
}
Constraint::Length(l) => length.min(l),
Constraint::Max(m) => length.min(m),
Constraint::Min(m) => length.max(m),
}
}
/// Convert an iterator of lengths into a vector of constraints
///
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// # let area = Rect::default();
/// let constraints = Constraint::from_lengths([1, 2, 3]);
/// let layout = Layout::default().constraints(constraints).split(area);
/// ```
pub fn from_lengths<T>(lengths: T) -> Vec<Constraint>
where
T: IntoIterator<Item = u16>,
{
lengths.into_iter().map(Constraint::Length).collect_vec()
}
/// Convert an iterator of ratios into a vector of constraints
///
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// # let area = Rect::default();
/// let constraints = Constraint::from_ratios([(1, 4), (1, 2), (1, 4)]);
/// let layout = Layout::default().constraints(constraints).split(area);
/// ```
pub fn from_ratios<T>(ratios: T) -> Vec<Constraint>
where
T: IntoIterator<Item = (u32, u32)>,
{
ratios
.into_iter()
.map(|(n, d)| Constraint::Ratio(n, d))
.collect_vec()
}
/// Convert an iterator of percentages into a vector of constraints
///
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// # let area = Rect::default();
/// let constraints = Constraint::from_percentages([25, 50, 25]);
/// let layout = Layout::default().constraints(constraints).split(area);
/// ```
pub fn from_percentages<T>(percentages: T) -> Vec<Constraint>
where
T: IntoIterator<Item = u16>,
{
percentages
.into_iter()
.map(Constraint::Percentage)
.collect_vec()
}
/// Convert an iterator of maxes into a vector of constraints
///
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// # let area = Rect::default();
/// let constraints = Constraint::from_maxes([1, 2, 3]);
/// let layout = Layout::default().constraints(constraints).split(area);
/// ```
pub fn from_maxes<T>(maxes: T) -> Vec<Constraint>
where
T: IntoIterator<Item = u16>,
{
maxes.into_iter().map(Constraint::Max).collect_vec()
}
/// Convert an iterator of mins into a vector of constraints
///
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// # let area = Rect::default();
/// let constraints = Constraint::from_mins([1, 2, 3]);
/// let layout = Layout::default().constraints(constraints).split(area);
/// ```
pub fn from_mins<T>(mins: T) -> Vec<Constraint>
where
T: IntoIterator<Item = u16>,
{
mins.into_iter().map(Constraint::Min).collect_vec()
}
}
impl From<u16> for Constraint {
/// Convert a u16 into a [Constraint::Length]
///
/// This is useful when you want to specify a fixed size for a layout, but don't want to
/// explicitly create a [Constraint::Length] yourself.
///
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// # let area = Rect::default();
/// let layout = Layout::new(Direction::Vertical, [1, 2, 3]).split(area);
/// let layout = Layout::horizontal([1, 2, 3]).split(area);
/// let layout = Layout::vertical([1, 2, 3]).split(area);
/// ````
fn from(length: u16) -> Constraint {
Constraint::Length(length)
}
}
impl From<&Constraint> for Constraint {
fn from(constraint: &Constraint) -> Self {
*constraint
}
}
impl AsRef<Constraint> for Constraint {
fn as_ref(&self) -> &Constraint {
self
}
}
impl Default for Constraint {
fn default() -> Self {
Constraint::Percentage(100)
}
}
impl Display for Constraint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Constraint::Percentage(p) => write!(f, "Percentage({})", p),
Constraint::Ratio(n, d) => write!(f, "Ratio({}, {})", n, d),
Constraint::Length(l) => write!(f, "Length({})", l),
Constraint::Max(m) => write!(f, "Max({})", m),
Constraint::Min(m) => write!(f, "Min({})", m),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default() {
assert_eq!(Constraint::default(), Constraint::Percentage(100));
}
#[test]
fn to_string() {
assert_eq!(Constraint::Percentage(50).to_string(), "Percentage(50)");
assert_eq!(Constraint::Ratio(1, 2).to_string(), "Ratio(1, 2)");
assert_eq!(Constraint::Length(10).to_string(), "Length(10)");
assert_eq!(Constraint::Max(10).to_string(), "Max(10)");
assert_eq!(Constraint::Min(10).to_string(), "Min(10)");
}
#[test]
fn from_lengths() {
let expected = [
Constraint::Length(1),
Constraint::Length(2),
Constraint::Length(3),
];
assert_eq!(Constraint::from_lengths([1, 2, 3]), expected);
assert_eq!(Constraint::from_lengths(vec![1, 2, 3]), expected);
}
#[test]
fn from_ratios() {
let expected = [
Constraint::Ratio(1, 4),
Constraint::Ratio(1, 2),
Constraint::Ratio(1, 4),
];
assert_eq!(Constraint::from_ratios([(1, 4), (1, 2), (1, 4)]), expected);
assert_eq!(
Constraint::from_ratios(vec![(1, 4), (1, 2), (1, 4)]),
expected
);
}
#[test]
fn from_percentages() {
let expected = [
Constraint::Percentage(25),
Constraint::Percentage(50),
Constraint::Percentage(25),
];
assert_eq!(Constraint::from_percentages([25, 50, 25]), expected);
assert_eq!(Constraint::from_percentages(vec![25, 50, 25]), expected);
}
#[test]
fn from_maxes() {
let expected = [Constraint::Max(1), Constraint::Max(2), Constraint::Max(3)];
assert_eq!(Constraint::from_maxes([1, 2, 3]), expected);
assert_eq!(Constraint::from_maxes(vec![1, 2, 3]), expected);
}
#[test]
fn from_mins() {
let expected = [Constraint::Min(1), Constraint::Min(2), Constraint::Min(3)];
assert_eq!(Constraint::from_mins([1, 2, 3]), expected);
assert_eq!(Constraint::from_mins(vec![1, 2, 3]), expected);
}
#[test]
fn apply() {
assert_eq!(Constraint::Percentage(0).apply(100), 0);
assert_eq!(Constraint::Percentage(50).apply(100), 50);
assert_eq!(Constraint::Percentage(100).apply(100), 100);
assert_eq!(Constraint::Percentage(200).apply(100), 100);
assert_eq!(Constraint::Percentage(u16::MAX).apply(100), 100);
// 0/0 intentionally avoids a panic by returning 0.
assert_eq!(Constraint::Ratio(0, 0).apply(100), 0);
// 1/0 intentionally avoids a panic by returning 100% of the length.
assert_eq!(Constraint::Ratio(1, 0).apply(100), 100);
assert_eq!(Constraint::Ratio(0, 1).apply(100), 0);
assert_eq!(Constraint::Ratio(1, 2).apply(100), 50);
assert_eq!(Constraint::Ratio(2, 2).apply(100), 100);
assert_eq!(Constraint::Ratio(3, 2).apply(100), 100);
assert_eq!(Constraint::Ratio(u32::MAX, 2).apply(100), 100);
assert_eq!(Constraint::Length(0).apply(100), 0);
assert_eq!(Constraint::Length(50).apply(100), 50);
assert_eq!(Constraint::Length(100).apply(100), 100);
assert_eq!(Constraint::Length(200).apply(100), 100);
assert_eq!(Constraint::Length(u16::MAX).apply(100), 100);
assert_eq!(Constraint::Max(0).apply(100), 0);
assert_eq!(Constraint::Max(50).apply(100), 50);
assert_eq!(Constraint::Max(100).apply(100), 100);
assert_eq!(Constraint::Max(200).apply(100), 100);
assert_eq!(Constraint::Max(u16::MAX).apply(100), 100);
assert_eq!(Constraint::Min(0).apply(100), 100);
assert_eq!(Constraint::Min(50).apply(100), 100);
assert_eq!(Constraint::Min(100).apply(100), 100);
assert_eq!(Constraint::Min(200).apply(100), 200);
assert_eq!(Constraint::Min(u16::MAX).apply(100), u16::MAX);
}
}

33
src/layout/corner.rs Normal file
View File

@@ -0,0 +1,33 @@
use strum::{Display, EnumString};
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
pub enum Corner {
#[default]
TopLeft,
TopRight,
BottomRight,
BottomLeft,
}
#[cfg(test)]
mod tests {
use strum::ParseError;
use super::*;
#[test]
fn corner_to_string() {
assert_eq!(Corner::BottomLeft.to_string(), "BottomLeft");
assert_eq!(Corner::BottomRight.to_string(), "BottomRight");
assert_eq!(Corner::TopLeft.to_string(), "TopLeft");
assert_eq!(Corner::TopRight.to_string(), "TopRight");
}
#[test]
fn corner_from_str() {
assert_eq!("BottomLeft".parse::<Corner>(), Ok(Corner::BottomLeft));
assert_eq!("BottomRight".parse::<Corner>(), Ok(Corner::BottomRight));
assert_eq!("TopLeft".parse::<Corner>(), Ok(Corner::TopLeft));
assert_eq!("TopRight".parse::<Corner>(), Ok(Corner::TopRight));
assert_eq!("".parse::<Corner>(), Err(ParseError::VariantNotFound));
}
}

28
src/layout/direction.rs Normal file
View File

@@ -0,0 +1,28 @@
use strum::{Display, EnumString};
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
pub enum Direction {
Horizontal,
#[default]
Vertical,
}
#[cfg(test)]
mod tests {
use strum::ParseError;
use super::*;
#[test]
fn direction_to_string() {
assert_eq!(Direction::Horizontal.to_string(), "Horizontal");
assert_eq!(Direction::Vertical.to_string(), "Vertical");
}
#[test]
fn direction_from_str() {
assert_eq!("Horizontal".parse::<Direction>(), Ok(Direction::Horizontal));
assert_eq!("Vertical".parse::<Direction>(), Ok(Direction::Vertical));
assert_eq!("".parse::<Direction>(), Err(ParseError::VariantNotFound));
}
}

1216
src/layout/layout.rs Normal file

File diff suppressed because it is too large Load Diff

43
src/layout/margin.rs Normal file
View File

@@ -0,0 +1,43 @@
use std::fmt::{self, Display};
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
pub struct Margin {
pub horizontal: u16,
pub vertical: u16,
}
impl Margin {
pub const fn new(horizontal: u16, vertical: u16) -> Margin {
Margin {
horizontal,
vertical,
}
}
}
impl Display for Margin {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}x{}", self.horizontal, self.vertical)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn margin_to_string() {
assert_eq!(Margin::new(1, 2).to_string(), "1x2");
}
#[test]
fn margin_new() {
assert_eq!(
Margin::new(1, 2),
Margin {
horizontal: 1,
vertical: 2
}
);
}
}

View File

@@ -8,6 +8,7 @@ use crate::prelude::*;
mod offset;
use layout::Size;
pub use offset::*;
/// A simple rectangle used in the computation of the layout and to give widgets a hint about the
@@ -25,6 +26,58 @@ pub struct Rect {
pub height: u16,
}
/// Manages row divisions within a `Rect`.
///
/// The `Rows` struct is an iterator that allows iterating through rows of a given `Rect`.
pub struct Rows {
/// The `Rect` associated with the rows.
pub rect: Rect,
/// The y coordinate of the row within the `Rect`.
pub current_row: u16,
}
impl Iterator for Rows {
type Item = Rect;
/// Retrieves the next row within the `Rect`.
///
/// Returns `None` when there are no more rows to iterate through.
fn next(&mut self) -> Option<Self::Item> {
if self.current_row >= self.rect.bottom() {
return None;
}
let row = Rect::new(self.rect.x, self.current_row, self.rect.width, 1);
self.current_row += 1;
Some(row)
}
}
/// Manages column divisions within a `Rect`.
///
/// The `Columns` struct is an iterator that allows iterating through columns of a given `Rect`.
pub struct Columns {
/// The `Rect` associated with the columns.
pub rect: Rect,
/// The x coordinate of the column within the `Rect`.
pub current_column: u16,
}
impl Iterator for Columns {
type Item = Rect;
/// Retrieves the next column within the `Rect`.
///
/// Returns `None` when there are no more columns to iterate through.
fn next(&mut self) -> Option<Self::Item> {
if self.current_column >= self.rect.right() {
return None;
}
let column = Rect::new(self.current_column, self.rect.y, 1, self.rect.height);
self.current_column += 1;
Some(column)
}
}
impl fmt::Display for Rect {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}x{}+{}+{}", self.width, self.height, self.x, self.y)
@@ -155,8 +208,8 @@ impl Rect {
Rect {
x: x1,
y: y1,
width: x2 - x1,
height: y2 - y1,
width: x2.saturating_sub(x1),
height: y2.saturating_sub(y1),
}
}
@@ -167,10 +220,126 @@ impl Rect {
&& self.y < other.bottom()
&& self.bottom() > other.y
}
/// Split the rect into a number of sub-rects according to the given [`Layout`]`.
///
/// An ergonomic wrapper around [`Layout::split`] that returns an array of `Rect`s instead of
/// `Rc<[Rect]>`.
///
/// This method requires the number of constraints to be known at compile time. If you don't
/// know the number of constraints at compile time, use [`Layout::split`] instead.
///
/// # Panics
///
/// Panics if the number of constraints is not equal to the length of the returned array.
///
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// # fn render(frame: &mut Frame) {
/// let area = frame.size();
/// let layout = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
/// let [top, main] = area.split(&layout);
/// // or explicitly specify the number of constraints:
/// let rects = area.split::<2>(&layout);
/// # }
pub fn split<const N: usize>(self, layout: &Layout) -> [Rect; N] {
layout
.split(self)
.to_vec()
.try_into()
.expect("invalid number of rects")
}
/// Clamp this rect to fit inside the other rect.
///
/// If the width or height of this rect is larger than the other rect, it will be clamped to the
/// other rect's width or height.
///
/// If the left or top coordinate of this rect is smaller than the other rect, it will be
/// clamped to the other rect's left or top coordinate.
///
/// If the right or bottom coordinate of this rect is larger than the other rect, it will be
/// clamped to the other rect's right or bottom coordinate.
///
/// This is different from [`Rect::intersection`] because it will move this rect to fit inside
/// the other rect, while [`Rect::intersection`] instead would keep this rect's position and
/// truncate its size to only that which is inside the other rect.
///
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// # fn render(frame: &mut Frame) {
/// let area = frame.size();
/// let rect = Rect::new(0, 0, 100, 100).clamp(area);
/// # }
/// ```
pub fn clamp(self, other: Rect) -> Rect {
let width = self.width.min(other.width);
let height = self.height.min(other.height);
let x = self.x.clamp(other.x, other.right().saturating_sub(width));
let y = self.y.clamp(other.y, other.bottom().saturating_sub(height));
Rect::new(x, y, width, height)
}
/// Creates an iterator over rows within the `Rect`.
///
/// This method returns a `Rows` iterator that allows iterating through rows of the `Rect`.
///
/// # Examples
///
/// ```
/// use ratatui::prelude::*;
/// let area = Rect::new(0, 0, 10, 5);
/// for row in area.rows() {
/// // Perform operations on each row of the area
/// println!("Row: {:?}", row);
/// }
/// ```
pub fn rows(&self) -> Rows {
Rows {
rect: *self,
current_row: self.y,
}
}
/// Creates an iterator over columns within the `Rect`.
///
/// This method returns a `Columns` iterator that allows iterating through columns of the
/// `Rect`.
///
/// # Examples
///
/// ```
/// use ratatui::prelude::*;
/// let area = Rect::new(0, 0, 10, 5);
/// for column in area.columns() {
/// // Perform operations on each column of the area
/// println!("Column: {:?}", column);
/// }
/// ```
pub fn columns(&self) -> Columns {
Columns {
rect: *self,
current_column: self.x,
}
}
/// Converts the rect into a size struct.
pub fn as_size(self) -> Size {
Size {
width: self.width,
height: self.height,
}
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[test]
@@ -280,6 +449,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)));
@@ -345,4 +522,72 @@ mod tests {
const _BOTTOM: u16 = RECT.bottom();
assert!(RECT.intersects(RECT));
}
#[test]
fn split() {
let layout = Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]);
let [a, b] = Rect::new(0, 0, 2, 1).split(&layout);
assert_eq!(a, Rect::new(0, 0, 1, 1));
assert_eq!(b, Rect::new(1, 0, 1, 1));
}
#[test]
#[should_panic(expected = "invalid number of rects")]
fn split_invalid_number_of_recs() {
let layout = Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]);
let [_a, _b, _c] = Rect::new(0, 0, 2, 1).split(&layout);
}
#[rstest]
#[case::inside(Rect::new(20, 20, 10, 10), Rect::new(20, 20, 10, 10))]
#[case::up_left(Rect::new(5, 5, 10, 10), Rect::new(10, 10, 10, 10))]
#[case::up(Rect::new(20, 5, 10, 10), Rect::new(20, 10, 10, 10))]
#[case::up_right(Rect::new(105, 5, 10, 10), Rect::new(100, 10, 10, 10))]
#[case::left(Rect::new(5, 20, 10, 10), Rect::new(10, 20, 10, 10))]
#[case::right(Rect::new(105, 20, 10, 10), Rect::new(100, 20, 10, 10))]
#[case::down_left(Rect::new(5, 105, 10, 10), Rect::new(10, 100, 10, 10))]
#[case::down(Rect::new(20, 105, 10, 10), Rect::new(20, 100, 10, 10))]
#[case::down_right(Rect::new(105, 105, 10, 10), Rect::new(100, 100, 10, 10))]
#[case::too_wide(Rect::new(5, 20, 200, 10), Rect::new(10, 20, 100, 10))]
#[case::too_tall(Rect::new(20, 5, 10, 200), Rect::new(20, 10, 10, 100))]
#[case::too_large(Rect::new(0, 0, 200, 200), Rect::new(10, 10, 100, 100))]
fn clamp(#[case] rect: Rect, #[case] expected: Rect) {
let other = Rect::new(10, 10, 100, 100);
assert_eq!(rect.clamp(other), expected);
}
#[test]
fn rows() {
let area = Rect::new(0, 0, 3, 2);
let rows: Vec<Rect> = area.rows().collect();
let expected_rows: Vec<Rect> = vec![Rect::new(0, 0, 3, 1), Rect::new(0, 1, 3, 1)];
assert_eq!(rows, expected_rows);
}
#[test]
fn columns() {
let area = Rect::new(0, 0, 3, 2);
let columns: Vec<Rect> = area.columns().collect();
let expected_columns: Vec<Rect> = vec![
Rect::new(0, 0, 1, 2),
Rect::new(1, 0, 1, 2),
Rect::new(2, 0, 1, 2),
];
assert_eq!(columns, expected_columns);
}
#[test]
fn as_size() {
assert_eq!(
Rect::new(1, 2, 3, 4).as_size(),
Size {
width: 3,
height: 4
}
);
}
}

146
src/layout/segment_size.rs Normal file
View File

@@ -0,0 +1,146 @@
use strum::{Display, EnumString};
/// Option for segment size preferences
///
/// This controls how the space is distributed when the constraints are satisfied. By default, the
/// last chunk is expanded to fill the remaining space, but this can be changed to prefer equal
/// chunks or to not distribute extra space at all (which is the default used for laying out the
/// columns for [`Table`] widgets).
///
/// Note: If you're using this feature please help us come up with a good name. See [Issue
/// #536](https://github.com/ratatui-org/ratatui/issues/536) for more information.
///
/// [`Table`]: crate::widgets::Table
#[stability::unstable(
feature = "segment-size",
reason = "The name for this feature is not final and may change in the future",
issue = "https://github.com/ratatui-org/ratatui/issues/536"
)]
#[derive(Copy, Debug, Default, Display, EnumString, Clone, Eq, PartialEq, Hash)]
pub enum SegmentSize {
/// prefer equal chunks if other constraints are all satisfied
EvenDistribution,
/// the last chunk is expanded to fill the remaining space
#[default]
LastTakesRemainder,
/// extra space is not distributed
None,
}
#[cfg(test)]
mod tests {
use strum::ParseError;
use super::{SegmentSize::*, *};
use crate::prelude::{Constraint::*, *};
#[test]
fn segment_size_to_string() {
assert_eq!(EvenDistribution.to_string(), "EvenDistribution");
assert_eq!(LastTakesRemainder.to_string(), "LastTakesRemainder");
assert_eq!(None.to_string(), "None");
}
#[test]
fn segment_size_from_string() {
assert_eq!(
"EvenDistribution".parse::<SegmentSize>(),
Ok(EvenDistribution)
);
assert_eq!(
"LastTakesRemainder".parse::<SegmentSize>(),
Ok(LastTakesRemainder)
);
assert_eq!("None".parse::<SegmentSize>(), Ok(None));
assert_eq!("".parse::<SegmentSize>(), Err(ParseError::VariantNotFound));
}
fn get_x_width_with_segment_size(
segment_size: SegmentSize,
constraints: Vec<Constraint>,
target: Rect,
) -> Vec<(u16, u16)> {
let layout = Layout::default()
.direction(Direction::Horizontal)
.constraints(constraints)
.segment_size(segment_size);
let chunks = layout.split(target);
chunks.iter().map(|r| (r.x, r.width)).collect()
}
#[test]
fn test_split_equally_in_underspecified_case() {
let target = Rect::new(100, 200, 10, 10);
assert_eq!(
get_x_width_with_segment_size(LastTakesRemainder, vec![Min(2), Min(2), Min(0)], target),
[(100, 2), (102, 2), (104, 6)]
);
assert_eq!(
get_x_width_with_segment_size(EvenDistribution, vec![Min(2), Min(2), Min(0)], target),
[(100, 3), (103, 4), (107, 3)]
);
}
#[test]
fn test_split_equally_in_overconstrained_case_for_min() {
let target = Rect::new(100, 200, 100, 10);
assert_eq!(
get_x_width_with_segment_size(
LastTakesRemainder,
vec![Percentage(50), Min(10), Percentage(50)],
target
),
[(100, 50), (150, 10), (160, 40)]
);
assert_eq!(
get_x_width_with_segment_size(
EvenDistribution,
vec![Percentage(50), Min(10), Percentage(50)],
target
),
[(100, 45), (145, 10), (155, 45)]
);
}
#[test]
fn test_split_equally_in_overconstrained_case_for_max() {
let target = Rect::new(100, 200, 100, 10);
assert_eq!(
get_x_width_with_segment_size(
LastTakesRemainder,
vec![Percentage(30), Max(10), Percentage(30)],
target
),
[(100, 30), (130, 10), (140, 60)]
);
assert_eq!(
get_x_width_with_segment_size(
EvenDistribution,
vec![Percentage(30), Max(10), Percentage(30)],
target
),
[(100, 45), (145, 10), (155, 45)]
);
}
#[test]
fn test_split_equally_in_overconstrained_case_for_length() {
let target = Rect::new(100, 200, 100, 10);
assert_eq!(
get_x_width_with_segment_size(
LastTakesRemainder,
vec![Percentage(50), Length(10), Percentage(50)],
target
),
[(100, 50), (150, 10), (160, 40)]
);
assert_eq!(
get_x_width_with_segment_size(
EvenDistribution,
vec![Percentage(50), Length(10), Percentage(50)],
target
),
[(100, 45), (145, 10), (155, 45)]
);
}
}

59
src/layout/size.rs Normal file
View File

@@ -0,0 +1,59 @@
#![warn(missing_docs)]
use crate::prelude::*;
/// A simple size struct
///
/// The width and height are stored as `u16` values and represent the number of columns and rows
/// respectively.
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
pub struct Size {
/// The width in columns
pub width: u16,
/// The height in rows
pub height: u16,
}
impl Size {
/// Create a new `Size` struct
pub fn new(width: u16, height: u16) -> Self {
Size { width, height }
}
}
impl From<(u16, u16)> for Size {
fn from((width, height): (u16, u16)) -> Self {
Size { width, height }
}
}
impl From<Rect> for Size {
fn from(rect: Rect) -> Self {
rect.as_size()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new() {
let size = Size::new(10, 20);
assert_eq!(size.width, 10);
assert_eq!(size.height, 20);
}
#[test]
fn from_tuple() {
let size = Size::from((10, 20));
assert_eq!(size.width, 10);
assert_eq!(size.height, 20);
}
#[test]
fn from_rect() {
let size = Size::from(Rect::new(0, 0, 10, 20));
assert_eq!(size.width, 10);
assert_eq!(size.height, 20);
}
}

View File

@@ -4,27 +4,21 @@
//!
//! <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)
//! · [Send a Pull Request](https://github.com/ratatui-org/ratatui/compare)
//! [![Crate Badge]][Crate] [![Docs Badge]][API Docs] [![CI Badge]][CI Workflow] [![License
//! Badge]](./LICENSE)<br>
//! [![Codecov Badge]][Codecov] [![Deps.rs Badge]][Deps.rs] [![Discord Badge]][Discord Server]
//! [![Matrix Badge]][Matrix]<br>
//!
//! [Ratatui Website] · [API Docs] · [Examples] · [Changelog] · [Breaking Changes]<br>
//! [Contributing] · [Report a bug] · [Request a Feature] · [Create a Pull Request]
//!
//! </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][Ratatui Website] 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 +29,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,16 +37,16 @@
//! 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
//! - [API Docs] - the full API documentation for the library on docs.rs.
//! - [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].
//! - [Contributing] - Please read this if you are interested in contributing to the project.
//! - [Changelog] - generated by [git-cliff] utilizing [Conventional Commits].
//! - [Breaking Changes] - a list of breaking changes in the library.
//!
//! ## Quickstart
@@ -60,12 +54,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 +82,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 +132,7 @@
//! if key.kind == event::KeyEventKind::Press && key.code == KeyCode::Char('q') {
//! return Ok(true);
//! }
//! }
//! }
//! }
//! Ok(false)
//! }
@@ -161,7 +155,7 @@
//! 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::*};
@@ -173,7 +167,7 @@
//! Constraint::Length(1),
//! Constraint::Min(0),
//! Constraint::Length(1),
//! ]
//! ],
//! )
//! .split(frame.size());
//! frame.render_widget(
@@ -187,7 +181,7 @@
//!
//! let inner_layout = Layout::new(
//! Direction::Horizontal,
//! [Constraint::Percentage(50), Constraint::Percentage(50)]
//! [Constraint::Percentage(50), Constraint::Percentage(50)],
//! )
//! .split(main_layout[1]);
//! frame.render_widget(
@@ -215,7 +209,7 @@
//! 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::*};
@@ -229,7 +223,7 @@
//! Constraint::Length(1),
//! Constraint::Length(1),
//! Constraint::Min(0),
//! ]
//! ],
//! )
//! .split(frame.size());
//!
@@ -285,23 +279,25 @@
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/concepts/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
//! [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
//! [Create a Pull Request]: https://github.com/ratatui-org/ratatui/compare
//! [git-cliff]: https://git-cliff.org
//! [Conventional Commits]: https://www.conventionalcommits.org
//! [API Documentation]: https://docs.rs/ratatui
//! [API Docs]: https://docs.rs/ratatui
//! [Changelog]: https://github.com/ratatui-org/ratatui/blob/main/CHANGELOG.md
//! [Contributing]: https://github.com/ratatui-org/ratatui/blob/main/CONTRIBUTING.md
//! [Breaking Changes]: https://github.com/ratatui-org/ratatui/blob/main/BREAKING-CHANGES.md
@@ -321,24 +317,29 @@
//! [`Backend`]: backend::Backend
//! [`backend` module]: backend
//! [`crossterm::event`]: https://docs.rs/crossterm/latest/crossterm/event/index.html
//! [Ratatui]: https://ratatui.rs
//! [Crate]: https://crates.io/crates/ratatui
//! [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
//! [License Badge]: https://img.shields.io/crates/l/ratatui?style=flat-square
//! [CI Badge]:
//! https://img.shields.io/github/actions/workflow/status/ratatui-org/ratatui/ci.yml?style=flat-square&logo=github
//! [CI Workflow]: https://github.com/ratatui-org/ratatui/actions/workflows/ci.yml
//! [Codecov Badge]:
//! https://img.shields.io/codecov/c/github/ratatui-org/ratatui?logo=codecov&style=flat-square&token=BAQ8SOKEST
//! [Dependencies Badge]: https://deps.rs/repo/github/ratatui-org/ratatui/status.svg?style=flat-square
//! [Codecov]: https://app.codecov.io/gh/ratatui-org/ratatui
//! [Deps.rs Badge]: https://deps.rs/repo/github/ratatui-org/ratatui/status.svg?style=flat-square
//! [Deps.rs]: https://deps.rs/repo/github/ratatui-org/ratatui
//! [Discord Badge]:
//! https://img.shields.io/discord/1070692720437383208?label=discord&logo=discord&style=flat-square
//! [Discord Server]: https://discord.gg/pMCEU9hNEj
//! [Docs Badge]: https://img.shields.io/docsrs/ratatui?logo=rust&style=flat-square
//! [License Badge]: https://img.shields.io/crates/l/ratatui?style=flat-square
//! [Matrix Badge]:
//! https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix
//! [Matrix]: https://matrix.to/#/#ratatui:matrix.org
// show the feature flags in the generated documentation
#![cfg_attr(docsrs, feature(doc_auto_cfg))]

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)
//! )
//! );
//! ```
//!
@@ -64,16 +72,21 @@ use std::fmt::{self, Debug};
use bitflags::bitflags;
mod stylize;
pub use stylize::{Styled, Stylize};
mod color;
mod stylize;
pub use color::Color;
pub use stylize::{Styled, Stylize};
pub mod palette;
bitflags! {
/// Modifier changes the way a piece of text is displayed.
///
/// 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 +126,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 +143,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 +198,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 +246,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 {
@@ -285,9 +321,18 @@ 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"]
@@ -341,6 +386,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::*;
@@ -349,10 +397,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)
/// );
/// ```
#[must_use = "`patch` returns the modified style without modifying the original"]
pub fn patch(mut self, other: Style) -> Style {
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);
@@ -370,6 +420,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::*;
@@ -631,4 +809,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,29 @@ pub enum Color {
Indexed(u8),
}
impl Color {
/// Convert a u32 to a Color
///
/// The u32 should be in the format 0x00RRGGBB.
pub const fn from_u32(u: u32) -> Color {
let r = (u >> 16) as u8;
let g = (u >> 8) as u8;
let b = u as u8;
Color::Rgb(r, g, b)
}
}
#[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 +174,7 @@ impl std::error::Error for ParseColorError {}
///
/// ```
/// use std::str::FromStr;
///
/// use ratatui::prelude::*;
///
/// let color: Color = Color::from_str("blue").unwrap();
@@ -249,8 +277,20 @@ impl Display for Color {
mod tests {
use std::error::Error;
#[cfg(feature = "serde")]
use serde::de::{Deserialize, IntoDeserializer};
use super::*;
#[test]
fn from_u32() {
assert_eq!(Color::from_u32(0x000000), Color::Rgb(0, 0, 0));
assert_eq!(Color::from_u32(0xFF0000), Color::Rgb(255, 0, 0));
assert_eq!(Color::from_u32(0x00FF00), Color::Rgb(0, 255, 0));
assert_eq!(Color::from_u32(0x0000FF), Color::Rgb(0, 0, 255));
assert_eq!(Color::from_u32(0xFFFFFF), Color::Rgb(255, 255, 255));
}
#[test]
fn from_rgb_color() {
let color: Color = Color::from_str("#FF0000").unwrap();
@@ -359,4 +399,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());
}
}

4
src/style/palette.rs Normal file
View File

@@ -0,0 +1,4 @@
//! A module for defining color palettes.
pub mod material;
pub mod tailwind;

View File

@@ -0,0 +1,606 @@
//! Material design color palettes.
//!
//! Represents the colors from the 2014 [Material design color palettes][palettes] by Google.
//!
//! [palettes]: https://m2.material.io/design/color/the-color-system.html#tools-for-picking-colors
//!
//! There are 16 palettes with accent colors, and 3 palettes without accent colors. Each palette
//! has 10 colors, with variants from 50 to 900. The accent palettes also have 4 accent colors
//! with variants from 100 to 700. Black and White are also included for completeness and to avoid
//! being affected by any terminal theme that might be in use.
//!
//! This module exists to provide a convenient way to use the colors from the
//! [`matdesign-color` crate] in your application.
//!
//! <style>
//! .color { display: flex; align-items: center; }
//! .color > div { width: 2rem; height: 2rem; }
//! .color > div.name { width: 150px; !important; }
//! </style>
//! <div style="overflow-x: auto">
//! <div style="display: flex; flex-direction:column; text-align: left">
//! <div class="color" style="font-size:0.8em">
//! <div class="name"></div>
//! <div>C50</div>
//! <div>C100</div>
//! <div>C200</div>
//! <div>C300</div>
//! <div>C400</div>
//! <div>C500</div>
//! <div>C600</div>
//! <div>C700</div>
//! <div>C800</div>
//! <div>C900</div>
//! <div>A100</div>
//! <div>A200</div>
//! <div>A400</div>
//! <div>A700</div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`RED`]</div>
//! <div style="background-color: #FFEBEE"></div>
//! <div style="background-color: #FFCDD2"></div>
//! <div style="background-color: #EF9A9A"></div>
//! <div style="background-color: #E57373"></div>
//! <div style="background-color: #EF5350"></div>
//! <div style="background-color: #F44336"></div>
//! <div style="background-color: #E53935"></div>
//! <div style="background-color: #D32F2F"></div>
//! <div style="background-color: #C62828"></div>
//! <div style="background-color: #B71C1C"></div>
//! <div style="background-color: #FF8A80"></div>
//! <div style="background-color: #FF5252"></div>
//! <div style="background-color: #FF1744"></div>
//! <div style="background-color: #D50000"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`PINK`]</div>
//! <div style="background-color: #FCE4EC"></div>
//! <div style="background-color: #F8BBD0"></div>
//! <div style="background-color: #F48FB1"></div>
//! <div style="background-color: #F06292"></div>
//! <div style="background-color: #EC407A"></div>
//! <div style="background-color: #E91E63"></div>
//! <div style="background-color: #D81B60"></div>
//! <div style="background-color: #C2185B"></div>
//! <div style="background-color: #AD1457"></div>
//! <div style="background-color: #880E4F"></div>
//! <div style="background-color: #FF80AB"></div>
//! <div style="background-color: #FF4081"></div>
//! <div style="background-color: #F50057"></div>
//! <div style="background-color: #C51162"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`PURPLE`]</div>
//! <div style="background-color: #F3E5F5"></div>
//! <div style="background-color: #E1BEE7"></div>
//! <div style="background-color: #CE93D8"></div>
//! <div style="background-color: #BA68C8"></div>
//! <div style="background-color: #AB47BC"></div>
//! <div style="background-color: #9C27B0"></div>
//! <div style="background-color: #8E24AA"></div>
//! <div style="background-color: #7B1FA2"></div>
//! <div style="background-color: #6A1B9A"></div>
//! <div style="background-color: #4A148C"></div>
//! <div style="background-color: #EA80FC"></div>
//! <div style="background-color: #E040FB"></div>
//! <div style="background-color: #D500F9"></div>
//! <div style="background-color: #AA00FF"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`DEEP_PURPLE`]</div>
//! <div style="background-color: #EDE7F6"></div>
//! <div style="background-color: #D1C4E9"></div>
//! <div style="background-color: #B39DDB"></div>
//! <div style="background-color: #9575CD"></div>
//! <div style="background-color: #7E57C2"></div>
//! <div style="background-color: #673AB7"></div>
//! <div style="background-color: #5E35B1"></div>
//! <div style="background-color: #512DA8"></div>
//! <div style="background-color: #4527A0"></div>
//! <div style="background-color: #311B92"></div>
//! <div style="background-color: #B388FF"></div>
//! <div style="background-color: #7C4DFF"></div>
//! <div style="background-color: #651FFF"></div>
//! <div style="background-color: #6200EA"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`INDIGO`]</div>
//! <div style="background-color: #E8EAF6"></div>
//! <div style="background-color: #C5CAE9"></div>
//! <div style="background-color: #9FA8DA"></div>
//! <div style="background-color: #7986CB"></div>
//! <div style="background-color: #5C6BC0"></div>
//! <div style="background-color: #3F51B5"></div>
//! <div style="background-color: #3949AB"></div>
//! <div style="background-color: #303F9F"></div>
//! <div style="background-color: #283593"></div>
//! <div style="background-color: #1A237E"></div>
//! <div style="background-color: #8C9EFF"></div>
//! <div style="background-color: #536DFE"></div>
//! <div style="background-color: #3D5AFE"></div>
//! <div style="background-color: #304FFE"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`BLUE`]</div>
//! <div style="background-color: #E3F2FD"></div>
//! <div style="background-color: #BBDEFB"></div>
//! <div style="background-color: #90CAF9"></div>
//! <div style="background-color: #64B5F6"></div>
//! <div style="background-color: #42A5F5"></div>
//! <div style="background-color: #2196F3"></div>
//! <div style="background-color: #1E88E5"></div>
//! <div style="background-color: #1976D2"></div>
//! <div style="background-color: #1565C0"></div>
//! <div style="background-color: #0D47A1"></div>
//! <div style="background-color: #82B1FF"></div>
//! <div style="background-color: #448AFF"></div>
//! <div style="background-color: #2979FF"></div>
//! <div style="background-color: #2962FF"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`LIGHT_BLUE`]</div>
//! <div style="background-color: #E1F5FE"></div>
//! <div style="background-color: #B3E5FC"></div>
//! <div style="background-color: #81D4FA"></div>
//! <div style="background-color: #4FC3F7"></div>
//! <div style="background-color: #29B6F6"></div>
//! <div style="background-color: #03A9F4"></div>
//! <div style="background-color: #039BE5"></div>
//! <div style="background-color: #0288D1"></div>
//! <div style="background-color: #0277BD"></div>
//! <div style="background-color: #01579B"></div>
//! <div style="background-color: #80D8FF"></div>
//! <div style="background-color: #40C4FF"></div>
//! <div style="background-color: #00B0FF"></div>
//! <div style="background-color: #0091EA"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`CYAN`]</div>
//! <div style="background-color: #E0F7FA"></div>
//! <div style="background-color: #B2EBF2"></div>
//! <div style="background-color: #80DEEA"></div>
//! <div style="background-color: #4DD0E1"></div>
//! <div style="background-color: #26C6DA"></div>
//! <div style="background-color: #00BCD4"></div>
//! <div style="background-color: #00ACC1"></div>
//! <div style="background-color: #0097A7"></div>
//! <div style="background-color: #00838F"></div>
//! <div style="background-color: #006064"></div>
//! <div style="background-color: #84FFFF"></div>
//! <div style="background-color: #18FFFF"></div>
//! <div style="background-color: #00E5FF"></div>
//! <div style="background-color: #00B8D4"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`TEAL`]</div>
//! <div style="background-color: #E0F2F1"></div>
//! <div style="background-color: #B2DFDB"></div>
//! <div style="background-color: #80CBC4"></div>
//! <div style="background-color: #4DB6AC"></div>
//! <div style="background-color: #26A69A"></div>
//! <div style="background-color: #009688"></div>
//! <div style="background-color: #00897B"></div>
//! <div style="background-color: #00796B"></div>
//! <div style="background-color: #00695C"></div>
//! <div style="background-color: #004D40"></div>
//! <div style="background-color: #A7FFEB"></div>
//! <div style="background-color: #64FFDA"></div>
//! <div style="background-color: #1DE9B6"></div>
//! <div style="background-color: #00BFA5"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`GREEN`]</div>
//! <div style="background-color: #E8F5E9"></div>
//! <div style="background-color: #C8E6C9"></div>
//! <div style="background-color: #A5D6A7"></div>
//! <div style="background-color: #81C784"></div>
//! <div style="background-color: #66BB6A"></div>
//! <div style="background-color: #4CAF50"></div>
//! <div style="background-color: #43A047"></div>
//! <div style="background-color: #388E3C"></div>
//! <div style="background-color: #2E7D32"></div>
//! <div style="background-color: #1B5E20"></div>
//! <div style="background-color: #B9F6CA"></div>
//! <div style="background-color: #69F0AE"></div>
//! <div style="background-color: #00E676"></div>
//! <div style="background-color: #00C853"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`LIGHT_GREEN`]</div>
//! <div style="background-color: #F1F8E9"></div>
//! <div style="background-color: #DCEDC8"></div>
//! <div style="background-color: #C5E1A5"></div>
//! <div style="background-color: #AED581"></div>
//! <div style="background-color: #9CCC65"></div>
//! <div style="background-color: #8BC34A"></div>
//! <div style="background-color: #7CB342"></div>
//! <div style="background-color: #689F38"></div>
//! <div style="background-color: #558B2F"></div>
//! <div style="background-color: #33691E"></div>
//! <div style="background-color: #CCFF90"></div>
//! <div style="background-color: #B2FF59"></div>
//! <div style="background-color: #76FF03"></div>
//! <div style="background-color: #64DD17"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`LIME`]</div>
//! <div style="background-color: #F9FBE7"></div>
//! <div style="background-color: #F0F4C3"></div>
//! <div style="background-color: #E6EE9C"></div>
//! <div style="background-color: #DCE775"></div>
//! <div style="background-color: #D4E157"></div>
//! <div style="background-color: #CDDC39"></div>
//! <div style="background-color: #C0CA33"></div>
//! <div style="background-color: #AFB42B"></div>
//! <div style="background-color: #9E9D24"></div>
//! <div style="background-color: #827717"></div>
//! <div style="background-color: #F4FF81"></div>
//! <div style="background-color: #EEFF41"></div>
//! <div style="background-color: #C6FF00"></div>
//! <div style="background-color: #AEEA00"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`YELLOW`]</div>
//! <div style="background-color: #FFFDE7"></div>
//! <div style="background-color: #FFF9C4"></div>
//! <div style="background-color: #FFF59D"></div>
//! <div style="background-color: #FFF176"></div>
//! <div style="background-color: #FFEE58"></div>
//! <div style="background-color: #FFEB3B"></div>
//! <div style="background-color: #FDD835"></div>
//! <div style="background-color: #FBC02D"></div>
//! <div style="background-color: #F9A825"></div>
//! <div style="background-color: #F57F17"></div>
//! <div style="background-color: #FFFF8D"></div>
//! <div style="background-color: #FFFF00"></div>
//! <div style="background-color: #FFEA00"></div>
//! <div style="background-color: #FFD600"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`AMBER`]</div>
//! <div style="background-color: #FFF8E1"></div>
//! <div style="background-color: #FFECB3"></div>
//! <div style="background-color: #FFE082"></div>
//! <div style="background-color: #FFD54F"></div>
//! <div style="background-color: #FFCA28"></div>
//! <div style="background-color: #FFC107"></div>
//! <div style="background-color: #FFB300"></div>
//! <div style="background-color: #FFA000"></div>
//! <div style="background-color: #FF8F00"></div>
//! <div style="background-color: #FF6F00"></div>
//! <div style="background-color: #FFE57F"></div>
//! <div style="background-color: #FFD740"></div>
//! <div style="background-color: #FFC400"></div>
//! <div style="background-color: #FFAB00"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`ORANGE`]</div>
//! <div style="background-color: #FFF3E0"></div>
//! <div style="background-color: #FFE0B2"></div>
//! <div style="background-color: #FFCC80"></div>
//! <div style="background-color: #FFB74D"></div>
//! <div style="background-color: #FFA726"></div>
//! <div style="background-color: #FF9800"></div>
//! <div style="background-color: #FB8C00"></div>
//! <div style="background-color: #F57C00"></div>
//! <div style="background-color: #EF6C00"></div>
//! <div style="background-color: #E65100"></div>
//! <div style="background-color: #FFD180"></div>
//! <div style="background-color: #FFAB40"></div>
//! <div style="background-color: #FF9100"></div>
//! <div style="background-color: #FF6D00"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`DEEP_ORANGE`]</div>
//! <div style="background-color: #FBE9E7"></div>
//! <div style="background-color: #FFCCBC"></div>
//! <div style="background-color: #FFAB91"></div>
//! <div style="background-color: #FF8A65"></div>
//! <div style="background-color: #FF7043"></div>
//! <div style="background-color: #FF5722"></div>
//! <div style="background-color: #F4511E"></div>
//! <div style="background-color: #E64A19"></div>
//! <div style="background-color: #D84315"></div>
//! <div style="background-color: #BF360C"></div>
//! <div style="background-color: #FF9E80"></div>
//! <div style="background-color: #FF6E40"></div>
//! <div style="background-color: #FF3D00"></div>
//! <div style="background-color: #DD2C00"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`BROWN`]</div>
//! <div style="background-color: #EFEBE9"></div>
//! <div style="background-color: #D7CCC8"></div>
//! <div style="background-color: #BCAAA4"></div>
//! <div style="background-color: #A1887F"></div>
//! <div style="background-color: #8D6E63"></div>
//! <div style="background-color: #795548"></div>
//! <div style="background-color: #6D4C41"></div>
//! <div style="background-color: #5D4037"></div>
//! <div style="background-color: #4E342E"></div>
//! <div style="background-color: #3E2723"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`GRAY`]</div>
//! <div style="background-color: #FAFAFA"></div>
//! <div style="background-color: #F5F5F5"></div>
//! <div style="background-color: #EEEEEE"></div>
//! <div style="background-color: #E0E0E0"></div>
//! <div style="background-color: #BDBDBD"></div>
//! <div style="background-color: #9E9E9E"></div>
//! <div style="background-color: #757575"></div>
//! <div style="background-color: #616161"></div>
//! <div style="background-color: #424242"></div>
//! <div style="background-color: #212121"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`BLUE_GRAY`]</div>
//! <div style="background-color: #ECEFF1"></div>
//! <div style="background-color: #CFD8DC"></div>
//! <div style="background-color: #B0BEC5"></div>
//! <div style="background-color: #90A4AE"></div>
//! <div style="background-color: #78909C"></div>
//! <div style="background-color: #607D8B"></div>
//! <div style="background-color: #546E7A"></div>
//! <div style="background-color: #455A64"></div>
//! <div style="background-color: #37474F"></div>
//! <div style="background-color: #263238"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`BLACK`]</div>
//! <div class="bw" style="width: 350px; background-color: #000000"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`WHITE`]</div>
//! <div style="width: 350px; background-color: #FFFFFF"></div>
//! </div>
//! </div>
//! </div>
//!
//! # Example
//!
//! ```rust
//! # use ratatui::prelude::*;
//! use ratatui::style::palette::material::{BLUE, RED};
//!
//! assert_eq!(RED.c500, Color::Rgb(244, 67, 54));
//! assert_eq!(BLUE.c500, Color::Rgb(33, 150, 243));
//! ```
//!
//! [`matdesign-color` crate]: https://crates.io/crates/matdesign-color
use crate::prelude::*;
/// A palette of colors for use in Material design with accent colors
///
/// This is a collection of colors that are used in Material design. They consist of a set of
/// colors from 50 to 900, and a set of accent colors from 100 to 700.
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub struct AccentedPalette {
pub c50: Color,
pub c100: Color,
pub c200: Color,
pub c300: Color,
pub c400: Color,
pub c500: Color,
pub c600: Color,
pub c700: Color,
pub c800: Color,
pub c900: Color,
pub a100: Color,
pub a200: Color,
pub a400: Color,
pub a700: Color,
}
/// A palette of colors for use in Material design without accent colors
///
/// This is a collection of colors that are used in Material design. They consist of a set of
/// colors from 50 to 900.
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub struct NonAccentedPalette {
pub c50: Color,
pub c100: Color,
pub c200: Color,
pub c300: Color,
pub c400: Color,
pub c500: Color,
pub c600: Color,
pub c700: Color,
pub c800: Color,
pub c900: Color,
}
impl AccentedPalette {
/// Create a new AccentedPalette from the given variants
///
/// The variants should be in the format [0x00RRGGBB, ...]
pub const fn from_variants(variants: [u32; 14]) -> AccentedPalette {
AccentedPalette {
c50: Color::from_u32(variants[0]),
c100: Color::from_u32(variants[1]),
c200: Color::from_u32(variants[2]),
c300: Color::from_u32(variants[3]),
c400: Color::from_u32(variants[4]),
c500: Color::from_u32(variants[5]),
c600: Color::from_u32(variants[6]),
c700: Color::from_u32(variants[7]),
c800: Color::from_u32(variants[8]),
c900: Color::from_u32(variants[9]),
a100: Color::from_u32(variants[10]),
a200: Color::from_u32(variants[11]),
a400: Color::from_u32(variants[12]),
a700: Color::from_u32(variants[13]),
}
}
}
impl NonAccentedPalette {
/// Create a new NonAccented from the given variants
///
/// The variants should be in the format [0x00RRGGBB, ...]
pub const fn from_variants(variants: [u32; 10]) -> NonAccentedPalette {
NonAccentedPalette {
c50: Color::from_u32(variants[0]),
c100: Color::from_u32(variants[1]),
c200: Color::from_u32(variants[2]),
c300: Color::from_u32(variants[3]),
c400: Color::from_u32(variants[4]),
c500: Color::from_u32(variants[5]),
c600: Color::from_u32(variants[6]),
c700: Color::from_u32(variants[7]),
c800: Color::from_u32(variants[8]),
c900: Color::from_u32(variants[9]),
}
}
}
// Accented palettes
pub const RED: AccentedPalette = AccentedPalette::from_variants(variants::RED);
pub const PINK: AccentedPalette = AccentedPalette::from_variants(variants::PINK);
pub const PURPLE: AccentedPalette = AccentedPalette::from_variants(variants::PURPLE);
pub const DEEP_PURPLE: AccentedPalette = AccentedPalette::from_variants(variants::DEEP_PURPLE);
pub const INDIGO: AccentedPalette = AccentedPalette::from_variants(variants::INDIGO);
pub const BLUE: AccentedPalette = AccentedPalette::from_variants(variants::BLUE);
pub const LIGHT_BLUE: AccentedPalette = AccentedPalette::from_variants(variants::LIGHT_BLUE);
pub const CYAN: AccentedPalette = AccentedPalette::from_variants(variants::CYAN);
pub const TEAL: AccentedPalette = AccentedPalette::from_variants(variants::TEAL);
pub const GREEN: AccentedPalette = AccentedPalette::from_variants(variants::GREEN);
pub const LIGHT_GREEN: AccentedPalette = AccentedPalette::from_variants(variants::LIGHT_GREEN);
pub const LIME: AccentedPalette = AccentedPalette::from_variants(variants::LIME);
pub const YELLOW: AccentedPalette = AccentedPalette::from_variants(variants::YELLOW);
pub const AMBER: AccentedPalette = AccentedPalette::from_variants(variants::AMBER);
pub const ORANGE: AccentedPalette = AccentedPalette::from_variants(variants::ORANGE);
pub const DEEP_ORANGE: AccentedPalette = AccentedPalette::from_variants(variants::DEEP_ORANGE);
// Unaccented palettes
pub const BROWN: NonAccentedPalette = NonAccentedPalette::from_variants(variants::BROWN);
pub const GRAY: NonAccentedPalette = NonAccentedPalette::from_variants(variants::GRAY);
pub const BLUE_GRAY: NonAccentedPalette = NonAccentedPalette::from_variants(variants::BLUE_GRAY);
// Black and white included for completeness
pub const BLACK: Color = Color::from_u32(0x000000);
pub const WHITE: Color = Color::from_u32(0xFFFFFF);
mod variants {
pub const RED: [u32; 14] = [
0xFFEBEE, 0xFFCDD2, 0xEF9A9A, 0xE57373, 0xEF5350, 0xF44336, 0xE53935, 0xD32F2F, 0xC62828,
0xB71C1C, 0xFF8A80, 0xFF5252, 0xFF1744, 0xD50000,
];
pub const PINK: [u32; 14] = [
0xFCE4EC, 0xF8BBD0, 0xF48FB1, 0xF06292, 0xEC407A, 0xE91E63, 0xD81B60, 0xC2185B, 0xAD1457,
0x880E4F, 0xFF80AB, 0xFF4081, 0xF50057, 0xC51162,
];
pub const PURPLE: [u32; 14] = [
0xF3E5F5, 0xE1BEE7, 0xCE93D8, 0xBA68C8, 0xAB47BC, 0x9C27B0, 0x8E24AA, 0x7B1FA2, 0x6A1B9A,
0x4A148C, 0xEA80FC, 0xE040FB, 0xD500F9, 0xAA00FF,
];
pub const DEEP_PURPLE: [u32; 14] = [
0xEDE7F6, 0xD1C4E9, 0xB39DDB, 0x9575CD, 0x7E57C2, 0x673AB7, 0x5E35B1, 0x512DA8, 0x4527A0,
0x311B92, 0xB388FF, 0x7C4DFF, 0x651FFF, 0x6200EA,
];
pub const INDIGO: [u32; 14] = [
0xE8EAF6, 0xC5CAE9, 0x9FA8DA, 0x7986CB, 0x5C6BC0, 0x3F51B5, 0x3949AB, 0x303F9F, 0x283593,
0x1A237E, 0x8C9EFF, 0x536DFE, 0x3D5AFE, 0x304FFE,
];
pub const BLUE: [u32; 14] = [
0xE3F2FD, 0xBBDEFB, 0x90CAF9, 0x64B5F6, 0x42A5F5, 0x2196F3, 0x1E88E5, 0x1976D2, 0x1565C0,
0x0D47A1, 0x82B1FF, 0x448AFF, 0x2979FF, 0x2962FF,
];
pub const LIGHT_BLUE: [u32; 14] = [
0xE1F5FE, 0xB3E5FC, 0x81D4FA, 0x4FC3F7, 0x29B6F6, 0x03A9F4, 0x039BE5, 0x0288D1, 0x0277BD,
0x01579B, 0x80D8FF, 0x40C4FF, 0x00B0FF, 0x0091EA,
];
pub const CYAN: [u32; 14] = [
0xE0F7FA, 0xB2EBF2, 0x80DEEA, 0x4DD0E1, 0x26C6DA, 0x00BCD4, 0x00ACC1, 0x0097A7, 0x00838F,
0x006064, 0x84FFFF, 0x18FFFF, 0x00E5FF, 0x00B8D4,
];
pub const TEAL: [u32; 14] = [
0xE0F2F1, 0xB2DFDB, 0x80CBC4, 0x4DB6AC, 0x26A69A, 0x009688, 0x00897B, 0x00796B, 0x00695C,
0x004D40, 0xA7FFEB, 0x64FFDA, 0x1DE9B6, 0x00BFA5,
];
pub const GREEN: [u32; 14] = [
0xE8F5E9, 0xC8E6C9, 0xA5D6A7, 0x81C784, 0x66BB6A, 0x4CAF50, 0x43A047, 0x388E3C, 0x2E7D32,
0x1B5E20, 0xB9F6CA, 0x69F0AE, 0x00E676, 0x00C853,
];
pub const LIGHT_GREEN: [u32; 14] = [
0xF1F8E9, 0xDCEDC8, 0xC5E1A5, 0xAED581, 0x9CCC65, 0x8BC34A, 0x7CB342, 0x689F38, 0x558B2F,
0x33691E, 0xCCFF90, 0xB2FF59, 0x76FF03, 0x64DD17,
];
pub const LIME: [u32; 14] = [
0xF9FBE7, 0xF0F4C3, 0xE6EE9C, 0xDCE775, 0xD4E157, 0xCDDC39, 0xC0CA33, 0xAFB42B, 0x9E9D24,
0x827717, 0xF4FF81, 0xEEFF41, 0xC6FF00, 0xAEEA00,
];
pub const YELLOW: [u32; 14] = [
0xFFFDE7, 0xFFF9C4, 0xFFF59D, 0xFFF176, 0xFFEE58, 0xFFEB3B, 0xFDD835, 0xFBC02D, 0xF9A825,
0xF57F17, 0xFFFF8D, 0xFFFF00, 0xFFEA00, 0xFFD600,
];
pub const AMBER: [u32; 14] = [
0xFFF8E1, 0xFFECB3, 0xFFE082, 0xFFD54F, 0xFFCA28, 0xFFC107, 0xFFB300, 0xFFA000, 0xFF8F00,
0xFF6F00, 0xFFE57F, 0xFFD740, 0xFFC400, 0xFFAB00,
];
pub const ORANGE: [u32; 14] = [
0xFFF3E0, 0xFFE0B2, 0xFFCC80, 0xFFB74D, 0xFFA726, 0xFF9800, 0xFB8C00, 0xF57C00, 0xEF6C00,
0xE65100, 0xFFD180, 0xFFAB40, 0xFF9100, 0xFF6D00,
];
pub const DEEP_ORANGE: [u32; 14] = [
0xFBE9E7, 0xFFCCBC, 0xFFAB91, 0xFF8A65, 0xFF7043, 0xFF5722, 0xF4511E, 0xE64A19, 0xD84315,
0xBF360C, 0xFF9E80, 0xFF6E40, 0xFF3D00, 0xDD2C00,
];
pub const BROWN: [u32; 10] = [
0xEFEBE9, 0xD7CCC8, 0xBCAAA4, 0xA1887F, 0x8D6E63, 0x795548, 0x6D4C41, 0x5D4037, 0x4E342E,
0x3E2723,
];
pub const GRAY: [u32; 10] = [
0xFAFAFA, 0xF5F5F5, 0xEEEEEE, 0xE0E0E0, 0xBDBDBD, 0x9E9E9E, 0x757575, 0x616161, 0x424242,
0x212121,
];
pub const BLUE_GRAY: [u32; 10] = [
0xECEFF1, 0xCFD8DC, 0xB0BEC5, 0x90A4AE, 0x78909C, 0x607D8B, 0x546E7A, 0x455A64, 0x37474F,
0x263238,
];
}

View File

@@ -0,0 +1,652 @@
//! Represents the Tailwind CSS [default color palette][palette].
//!
//! [palette]: https://tailwindcss.com/docs/customizing-colors#default-color-palette
//!
//! There are 22 palettes. Each palette has 11 colors, with variants from 50 to 950. Black and White
//! are also included for completeness and to avoid being affected by any terminal theme that might
//! be in use.
//!
//! <style>
//! .color { display: flex; align-items: center; }
//! .color > div { width: 2rem; height: 2rem; }
//! .color > div.name { width: 150px; !important; }
//! </style>
//! <div style="overflow-x: auto">
//! <div style="display: flex; flex-direction:column; text-align: left">
//! <div class="color" style="font-size:0.8em">
//! <div class="name"></div>
//! <div>C50</div> <div>C100</div> <div>C200</div> <div>C300</div> <div>C400</div>
//! <div>C500</div> <div>C600</div> <div>C700</div> <div>C800</div> <div>C900</div>
//! <div>C950</div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`SLATE`]</div>
//! <div style="background-color: #f8fafc"></div> <div style="background-color: #f1f5f9"></div>
//! <div style="background-color: #e2e8f0"></div> <div style="background-color: #cbd5e1"></div>
//! <div style="background-color: #94a3b8"></div> <div style="background-color: #64748b"></div>
//! <div style="background-color: #475569"></div> <div style="background-color: #334155"></div>
//! <div style="background-color: #1e293b"></div> <div style="background-color: #0f172a"></div>
//! <div style="background-color: #020617"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`GRAY`]</div>
//! <div style="background-color: #f9fafb"></div> <div style="background-color: #f3f4f6"></div>
//! <div style="background-color: #e5e7eb"></div> <div style="background-color: #d1d5db"></div>
//! <div style="background-color: #9ca3af"></div> <div style="background-color: #6b7280"></div>
//! <div style="background-color: #4b5563"></div> <div style="background-color: #374151"></div>
//! <div style="background-color: #1f2937"></div> <div style="background-color: #111827"></div>
//! <div style="background-color: #0a0a0a"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`ZINC`]</div>
//! <div style="background-color: #fafafa"></div> <div style="background-color: #f5f5f5"></div>
//! <div style="background-color: #e5e5e5"></div> <div style="background-color: #d4d4d4"></div>
//! <div style="background-color: #a1a1aa"></div> <div style="background-color: #71717a"></div>
//! <div style="background-color: #52525b"></div> <div style="background-color: #404040"></div>
//! <div style="background-color: #262626"></div> <div style="background-color: #171717"></div>
//! <div style="background-color: #0a0a0a"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`NEUTRAL`]</div>
//! <div style="background-color: #fafafa"></div> <div style="background-color: #f5f5f5"></div>
//! <div style="background-color: #e5e5e5"></div> <div style="background-color: #d4d4d4"></div>
//! <div style="background-color: #a3a3a3"></div> <div style="background-color: #737373"></div>
//! <div style="background-color: #525252"></div> <div style="background-color: #404040"></div>
//! <div style="background-color: #262626"></div> <div style="background-color: #171717"></div>
//! <div style="background-color: #0a0a0a"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`STONE`]</div>
//! <div style="background-color: #fafaf9"></div> <div style="background-color: #f5f5f4"></div>
//! <div style="background-color: #e7e5e4"></div> <div style="background-color: #d6d3d1"></div>
//! <div style="background-color: #a8a29e"></div> <div style="background-color: #78716c"></div>
//! <div style="background-color: #57534e"></div> <div style="background-color: #44403c"></div>
//! <div style="background-color: #292524"></div> <div style="background-color: #1c1917"></div>
//! <div style="background-color: #0c0a09"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`RED`]</div>
//! <div style="background-color: #fef2f2"></div> <div style="background-color: #fee2e2"></div>
//! <div style="background-color: #fecaca"></div> <div style="background-color: #fca5a5"></div>
//! <div style="background-color: #f87171"></div> <div style="background-color: #ef4444"></div>
//! <div style="background-color: #dc2626"></div> <div style="background-color: #b91c1c"></div>
//! <div style="background-color: #991b1b"></div> <div style="background-color: #7f1d1d"></div>
//! <div style="background-color: #450a0a"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`ORANGE`]</div>
//! <div style="background-color: #fff7ed"></div> <div style="background-color: #ffedd5"></div>
//! <div style="background-color: #fed7aa"></div> <div style="background-color: #fdba74"></div>
//! <div style="background-color: #fb923c"></div> <div style="background-color: #f97316"></div>
//! <div style="background-color: #ea580c"></div> <div style="background-color: #c2410c"></div>
//! <div style="background-color: #9a3412"></div> <div style="background-color: #7c2d12"></div>
//! <div style="background-color: #431407"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`AMBER`]</div>
//! <div style="background-color: #fffbeb"></div> <div style="background-color: #fef3c7"></div>
//! <div style="background-color: #fde68a"></div> <div style="background-color: #fcd34d"></div>
//! <div style="background-color: #fbbf24"></div> <div style="background-color: #f59e0b"></div>
//! <div style="background-color: #d97706"></div> <div style="background-color: #b45309"></div>
//! <div style="background-color: #92400e"></div> <div style="background-color: #78350f"></div>
//! <div style="background-color: #451a03"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`YELLOW`]</div>
//! <div style="background-color: #fefce8"></div> <div style="background-color: #fef9c3"></div>
//! <div style="background-color: #fef08a"></div> <div style="background-color: #fde047"></div>
//! <div style="background-color: #facc15"></div> <div style="background-color: #eab308"></div>
//! <div style="background-color: #ca8a04"></div> <div style="background-color: #a16207"></div>
//! <div style="background-color: #854d0e"></div> <div style="background-color: #713f12"></div>
//! <div style="background-color: #422006"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`LIME`]</div>
//! <div style="background-color: #f7fee7"></div> <div style="background-color: #ecfccb"></div>
//! <div style="background-color: #d9f99d"></div> <div style="background-color: #bef264"></div>
//! <div style="background-color: #a3e635"></div> <div style="background-color: #84cc16"></div>
//! <div style="background-color: #65a30d"></div> <div style="background-color: #4d7c0f"></div>
//! <div style="background-color: #3f6212"></div> <div style="background-color: #365314"></div>
//! <div style="background-color: #1a2e05"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`GREEN`]</div>
//! <div style="background-color: #f0fdf4"></div> <div style="background-color: #dcfce7"></div>
//! <div style="background-color: #bbf7d0"></div> <div style="background-color: #86efac"></div>
//! <div style="background-color: #4ade80"></div> <div style="background-color: #22c55e"></div>
//! <div style="background-color: #16a34a"></div> <div style="background-color: #15803d"></div>
//! <div style="background-color: #166534"></div> <div style="background-color: #14532d"></div>
//! <div style="background-color: #052e16"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`EMERALD`]</div>
//! <div style="background-color: #ecfdf5"></div> <div style="background-color: #d1fae5"></div>
//! <div style="background-color: #a7f3d0"></div> <div style="background-color: #6ee7b7"></div>
//! <div style="background-color: #34d399"></div> <div style="background-color: #10b981"></div>
//! <div style="background-color: #059669"></div> <div style="background-color: #047857"></div>
//! <div style="background-color: #065f46"></div> <div style="background-color: #064e3b"></div>
//! <div style="background-color: #022c22"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`TEAL`]</div>
//! <div style="background-color: #f0fdfa"></div> <div style="background-color: #ccfbf1"></div>
//! <div style="background-color: #99f6e4"></div> <div style="background-color: #5eead4"></div>
//! <div style="background-color: #2dd4bf"></div> <div style="background-color: #14b8a6"></div>
//! <div style="background-color: #0d9488"></div> <div style="background-color: #0f766e"></div>
//! <div style="background-color: #115e59"></div> <div style="background-color: #134e4a"></div>
//! <div style="background-color: #042f2e"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`CYAN`]</div>
//! <div style="background-color: #ecfeff"></div> <div style="background-color: #cffafe"></div>
//! <div style="background-color: #a5f3fc"></div> <div style="background-color: #67e8f9"></div>
//! <div style="background-color: #22d3ee"></div> <div style="background-color: #06b6d4"></div>
//! <div style="background-color: #0891b2"></div> <div style="background-color: #0e7490"></div>
//! <div style="background-color: #155e75"></div> <div style="background-color: #164e63"></div>
//! <div style="background-color: #083344"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`SKY`]</div>
//! <div style="background-color: #f0f9ff"></div> <div style="background-color: #e0f2fe"></div>
//! <div style="background-color: #bae6fd"></div> <div style="background-color: #7dd3fc"></div>
//! <div style="background-color: #38bdf8"></div> <div style="background-color: #0ea5e9"></div>
//! <div style="background-color: #0284c7"></div> <div style="background-color: #0369a1"></div>
//! <div style="background-color: #075985"></div> <div style="background-color: #0c4a6e"></div>
//! <div style="background-color: #082f49"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`BLUE`]</div>
//! <div style="background-color: #eff6ff"></div> <div style="background-color: #dbeafe"></div>
//! <div style="background-color: #bfdbfe"></div> <div style="background-color: #93c5fd"></div>
//! <div style="background-color: #60a5fa"></div> <div style="background-color: #3b82f6"></div>
//! <div style="background-color: #2563eb"></div> <div style="background-color: #1d4ed8"></div>
//! <div style="background-color: #1e40af"></div> <div style="background-color: #1e3a8a"></div>
//! <div style="background-color: #172554"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`INDIGO`]</div>
//! <div style="background-color: #eef2ff"></div> <div style="background-color: #e0e7ff"></div>
//! <div style="background-color: #c7d2fe"></div> <div style="background-color: #a5b4fc"></div>
//! <div style="background-color: #818cf8"></div> <div style="background-color: #6366f1"></div>
//! <div style="background-color: #4f46e5"></div> <div style="background-color: #4338ca"></div>
//! <div style="background-color: #3730a3"></div> <div style="background-color: #312e81"></div>
//! <div style="background-color: #1e1b4b"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`VIOLET`]</div>
//! <div style="background-color: #f5f3ff"></div> <div style="background-color: #ede9fe"></div>
//! <div style="background-color: #ddd6fe"></div> <div style="background-color: #c4b5fd"></div>
//! <div style="background-color: #a78bfa"></div> <div style="background-color: #8b5cf6"></div>
//! <div style="background-color: #7c3aed"></div> <div style="background-color: #6d28d9"></div>
//! <div style="background-color: #5b21b6"></div> <div style="background-color: #4c1d95"></div>
//! <div style="background-color: #2e1065"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`PURPLE`]</div>
//! <div style="background-color: #faf5ff"></div> <div style="background-color: #f3e8ff"></div>
//! <div style="background-color: #e9d5ff"></div> <div style="background-color: #d8b4fe"></div>
//! <div style="background-color: #c084fc"></div> <div style="background-color: #a855f7"></div>
//! <div style="background-color: #9333ea"></div> <div style="background-color: #7e22ce"></div>
//! <div style="background-color: #6b21a8"></div> <div style="background-color: #581c87"></div>
//! <div style="background-color: #4c136e"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`FUCHSIA`]</div>
//! <div style="background-color: #fdf4ff"></div> <div style="background-color: #fae8ff"></div>
//! <div style="background-color: #f5d0fe"></div> <div style="background-color: #f0abfc"></div>
//! <div style="background-color: #e879f9"></div> <div style="background-color: #d946ef"></div>
//! <div style="background-color: #c026d3"></div> <div style="background-color: #a21caf"></div>
//! <div style="background-color: #86198f"></div> <div style="background-color: #701a75"></div>
//! <div style="background-color: #4e145b"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`PINK`]</div>
//! <div style="background-color: #fdf2f8"></div> <div style="background-color: #fce7f3"></div>
//! <div style="background-color: #fbcfe8"></div> <div style="background-color: #f9a8d4"></div>
//! <div style="background-color: #f472b6"></div> <div style="background-color: #ec4899"></div>
//! <div style="background-color: #db2777"></div> <div style="background-color: #be185d"></div>
//! <div style="background-color: #9d174d"></div> <div style="background-color: #831843"></div>
//! <div style="background-color: #5f0b37"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`BLACK`]</div>
//! <div style="background-color: #000000; width:22rem"></div>
//! </div>
//! <div class="color">
//! <div class="name">
//!
//! [`WHITE`]</div>
//! <div style="background-color: #ffffff; width:22rem"></div>
//! </div>
//! </div>
//! </div>
//!
//! # Example
//!
//! ```rust
//! # use ratatui::prelude::*;
//! use ratatui::style::palette::tailwind::{BLUE, RED};
//!
//! assert_eq!(RED.c500, Color::Rgb(239, 68, 68));
//! assert_eq!(BLUE.c500, Color::Rgb(59, 130, 246));
//! ```
use crate::prelude::*;
pub struct Palette {
pub c50: Color,
pub c100: Color,
pub c200: Color,
pub c300: Color,
pub c400: Color,
pub c500: Color,
pub c600: Color,
pub c700: Color,
pub c800: Color,
pub c900: Color,
pub c950: Color,
}
#[rustfmt::skip]
/// <style>.palette div{width:22rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #000000"></div></div>
pub const BLACK: Color = Color::from_u32(0x000000);
#[rustfmt::skip]
/// <style>.palette div{width:22rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #ffffff"></div></div>
pub const WHITE: Color = Color::from_u32(0xffffff);
#[rustfmt::skip]
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #f8fafc"></div><div style="background-color: #f1f5f9"></div><div style="background-color: #e2e8f0"></div><div style="background-color: #cbd5e1"></div><div style="background-color: #94a3b8"></div><div style="background-color: #64748b"></div><div style="background-color: #475569"></div><div style="background-color: #334155"></div><div style="background-color: #1e293b"></div><div style="background-color: #0f172a"></div><div style="background-color: #020617"></div></div>
pub const SLATE: Palette = Palette {
c50: Color::from_u32(0xf8fafc),
c100: Color::from_u32(0xf1f5f9),
c200: Color::from_u32(0xe2e8f0),
c300: Color::from_u32(0xcbd5e1),
c400: Color::from_u32(0x94a3b8),
c500: Color::from_u32(0x64748b),
c600: Color::from_u32(0x475569),
c700: Color::from_u32(0x334155),
c800: Color::from_u32(0x1e293b),
c900: Color::from_u32(0x0f172a),
c950: Color::from_u32(0x020617),
};
#[rustfmt::skip]
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #f9fafb"></div><div style="background-color: #f3f4f6"></div><div style="background-color: #e5e7eb"></div><div style="background-color: #d1d5db"></div><div style="background-color: #9ca3af"></div><div style="background-color: #6b7280"></div><div style="background-color: #4b5563"></div><div style="background-color: #374151"></div><div style="background-color: #1f2937"></div><div style="background-color: #111827"></div><div style="background-color: #030712"></div></div>
pub const GRAY: Palette = Palette {
c50: Color::from_u32(0xf9fafb),
c100: Color::from_u32(0xf3f4f6),
c200: Color::from_u32(0xe5e7eb),
c300: Color::from_u32(0xd1d5db),
c400: Color::from_u32(0x9ca3af),
c500: Color::from_u32(0x6b7280),
c600: Color::from_u32(0x4b5563),
c700: Color::from_u32(0x374151),
c800: Color::from_u32(0x1f2937),
c900: Color::from_u32(0x111827),
c950: Color::from_u32(0x030712),
};
#[rustfmt::skip]
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #fafafa"></div><div style="background-color: #f5f5f5"></div><div style="background-color: #e5e5e5"></div><div style="background-color: #d4d4d4"></div><div style="background-color: #a1a1aa"></div><div style="background-color: #71717a"></div><div style="background-color: #52525b"></div><div style="background-color: #404040"></div><div style="background-color: #262626"></div><div style="background-color: #171717"></div><div style="background-color: #09090b"></div></div>
pub const ZINC: Palette = Palette {
c50: Color::from_u32(0xfafafa),
c100: Color::from_u32(0xf4f4f5),
c200: Color::from_u32(0xe4e4e7),
c300: Color::from_u32(0xd4d4d8),
c400: Color::from_u32(0xa1a1aa),
c500: Color::from_u32(0x71717a),
c600: Color::from_u32(0x52525b),
c700: Color::from_u32(0x3f3f46),
c800: Color::from_u32(0x27272a),
c900: Color::from_u32(0x18181b),
c950: Color::from_u32(0x09090b),
};
#[rustfmt::skip]
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #fafafa"></div><div style="background-color: #f5f5f5"></div><div style="background-color: #e5e5e5"></div><div style="background-color: #d4d4d4"></div><div style="background-color: #a3a3a3"></div><div style="background-color: #737373"></div><div style="background-color: #525252"></div><div style="background-color: #404040"></div><div style="background-color: #262626"></div><div style="background-color: #171717"></div><div style="background-color: #0a0a0a"></div></div>
pub const NEUTRAL: Palette = Palette {
c50: Color::from_u32(0xfafafa),
c100: Color::from_u32(0xf5f5f5),
c200: Color::from_u32(0xe5e5e5),
c300: Color::from_u32(0xd4d4d4),
c400: Color::from_u32(0xa3a3a3),
c500: Color::from_u32(0x737373),
c600: Color::from_u32(0x525252),
c700: Color::from_u32(0x404040),
c800: Color::from_u32(0x262626),
c900: Color::from_u32(0x171717),
c950: Color::from_u32(0x0a0a0a),
};
#[rustfmt::skip]
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #fafaf9"></div><div style="background-color: #f5f5f4"></div><div style="background-color: #e7e5e4"></div><div style="background-color: #d6d3d1"></div><div style="background-color: #a8a29e"></div><div style="background-color: #78716c"></div><div style="background-color: #57534e"></div><div style="background-color: #44403c"></div><div style="background-color: #292524"></div><div style="background-color: #1c1917"></div><div style="background-color: #0c0a09"></div></div>
pub const STONE: Palette = Palette {
c50: Color::from_u32(0xfafaf9),
c100: Color::from_u32(0xf5f5f4),
c200: Color::from_u32(0xe7e5e4),
c300: Color::from_u32(0xd6d3d1),
c400: Color::from_u32(0xa8a29e),
c500: Color::from_u32(0x78716c),
c600: Color::from_u32(0x57534e),
c700: Color::from_u32(0x44403c),
c800: Color::from_u32(0x292524),
c900: Color::from_u32(0x1c1917),
c950: Color::from_u32(0x0c0a09),
};
#[rustfmt::skip]
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #fef2f2"></div><div style="background-color: #fee2e2"></div><div style="background-color: #fecaca"></div><div style="background-color: #fca5a5"></div><div style="background-color: #f87171"></div><div style="background-color: #ef4444"></div><div style="background-color: #dc2626"></div><div style="background-color: #b91c1c"></div><div style="background-color: #991b1b"></div><div style="background-color: #7f1d1d"></div><div style="background-color: #450a0a"></div></div>
pub const RED: Palette = Palette {
c50: Color::from_u32(0xfef2f2),
c100: Color::from_u32(0xfee2e2),
c200: Color::from_u32(0xfecaca),
c300: Color::from_u32(0xfca5a5),
c400: Color::from_u32(0xf87171),
c500: Color::from_u32(0xef4444),
c600: Color::from_u32(0xdc2626),
c700: Color::from_u32(0xb91c1c),
c800: Color::from_u32(0x991b1b),
c900: Color::from_u32(0x7f1d1d),
c950: Color::from_u32(0x450a0a),
};
#[rustfmt::skip]
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #fff7ed"></div><div style="background-color: #ffedd5"></div><div style="background-color: #fed7aa"></div><div style="background-color: #fdba74"></div><div style="background-color: #fb923c"></div><div style="background-color: #f97316"></div><div style="background-color: #ea580c"></div><div style="background-color: #c2410c"></div><div style="background-color: #9a3412"></div><div style="background-color: #7c2d12"></div><div style="background-color: #431407"></div></div>
pub const ORANGE: Palette = Palette {
c50: Color::from_u32(0xfff7ed),
c100: Color::from_u32(0xffedd5),
c200: Color::from_u32(0xfed7aa),
c300: Color::from_u32(0xfdba74),
c400: Color::from_u32(0xfb923c),
c500: Color::from_u32(0xf97316),
c600: Color::from_u32(0xea580c),
c700: Color::from_u32(0xc2410c),
c800: Color::from_u32(0x9a3412),
c900: Color::from_u32(0x7c2d12),
c950: Color::from_u32(0x431407),
};
#[rustfmt::skip]
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #fffbeb"></div><div style="background-color: #fef3c7"></div><div style="background-color: #fde68a"></div><div style="background-color: #fcd34d"></div><div style="background-color: #fbbf24"></div><div style="background-color: #f59e0b"></div><div style="background-color: #d97706"></div><div style="background-color: #b45309"></div><div style="background-color: #92400e"></div><div style="background-color: #78350f"></div><div style="background-color: #451a03"></div></div>
pub const AMBER: Palette = Palette {
c50: Color::from_u32(0xfffbeb),
c100: Color::from_u32(0xfef3c7),
c200: Color::from_u32(0xfde68a),
c300: Color::from_u32(0xfcd34d),
c400: Color::from_u32(0xfbbf24),
c500: Color::from_u32(0xf59e0b),
c600: Color::from_u32(0xd97706),
c700: Color::from_u32(0xb45309),
c800: Color::from_u32(0x92400e),
c900: Color::from_u32(0x78350f),
c950: Color::from_u32(0x451a03),
};
#[rustfmt::skip]
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #fefce8"></div><div style="background-color: #fef9c3"></div><div style="background-color: #fef08a"></div><div style="background-color: #fde047"></div><div style="background-color: #facc15"></div><div style="background-color: #eab308"></div><div style="background-color: #ca8a04"></div><div style="background-color: #a16207"></div><div style="background-color: #854d0e"></div><div style="background-color: #713f12"></div><div style="background-color: #422006"></div></div>
pub const YELLOW: Palette = Palette {
c50: Color::from_u32(0xfefce8),
c100: Color::from_u32(0xfef9c3),
c200: Color::from_u32(0xfef08a),
c300: Color::from_u32(0xfde047),
c400: Color::from_u32(0xfacc15),
c500: Color::from_u32(0xeab308),
c600: Color::from_u32(0xca8a04),
c700: Color::from_u32(0xa16207),
c800: Color::from_u32(0x854d0e),
c900: Color::from_u32(0x713f12),
c950: Color::from_u32(0x422006),
};
#[rustfmt::skip]
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #f7fee7"></div><div style="background-color: #ecfccb"></div><div style="background-color: #d9f99d"></div><div style="background-color: #bef264"></div><div style="background-color: #a3e635"></div><div style="background-color: #84cc16"></div><div style="background-color: #65a30d"></div><div style="background-color: #4d7c0f"></div><div style="background-color: #3f6212"></div><div style="background-color: #365314"></div><div style="background-color: #1a2e05"></div></div>
pub const LIME: Palette = Palette {
c50: Color::from_u32(0xf7fee7),
c100: Color::from_u32(0xecfccb),
c200: Color::from_u32(0xd9f99d),
c300: Color::from_u32(0xbef264),
c400: Color::from_u32(0xa3e635),
c500: Color::from_u32(0x84cc16),
c600: Color::from_u32(0x65a30d),
c700: Color::from_u32(0x4d7c0f),
c800: Color::from_u32(0x3f6212),
c900: Color::from_u32(0x365314),
c950: Color::from_u32(0x1a2e05),
};
#[rustfmt::skip]
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #f0fdf4"></div><div style="background-color: #dcfce7"></div><div style="background-color: #bbf7d0"></div><div style="background-color: #86efac"></div><div style="background-color: #4ade80"></div><div style="background-color: #22c55e"></div><div style="background-color: #16a34a"></div><div style="background-color: #15803d"></div><div style="background-color: #166534"></div><div style="background-color: #14532d"></div><div style="background-color: #052e16"></div></div>
pub const GREEN: Palette = Palette {
c50: Color::from_u32(0xf0fdf4),
c100: Color::from_u32(0xdcfce7),
c200: Color::from_u32(0xbbf7d0),
c300: Color::from_u32(0x86efac),
c400: Color::from_u32(0x4ade80),
c500: Color::from_u32(0x22c55e),
c600: Color::from_u32(0x16a34a),
c700: Color::from_u32(0x15803d),
c800: Color::from_u32(0x166534),
c900: Color::from_u32(0x14532d),
c950: Color::from_u32(0x052e16),
};
#[rustfmt::skip]
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #f0fdfa"></div><div style="background-color: #ccfbf1"></div><div style="background-color: #99f6e4"></div><div style="background-color: #5eead4"></div><div style="background-color: #2dd4bf"></div><div style="background-color: #14b8a6"></div><div style="background-color: #0d9488"></div><div style="background-color: #0f766e"></div><div style="background-color: #115e59"></div><div style="background-color: #134e4a"></div><div style="background-color: #042f2e"></div></div>
pub const EMERALD: Palette = Palette {
c50: Color::from_u32(0xecfdf5),
c100: Color::from_u32(0xd1fae5),
c200: Color::from_u32(0xa7f3d0),
c300: Color::from_u32(0x6ee7b7),
c400: Color::from_u32(0x34d399),
c500: Color::from_u32(0x10b981),
c600: Color::from_u32(0x059669),
c700: Color::from_u32(0x047857),
c800: Color::from_u32(0x065f46),
c900: Color::from_u32(0x064e3b),
c950: Color::from_u32(0x022c22),
};
#[rustfmt::skip]
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #f5fdf4"></div><div style="background-color: #e7f9e7"></div><div style="background-color: #c6f6d5"></div><div style="background-color: #9ae6b4"></div><div style="background-color: #68d391"></div><div style="background-color: #48bb78"></div><div style="background-color: #38a169"></div><div style="background-color: #2f855a"></div><div style="background-color: #276749"></div><div style="background-color: #22543d"></div><div style="background-color: #0d3321"></div></div>
pub const TEAL: Palette = Palette {
c50: Color::from_u32(0xf0fdfa),
c100: Color::from_u32(0xccfbf1),
c200: Color::from_u32(0x99f6e4),
c300: Color::from_u32(0x5eead4),
c400: Color::from_u32(0x2dd4bf),
c500: Color::from_u32(0x14b8a6),
c600: Color::from_u32(0x0d9488),
c700: Color::from_u32(0x0f766e),
c800: Color::from_u32(0x115e59),
c900: Color::from_u32(0x134e4a),
c950: Color::from_u32(0x042f2e),
};
#[rustfmt::skip]
/// <style>.palette div{width:2rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #ecfeff"></div><div style="background-color: #cffafe"></div><div style="background-color: #a5f3fc"></div><div style="background-color: #67e8f9"></div><div style="background-color: #22d3ee"></div><div style="background-color: #06b6d4"></div><div style="background-color: #0891b2"></div><div style="background-color: #0e7490"></div><div style="background-color: #155e75"></div><div style="background-color: #164e63"></div><div style="background-color: #083344"></div></div>
pub const CYAN: Palette = Palette {
c50: Color::from_u32(0xecfeff),
c100: Color::from_u32(0xcffafe),
c200: Color::from_u32(0xa5f3fc),
c300: Color::from_u32(0x67e8f9),
c400: Color::from_u32(0x22d3ee),
c500: Color::from_u32(0x06b6d4),
c600: Color::from_u32(0x0891b2),
c700: Color::from_u32(0x0e7490),
c800: Color::from_u32(0x155e75),
c900: Color::from_u32(0x164e63),
c950: Color::from_u32(0x083344),
};
#[rustfmt::skip]
/// <style>.palette div{width:22rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #f0f9ff"></div><div style="background-color: #e0f2fe"></div><div style="background-color: #bae6fd"></div><div style="background-color: #7dd3fc"></div><div style="background-color: #38bdf8"></div><div style="background-color: #0ea5e9"></div><div style="background-color: #0284c7"></div><div style="background-color: #0369a1"></div><div style="background-color: #075985"></div><div style="background-color: #0c4a6e"></div><div style="background-color: #082f49"></div></div>
pub const SKY: Palette = Palette {
c50: Color::from_u32(0xf0f9ff),
c100: Color::from_u32(0xe0f2fe),
c200: Color::from_u32(0xbae6fd),
c300: Color::from_u32(0x7dd3fc),
c400: Color::from_u32(0x38bdf8),
c500: Color::from_u32(0x0ea5e9),
c600: Color::from_u32(0x0284c7),
c700: Color::from_u32(0x0369a1),
c800: Color::from_u32(0x075985),
c900: Color::from_u32(0x0c4a6e),
c950: Color::from_u32(0x082f49),
};
#[rustfmt::skip]
/// <style>.palette div{width:22rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #eff6ff"></div><div style="background-color: #dbeafe"></div><div style="background-color: #bfdbfe"></div><div style="background-color: #93c5fd"></div><div style="background-color: #60a5fa"></div><div style="background-color: #3b82f6"></div><div style="background-color: #2563eb"></div><div style="background-color: #1d4ed8"></div><div style="background-color: #1e40af"></div><div style="background-color: #1e3a8a"></div><div style="background-color: #172554"></div></div>
pub const BLUE: Palette = Palette {
c50: Color::from_u32(0xeff6ff),
c100: Color::from_u32(0xdbeafe),
c200: Color::from_u32(0xbfdbfe),
c300: Color::from_u32(0x93c5fd),
c400: Color::from_u32(0x60a5fa),
c500: Color::from_u32(0x3b82f6),
c600: Color::from_u32(0x2563eb),
c700: Color::from_u32(0x1d4ed8),
c800: Color::from_u32(0x1e40af),
c900: Color::from_u32(0x1e3a8a),
c950: Color::from_u32(0x172554),
};
#[rustfmt::skip]
/// <style>.palette div{width:22rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #eef2ff"></div><div style="background-color: #e0e7ff"></div><div style="background-color: #c7d2fe"></div><div style="background-color: #a5b4fc"></div><div style="background-color: #818cf8"></div><div style="background-color: #6366f1"></div><div style="background-color: #4f46e5"></div><div style="background-color: #4338ca"></div><div style="background-color: #3730a3"></div><div style="background-color: #312e81"></div><div style="background-color: #1e1b4b"></div></div>
pub const INDIGO: Palette = Palette {
c50: Color::from_u32(0xeef2ff),
c100: Color::from_u32(0xe0e7ff),
c200: Color::from_u32(0xc7d2fe),
c300: Color::from_u32(0xa5b4fc),
c400: Color::from_u32(0x818cf8),
c500: Color::from_u32(0x6366f1),
c600: Color::from_u32(0x4f46e5),
c700: Color::from_u32(0x4338ca),
c800: Color::from_u32(0x3730a3),
c900: Color::from_u32(0x312e81),
c950: Color::from_u32(0x1e1b4b),
};
#[rustfmt::skip]
/// <style>.palette div{width:22rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #f5f3ff"></div><div style="background-color: #ede9fe"></div><div style="background-color: #ddd6fe"></div><div style="background-color: #c4b5fd"></div><div style="background-color: #a78bfa"></div><div style="background-color: #8b5cf6"></div><div style="background-color: #7c3aed"></div><div style="background-color: #6d28d9"></div><div style="background-color: #5b21b6"></div><div style="background-color: #4c1d95"></div><div style="background-color: #2e1065"></div></div>
pub const VIOLET: Palette = Palette {
c50: Color::from_u32(0xf5f3ff),
c100: Color::from_u32(0xede9fe),
c200: Color::from_u32(0xddd6fe),
c300: Color::from_u32(0xc4b5fd),
c400: Color::from_u32(0xa78bfa),
c500: Color::from_u32(0x8b5cf6),
c600: Color::from_u32(0x7c3aed),
c700: Color::from_u32(0x6d28d9),
c800: Color::from_u32(0x5b21b6),
c900: Color::from_u32(0x4c1d95),
c950: Color::from_u32(0x2e1065),
};
#[rustfmt::skip]
/// <style>.palette div{width:22rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #faf5ff"></div><div style="background-color: #f3e8ff"></div><div style="background-color: #e9d5ff"></div><div style="background-color: #d8b4fe"></div><div style="background-color: #c084fc"></div><div style="background-color: #a855f7"></div><div style="background-color: #9333ea"></div><div style="background-color: #7e22ce"></div><div style="background-color: #6b21a8"></div><div style="background-color: #581c87"></div><div style="background-color: #3b0764"></div></div>
pub const PURPLE: Palette = Palette {
c50: Color::from_u32(0xfaf5ff),
c100: Color::from_u32(0xf3e8ff),
c200: Color::from_u32(0xe9d5ff),
c300: Color::from_u32(0xd8b4fe),
c400: Color::from_u32(0xc084fc),
c500: Color::from_u32(0xa855f7),
c600: Color::from_u32(0x9333ea),
c700: Color::from_u32(0x7e22ce),
c800: Color::from_u32(0x6b21a8),
c900: Color::from_u32(0x581c87),
c950: Color::from_u32(0x3b0764),
};
#[rustfmt::skip]
/// <style>.palette div{width:22rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #fdf4ff"></div><div style="background-color: #fae8ff"></div><div style="background-color: #f5d0fe"></div><div style="background-color: #f0abfc"></div><div style="background-color: #e879f9"></div><div style="background-color: #d946ef"></div><div style="background-color: #c026d3"></div><div style="background-color: #a21caf"></div><div style="background-color: #86198f"></div><div style="background-color: #701a75"></div><div style="background-color: #4a044e"></div></div>
pub const FUCHSIA: Palette = Palette {
c50: Color::from_u32(0xfdf4ff),
c100: Color::from_u32(0xfae8ff),
c200: Color::from_u32(0xf5d0fe),
c300: Color::from_u32(0xf0abfc),
c400: Color::from_u32(0xe879f9),
c500: Color::from_u32(0xd946ef),
c600: Color::from_u32(0xc026d3),
c700: Color::from_u32(0xa21caf),
c800: Color::from_u32(0x86198f),
c900: Color::from_u32(0x701a75),
c950: Color::from_u32(0x4a044e),
};
#[rustfmt::skip]
/// <style>.palette div{width:22rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #fdf2f8"></div><div style="background-color: #fce7f3"></div><div style="background-color: #fbcfe8"></div><div style="background-color: #f9a8d4"></div><div style="background-color: #f472b6"></div><div style="background-color: #ec4899"></div><div style="background-color: #db2777"></div><div style="background-color: #be185d"></div><div style="background-color: #9d174d"></div><div style="background-color: #831843"></div><div style="background-color: #500724"></div></div>
pub const PINK: Palette = Palette {
c50: Color::from_u32(0xfdf2f8),
c100: Color::from_u32(0xfce7f3),
c200: Color::from_u32(0xfbcfe8),
c300: Color::from_u32(0xf9a8d4),
c400: Color::from_u32(0xf472b6),
c500: Color::from_u32(0xec4899),
c600: Color::from_u32(0xdb2777),
c700: Color::from_u32(0xbe185d),
c800: Color::from_u32(0x9d174d),
c900: Color::from_u32(0x831843),
c950: Color::from_u32(0x500724),
};
#[rustfmt::skip]
/// <style>.palette div{width:22rem;height:2rem}</style><div class="palette" style="display:flex;flex-direction:row"><div style="background-color: #fff1f2"></div><div style="background-color: #ffe4e6"></div><div style="background-color: #fecdd3"></div><div style="background-color: #fda4af"></div><div style="background-color: #fb7185"></div><div style="background-color: #f43f5e"></div><div style="background-color: #e11d48"></div><div style="background-color: #be123c"></div><div style="background-color: #9f1239"></div><div style="background-color: #881337"></div><div style="background-color: #4c0519"></div></div>
pub const ROSE: Palette = Palette {
c50: Color::from_u32(0xfff1f2),
c100: Color::from_u32(0xffe4e6),
c200: Color::from_u32(0xfecdd3),
c300: Color::from_u32(0xfda4af),
c400: Color::from_u32(0xfb7185),
c500: Color::from_u32(0xf43f5e),
c600: Color::from_u32(0xe11d48),
c700: Color::from_u32(0xbe123c),
c800: Color::from_u32(0x9f1239),
c900: Color::from_u32(0x881337),
c950: Color::from_u32(0x4c0519),
};

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()`,
@@ -127,7 +133,11 @@ 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"]
@@ -205,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)
}
}
@@ -217,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());
@@ -29,659 +30,12 @@
//! [`backend`]: crate::backend
//! [`Backend`]: crate::backend::Backend
//! [`Buffer`]: crate::buffer::Buffer
use std::{fmt, io};
use crate::{
backend::{Backend, ClearType},
buffer::Buffer,
layout::Rect,
widgets::{StatefulWidget, Widget},
};
mod frame;
#[allow(clippy::module_inception)]
mod terminal;
mod viewport;
/// Represents the viewport of the terminal. The viewport is the area of the terminal that is
/// currently visible to the user. It can be either fullscreen, inline or fixed.
///
/// When the viewport is fullscreen, the whole terminal is used to draw the application.
///
/// When the viewport is inline, it is drawn inline with the rest of the terminal. The height of
/// the viewport is fixed, but the width is the same as the terminal width.
///
/// When the viewport is fixed, it is drawn in a fixed area of the terminal. The area is specified
/// by a [`Rect`].
///
/// See [`Terminal::with_options`] for more information.
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub enum Viewport {
/// The viewport is fullscreen
#[default]
Fullscreen,
/// The viewport is inline with the rest of the terminal.
///
/// The viewport's height is fixed and specified in number of lines. The width is the same as
/// the terminal's width. The viewport is drawn below the cursor position.
Inline(u16),
/// The viewport is drawn in a fixed area of the terminal. The area is specified by a [`Rect`].
Fixed(Rect),
}
impl fmt::Display for Viewport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Viewport::Fullscreen => write!(f, "Fullscreen"),
Viewport::Inline(height) => write!(f, "Inline({})", height),
Viewport::Fixed(area) => write!(f, "Fixed({})", area),
}
}
}
/// Options to pass to [`Terminal::with_options`]
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct TerminalOptions {
/// Viewport used to draw to the terminal
pub viewport: Viewport,
}
/// An interface to interact and draw [`Frame`]s on the user's terminal.
///
/// This is the main entry point for Ratatui. It is responsible for drawing and maintaining the
/// state of the buffers, cursor and viewport.
///
/// The [`Terminal`] is generic over a [`Backend`] implementation which is used to interface with
/// the underlying terminal library. The [`Backend`] trait is implemented for three popular Rust
/// terminal libraries: [Crossterm], [Termion] and [Termwiz]. See the [`backend`] module for more
/// information.
///
/// The `Terminal` struct maintains two buffers: the current and the previous.
/// When the widgets are drawn, the changes are accumulated in the current buffer.
/// At the end of each draw pass, the two buffers are compared, and only the changes
/// between these buffers are written to the terminal, avoiding any redundant operations.
/// After flushing these changes, the buffers are swapped to prepare for the next draw cycle./
///
/// The terminal also has a viewport which is the area of the terminal that is currently visible to
/// the user. It can be either fullscreen, inline or fixed. See [`Viewport`] for more information.
///
/// Applications should detect terminal resizes and call [`Terminal::draw`] to redraw the
/// application with the new size. This will automatically resize the internal buffers to match the
/// new size for inline and fullscreen viewports. Fixed viewports are not resized automatically.
///
/// # Examples
///
/// ```rust,no_run
/// use std::io::stdout;
/// use ratatui::{prelude::*, widgets::Paragraph};
///
/// let backend = CrosstermBackend::new(stdout());
/// let mut terminal = Terminal::new(backend)?;
/// terminal.draw(|frame| {
/// let area = frame.size();
/// frame.render_widget(Paragraph::new("Hello World!"), area);
/// frame.set_cursor(0, 0);
/// })?;
/// # std::io::Result::Ok(())
/// ```
///
/// [Crossterm]: https://crates.io/crates/crossterm
/// [Termion]: https://crates.io/crates/termion
/// [Termwiz]: https://crates.io/crates/termwiz
/// [`backend`]: crate::backend
/// [`Backend`]: crate::backend::Backend
/// [`Buffer`]: crate::buffer::Buffer
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Terminal<B>
where
B: Backend,
{
/// The backend used to interface with the terminal
backend: B,
/// Holds the results of the current and previous draw calls. The two are compared at the end
/// of each draw pass to output the necessary updates to the terminal
buffers: [Buffer; 2],
/// Index of the current buffer in the previous array
current: usize,
/// Whether the cursor is currently hidden
hidden_cursor: bool,
/// Viewport
viewport: Viewport,
/// Area of the viewport
viewport_area: Rect,
/// Last known size of the terminal. Used to detect if the internal buffers have to be resized.
last_known_size: Rect,
/// Last known position of the cursor. Used to find the new area when the viewport is inlined
/// and the terminal resized.
last_known_cursor_pos: (u16, u16),
}
impl<B> Drop for Terminal<B>
where
B: Backend,
{
fn drop(&mut self) {
// Attempt to restore the cursor state
if self.hidden_cursor {
if let Err(err) = self.show_cursor() {
eprintln!("Failed to show the cursor: {err}");
}
}
}
}
impl<B> Terminal<B>
where
B: Backend,
{
/// Creates a new [`Terminal`] with the given [`Backend`] with a full screen viewport.
///
/// # Example
///
/// ```rust,no_run
/// # use std::io::stdout;
/// # use ratatui::prelude::*;
/// let backend = CrosstermBackend::new(stdout());
/// let terminal = Terminal::new(backend)?;
/// # std::io::Result::Ok(())
/// ```
pub fn new(backend: B) -> io::Result<Terminal<B>> {
Terminal::with_options(
backend,
TerminalOptions {
viewport: Viewport::Fullscreen,
},
)
}
/// Creates a new [`Terminal`] with the given [`Backend`] and [`TerminalOptions`].
///
/// # Example
///
/// ```rust
/// # use std::io::stdout;
/// # use ratatui::{prelude::*, backend::TestBackend};
/// let backend = CrosstermBackend::new(stdout());
/// let viewport = Viewport::Fixed(Rect::new(0, 0, 10, 10));
/// let terminal = Terminal::with_options(
/// backend,
/// TerminalOptions { viewport },
/// )?;
/// # std::io::Result::Ok(())
/// ```
pub fn with_options(mut backend: B, options: TerminalOptions) -> io::Result<Terminal<B>> {
let size = match options.viewport {
Viewport::Fullscreen | Viewport::Inline(_) => backend.size()?,
Viewport::Fixed(area) => area,
};
let (viewport_area, cursor_pos) = match options.viewport {
Viewport::Fullscreen => (size, (0, 0)),
Viewport::Inline(height) => compute_inline_size(&mut backend, height, size, 0)?,
Viewport::Fixed(area) => (area, (area.left(), area.top())),
};
Ok(Terminal {
backend,
buffers: [Buffer::empty(viewport_area), Buffer::empty(viewport_area)],
current: 0,
hidden_cursor: false,
viewport: options.viewport,
viewport_area,
last_known_size: size,
last_known_cursor_pos: cursor_pos,
})
}
/// Get a Frame object which provides a consistent view into the terminal state for rendering.
pub fn get_frame(&mut self) -> Frame {
Frame {
cursor_position: None,
viewport_area: self.viewport_area,
buffer: self.current_buffer_mut(),
}
}
/// Gets the current buffer as a mutable reference.
pub fn current_buffer_mut(&mut self) -> &mut Buffer {
&mut self.buffers[self.current]
}
/// Gets the backend
pub fn backend(&self) -> &B {
&self.backend
}
/// Gets the backend as a mutable reference
pub fn backend_mut(&mut self) -> &mut B {
&mut self.backend
}
/// Obtains a difference between the previous and the current buffer and passes it to the
/// current backend for drawing.
pub fn flush(&mut self) -> io::Result<()> {
let previous_buffer = &self.buffers[1 - self.current];
let current_buffer = &self.buffers[self.current];
let updates = previous_buffer.diff(current_buffer);
if let Some((col, row, _)) = updates.last() {
self.last_known_cursor_pos = (*col, *row);
}
self.backend.draw(updates.into_iter())
}
/// Updates the Terminal so that internal buffers match the requested size.
///
/// Requested size will be saved so the size can remain consistent when rendering. This leads
/// to a full clear of the screen.
pub fn resize(&mut self, size: Rect) -> io::Result<()> {
let next_area = match self.viewport {
Viewport::Fullscreen => size,
Viewport::Inline(height) => {
let offset_in_previous_viewport = self
.last_known_cursor_pos
.1
.saturating_sub(self.viewport_area.top());
compute_inline_size(&mut self.backend, height, size, offset_in_previous_viewport)?.0
}
Viewport::Fixed(area) => area,
};
self.set_viewport_area(next_area);
self.clear()?;
self.last_known_size = size;
Ok(())
}
fn set_viewport_area(&mut self, area: Rect) {
self.buffers[self.current].resize(area);
self.buffers[1 - self.current].resize(area);
self.viewport_area = area;
}
/// Queries the backend for size and resizes if it doesn't match the previous size.
pub fn autoresize(&mut self) -> io::Result<()> {
// fixed viewports do not get autoresized
if matches!(self.viewport, Viewport::Fullscreen | Viewport::Inline(_)) {
let size = self.size()?;
if size != self.last_known_size {
self.resize(size)?;
}
};
Ok(())
}
/// Synchronizes terminal size, calls the rendering closure, flushes the current internal state
/// and prepares for the next draw call.
///
/// This is the main entry point for drawing to the terminal.
///
/// # Examples
///
/// ```rust,no_run
/// # use std::io::stdout;
/// # use ratatui::{prelude::*, widgets::Paragraph};
/// let backend = CrosstermBackend::new(stdout());
/// let mut terminal = Terminal::new(backend)?;
/// terminal.draw(|frame| {
/// let area = frame.size();
/// frame.render_widget(Paragraph::new("Hello World!"), area);
/// frame.set_cursor(0, 0);
/// })?;
/// # std::io::Result::Ok(())
/// ```
pub fn draw<F>(&mut self, f: F) -> io::Result<CompletedFrame>
where
F: FnOnce(&mut Frame),
{
// Autoresize - otherwise we get glitches if shrinking or potential desync between widgets
// and the terminal (if growing), which may OOB.
self.autoresize()?;
let mut frame = self.get_frame();
f(&mut frame);
// We can't change the cursor position right away because we have to flush the frame to
// stdout first. But we also can't keep the frame around, since it holds a &mut to
// Buffer. Thus, we're taking the important data out of the Frame and dropping it.
let cursor_position = frame.cursor_position;
// Draw to stdout
self.flush()?;
match cursor_position {
None => self.hide_cursor()?,
Some((x, y)) => {
self.show_cursor()?;
self.set_cursor(x, y)?;
}
}
self.swap_buffers();
// Flush
self.backend.flush()?;
Ok(CompletedFrame {
buffer: &self.buffers[1 - self.current],
area: self.last_known_size,
})
}
/// Hides the cursor.
pub fn hide_cursor(&mut self) -> io::Result<()> {
self.backend.hide_cursor()?;
self.hidden_cursor = true;
Ok(())
}
/// Shows the cursor.
pub fn show_cursor(&mut self) -> io::Result<()> {
self.backend.show_cursor()?;
self.hidden_cursor = false;
Ok(())
}
/// Gets the current cursor position.
///
/// This is the position of the cursor after the last draw call and is returned as a tuple of
/// `(x, y)` coordinates.
pub fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
self.backend.get_cursor()
}
/// Sets the cursor position.
pub fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
self.backend.set_cursor(x, y)?;
self.last_known_cursor_pos = (x, y);
Ok(())
}
/// Clear the terminal and force a full redraw on the next draw call.
pub fn clear(&mut self) -> io::Result<()> {
match self.viewport {
Viewport::Fullscreen => self.backend.clear_region(ClearType::All)?,
Viewport::Inline(_) => {
self.backend
.set_cursor(self.viewport_area.left(), self.viewport_area.top())?;
self.backend.clear_region(ClearType::AfterCursor)?;
}
Viewport::Fixed(area) => {
for row in area.top()..area.bottom() {
self.backend.set_cursor(0, row)?;
self.backend.clear_region(ClearType::AfterCursor)?;
}
}
}
// Reset the back buffer to make sure the next update will redraw everything.
self.buffers[1 - self.current].reset();
Ok(())
}
/// Clears the inactive buffer and swaps it with the current buffer
pub fn swap_buffers(&mut self) {
self.buffers[1 - self.current].reset();
self.current = 1 - self.current;
}
/// Queries the real size of the backend.
pub fn size(&self) -> io::Result<Rect> {
self.backend.size()
}
/// Insert some content before the current inline viewport. This has no effect when the
/// viewport is fullscreen.
///
/// This function scrolls down the current viewport by the given height. The newly freed space
/// is then made available to the `draw_fn` closure through a writable `Buffer`.
///
/// Before:
/// ```ignore
/// +-------------------+
/// | |
/// | viewport |
/// | |
/// +-------------------+
/// ```
///
/// After:
/// ```ignore
/// +-------------------+
/// | buffer |
/// +-------------------+
/// +-------------------+
/// | |
/// | viewport |
/// | |
/// +-------------------+
/// ```
///
/// # Examples
///
/// ## Insert a single line before the current viewport
///
/// ```rust
/// # use ratatui::{backend::TestBackend, prelude::*, widgets::*};
/// # let backend = TestBackend::new(10, 10);
/// # let mut terminal = Terminal::new(backend).unwrap();
/// terminal.insert_before(1, |buf| {
/// 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);
/// });
/// ```
pub fn insert_before<F>(&mut self, height: u16, draw_fn: F) -> io::Result<()>
where
F: FnOnce(&mut Buffer),
{
if !matches!(self.viewport, Viewport::Inline(_)) {
return Ok(());
}
// Clear the viewport off the screen
self.clear()?;
// 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: 0,
width: self.viewport_area.width,
height,
};
let mut buffer = Buffer::empty(area);
draw_fn(&mut buffer);
// 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;
self.backend
.append_lines(self.viewport_area.height.saturating_sub(1) + chunk_size)?;
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(())
}
}
fn compute_inline_size<B: Backend>(
backend: &mut B,
height: u16,
size: Rect,
offset_in_previous_viewport: u16,
) -> io::Result<(Rect, (u16, u16))> {
let pos = backend.get_cursor()?;
let mut row = pos.1;
let max_height = size.height.min(height);
let lines_after_cursor = height
.saturating_sub(offset_in_previous_viewport)
.saturating_sub(1);
backend.append_lines(lines_after_cursor)?;
let available_lines = size.height.saturating_sub(row).saturating_sub(1);
let missing_lines = lines_after_cursor.saturating_sub(available_lines);
if missing_lines > 0 {
row = row.saturating_sub(missing_lines);
}
row = row.saturating_sub(offset_in_previous_viewport);
Ok((
Rect {
x: 0,
y: row,
width: size.width,
height: max_height,
},
pos,
))
}
/// A consistent view into the terminal state for rendering a single frame.
///
/// This is obtained via the closure argument of [`Terminal::draw`]. It is used to render widgets
/// to the terminal and control the cursor position.
///
/// The changes drawn to the frame are applied only to the current [`Buffer`].
/// After the closure returns, the current buffer is compared to the previous
/// buffer and only the changes are applied to the terminal.
///
/// [`Buffer`]: crate::buffer::Buffer
#[derive(Debug, Hash)]
pub struct Frame<'a> {
/// Where should the cursor be after drawing this frame?
///
/// If `None`, the cursor is hidden and its position is controlled by the backend. If `Some((x,
/// y))`, the cursor is shown and placed at `(x, y)` after the call to `Terminal::draw()`.
cursor_position: Option<(u16, u16)>,
/// The area of the viewport
viewport_area: Rect,
/// The buffer that is used to draw the current frame
buffer: &'a mut Buffer,
}
impl Frame<'_> {
/// The size of the current frame
///
/// This is guaranteed not to change when rendering.
pub fn size(&self) -> Rect {
self.viewport_area
}
/// Render a [`Widget`] to the current buffer using [`Widget::render`].
///
/// Usually the area argument is the size of the current frame or a sub-area of the current
/// frame (which can be obtained using [`Layout`] to split the total area).
///
/// # Example
///
/// ```rust
/// # use ratatui::{backend::TestBackend, prelude::*, widgets::Block};
/// # let backend = TestBackend::new(5, 5);
/// # let mut terminal = Terminal::new(backend).unwrap();
/// # let mut frame = terminal.get_frame();
/// let block = Block::default();
/// let area = Rect::new(0, 0, 5, 5);
/// frame.render_widget(block, area);
/// ```
///
/// [`Layout`]: crate::layout::Layout
pub fn render_widget<W>(&mut self, widget: W, area: Rect)
where
W: Widget,
{
widget.render(area, self.buffer);
}
/// Render a [`StatefulWidget`] to the current buffer using [`StatefulWidget::render`].
///
/// Usually the area argument is the size of the current frame or a sub-area of the current
/// frame (which can be obtained using [`Layout`] to split the total area).
///
/// The last argument should be an instance of the [`StatefulWidget::State`] associated to the
/// given [`StatefulWidget`].
///
/// # Examples
///
/// ```rust
/// # use ratatui::{backend::TestBackend, prelude::*, widgets::*};
/// # let backend = TestBackend::new(5, 5);
/// # let mut terminal = Terminal::new(backend).unwrap();
/// # let mut frame = terminal.get_frame();
/// let mut state = ListState::default().with_selected(Some(1));
/// let list = List::new(vec![
/// ListItem::new("Item 1"),
/// ListItem::new("Item 2"),
/// ]);
/// let area = Rect::new(0, 0, 5, 5);
/// frame.render_stateful_widget(list, area, &mut state);
/// ```
///
/// [`Layout`]: crate::layout::Layout
pub fn render_stateful_widget<W>(&mut self, widget: W, area: Rect, state: &mut W::State)
where
W: StatefulWidget,
{
widget.render(area, self.buffer, state);
}
/// After drawing this frame, make the cursor visible and put it at the specified (x, y)
/// coordinates. If this method is not called, the cursor will be hidden.
///
/// Note that this will interfere with calls to `Terminal::hide_cursor()`,
/// `Terminal::show_cursor()`, and `Terminal::set_cursor()`. Pick one of the APIs and stick
/// with it.
pub fn set_cursor(&mut self, x: u16, y: u16) {
self.cursor_position = Some((x, y));
}
/// Gets the buffer that this `Frame` draws into as a mutable reference.
pub fn buffer_mut(&mut self) -> &mut Buffer {
self.buffer
}
}
/// `CompletedFrame` represents the state of the terminal after all changes performed in the last
/// [`Terminal::draw`] call have been applied. Therefore, it is only valid until the next call to
/// [`Terminal::draw`].
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct CompletedFrame<'a> {
/// The buffer that was used to draw the last frame.
pub buffer: &'a Buffer,
/// The size of the last frame.
pub area: Rect,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn viewport_to_string() {
assert_eq!(Viewport::Fullscreen.to_string(), "Fullscreen");
assert_eq!(Viewport::Inline(5).to_string(), "Inline(5)");
assert_eq!(
Viewport::Fixed(Rect::new(0, 0, 5, 5)).to_string(),
"Fixed(5x5+0+0)"
);
}
}
pub use frame::{CompletedFrame, Frame};
pub use terminal::{Options as TerminalOptions, Terminal};
pub use viewport::Viewport;

155
src/terminal/frame.rs Normal file
View File

@@ -0,0 +1,155 @@
use crate::{
prelude::*,
widgets::{StatefulWidget, Widget},
};
/// A consistent view into the terminal state for rendering a single frame.
///
/// This is obtained via the closure argument of [`Terminal::draw`]. It is used to render widgets
/// to the terminal and control the cursor position.
///
/// The changes drawn to the frame are applied only to the current [`Buffer`].
/// After the closure returns, the current buffer is compared to the previous
/// buffer and only the changes are applied to the terminal.
///
/// [`Buffer`]: crate::buffer::Buffer
#[derive(Debug, Hash)]
pub struct Frame<'a> {
/// Where should the cursor be after drawing this frame?
///
/// If `None`, the cursor is hidden and its position is controlled by the backend. If `Some((x,
/// y))`, the cursor is shown and placed at `(x, y)` after the call to `Terminal::draw()`.
pub(crate) cursor_position: Option<(u16, u16)>,
/// The area of the viewport
pub(crate) viewport_area: Rect,
/// The buffer that is used to draw the current frame
pub(crate) buffer: &'a mut Buffer,
/// The frame count indicating the sequence number of this frame.
pub(crate) count: usize,
}
/// `CompletedFrame` represents the state of the terminal after all changes performed in the last
/// [`Terminal::draw`] call have been applied. Therefore, it is only valid until the next call to
/// [`Terminal::draw`].
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct CompletedFrame<'a> {
/// The buffer that was used to draw the last frame.
pub buffer: &'a Buffer,
/// The size of the last frame.
pub area: Rect,
/// The frame count indicating the sequence number of this frame.
pub count: usize,
}
impl Frame<'_> {
/// The size of the current frame
///
/// 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
}
/// Render a [`Widget`] to the current buffer using [`Widget::render`].
///
/// Usually the area argument is the size of the current frame or a sub-area of the current
/// frame (which can be obtained using [`Layout`] to split the total area).
///
/// # Example
///
/// ```rust
/// # use ratatui::{backend::TestBackend, prelude::*, widgets::Block};
/// # let backend = TestBackend::new(5, 5);
/// # let mut terminal = Terminal::new(backend).unwrap();
/// # let mut frame = terminal.get_frame();
/// let block = Block::default();
/// let area = Rect::new(0, 0, 5, 5);
/// frame.render_widget(block, area);
/// ```
///
/// [`Layout`]: crate::layout::Layout
pub fn render_widget<W>(&mut self, widget: W, area: Rect)
where
W: Widget,
{
widget.render(area, self.buffer);
}
/// Render a [`StatefulWidget`] to the current buffer using [`StatefulWidget::render`].
///
/// Usually the area argument is the size of the current frame or a sub-area of the current
/// frame (which can be obtained using [`Layout`] to split the total area).
///
/// The last argument should be an instance of the [`StatefulWidget::State`] associated to the
/// given [`StatefulWidget`].
///
/// # Examples
///
/// ```rust
/// # use ratatui::{backend::TestBackend, prelude::*, widgets::*};
/// # let backend = TestBackend::new(5, 5);
/// # let mut terminal = Terminal::new(backend).unwrap();
/// # let mut frame = terminal.get_frame();
/// let mut state = ListState::default().with_selected(Some(1));
/// let list = List::new(vec![ListItem::new("Item 1"), ListItem::new("Item 2")]);
/// let area = Rect::new(0, 0, 5, 5);
/// frame.render_stateful_widget(list, area, &mut state);
/// ```
///
/// [`Layout`]: crate::layout::Layout
pub fn render_stateful_widget<W>(&mut self, widget: W, area: Rect, state: &mut W::State)
where
W: StatefulWidget,
{
widget.render(area, self.buffer, state);
}
/// After drawing this frame, make the cursor visible and put it at the specified (x, y)
/// coordinates. If this method is not called, the cursor will be hidden.
///
/// Note that this will interfere with calls to `Terminal::hide_cursor()`,
/// `Terminal::show_cursor()`, and `Terminal::set_cursor()`. Pick one of the APIs and stick
/// with it.
pub fn set_cursor(&mut self, x: u16, y: u16) {
self.cursor_position = Some((x, y));
}
/// Gets the buffer that this `Frame` draws into as a mutable reference.
pub fn buffer_mut(&mut self) -> &mut Buffer {
self.buffer
}
/// Returns the current frame count.
///
/// This method provides access to the frame count, which is a sequence number indicating
/// how many frames have been rendered up to (but not including) this one. It can be used
/// for purposes such as animation, performance tracking, or debugging.
///
/// Each time a frame has been rendered, this count is incremented,
/// providing a consistent way to reference the order and number of frames processed by the
/// terminal. When count reaches its maximum value (usize::MAX), it wraps around to zero.
///
/// This count is particularly useful when dealing with dynamic content or animations where the
/// state of the display changes over time. By tracking the frame count, developers can
/// synchronize updates or changes to the content with the rendering process.
///
/// # Examples
///
/// ```rust
/// # use ratatui::{backend::TestBackend, prelude::*, widgets::*};
/// # let backend = TestBackend::new(5, 5);
/// # let mut terminal = Terminal::new(backend).unwrap();
/// # let mut frame = terminal.get_frame();
/// let current_count = frame.count();
/// println!("Current frame count: {}", current_count);
/// ```
pub fn count(&self) -> usize {
self.count
}
}

494
src/terminal/terminal.rs Normal file
View File

@@ -0,0 +1,494 @@
use std::io;
use crate::{backend::ClearType, prelude::*};
/// An interface to interact and draw [`Frame`]s on the user's terminal.
///
/// This is the main entry point for Ratatui. It is responsible for drawing and maintaining the
/// state of the buffers, cursor and viewport.
///
/// The [`Terminal`] is generic over a [`Backend`] implementation which is used to interface with
/// the underlying terminal library. The [`Backend`] trait is implemented for three popular Rust
/// terminal libraries: [Crossterm], [Termion] and [Termwiz]. See the [`backend`] module for more
/// information.
///
/// The `Terminal` struct maintains two buffers: the current and the previous.
/// When the widgets are drawn, the changes are accumulated in the current buffer.
/// At the end of each draw pass, the two buffers are compared, and only the changes
/// between these buffers are written to the terminal, avoiding any redundant operations.
/// After flushing these changes, the buffers are swapped to prepare for the next draw cycle./
///
/// The terminal also has a viewport which is the area of the terminal that is currently visible to
/// the user. It can be either fullscreen, inline or fixed. See [`Viewport`] for more information.
///
/// Applications should detect terminal resizes and call [`Terminal::draw`] to redraw the
/// application with the new size. This will automatically resize the internal buffers to match the
/// new size for inline and fullscreen viewports. Fixed viewports are not resized automatically.
///
/// # Examples
///
/// ```rust,no_run
/// use std::io::stdout;
///
/// use ratatui::{prelude::*, widgets::Paragraph};
///
/// let backend = CrosstermBackend::new(stdout());
/// let mut terminal = Terminal::new(backend)?;
/// terminal.draw(|frame| {
/// let area = frame.size();
/// frame.render_widget(Paragraph::new("Hello World!"), area);
/// frame.set_cursor(0, 0);
/// })?;
/// # std::io::Result::Ok(())
/// ```
///
/// [Crossterm]: https://crates.io/crates/crossterm
/// [Termion]: https://crates.io/crates/termion
/// [Termwiz]: https://crates.io/crates/termwiz
/// [`backend`]: crate::backend
/// [`Backend`]: crate::backend::Backend
/// [`Buffer`]: crate::buffer::Buffer
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Terminal<B>
where
B: Backend,
{
/// The backend used to interface with the terminal
backend: B,
/// Holds the results of the current and previous draw calls. The two are compared at the end
/// of each draw pass to output the necessary updates to the terminal
buffers: [Buffer; 2],
/// Index of the current buffer in the previous array
current: usize,
/// Whether the cursor is currently hidden
hidden_cursor: bool,
/// Viewport
viewport: Viewport,
/// Area of the viewport
viewport_area: Rect,
/// Last known size of the terminal. Used to detect if the internal buffers have to be resized.
last_known_size: Rect,
/// Last known position of the cursor. Used to find the new area when the viewport is inlined
/// and the terminal resized.
last_known_cursor_pos: (u16, u16),
/// Number of frames rendered up until current time.
frame_count: usize,
}
/// Options to pass to [`Terminal::with_options`]
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Options {
/// Viewport used to draw to the terminal
pub viewport: Viewport,
}
impl<B> Drop for Terminal<B>
where
B: Backend,
{
fn drop(&mut self) {
// Attempt to restore the cursor state
if self.hidden_cursor {
if let Err(err) = self.show_cursor() {
eprintln!("Failed to show the cursor: {err}");
}
}
}
}
impl<B> Terminal<B>
where
B: Backend,
{
/// Creates a new [`Terminal`] with the given [`Backend`] with a full screen viewport.
///
/// # Example
///
/// ```rust,no_run
/// # use std::io::stdout;
/// # use ratatui::prelude::*;
/// let backend = CrosstermBackend::new(stdout());
/// let terminal = Terminal::new(backend)?;
/// # std::io::Result::Ok(())
/// ```
pub fn new(backend: B) -> io::Result<Terminal<B>> {
Terminal::with_options(
backend,
TerminalOptions {
viewport: Viewport::Fullscreen,
},
)
}
/// Creates a new [`Terminal`] with the given [`Backend`] and [`TerminalOptions`].
///
/// # Example
///
/// ```rust
/// # use std::io::stdout;
/// # use ratatui::{prelude::*, backend::TestBackend};
/// let backend = CrosstermBackend::new(stdout());
/// let viewport = Viewport::Fixed(Rect::new(0, 0, 10, 10));
/// let terminal = Terminal::with_options(backend, TerminalOptions { viewport })?;
/// # std::io::Result::Ok(())
/// ```
pub fn with_options(mut backend: B, options: TerminalOptions) -> io::Result<Terminal<B>> {
let size = match options.viewport {
Viewport::Fullscreen | Viewport::Inline(_) => backend.size()?,
Viewport::Fixed(area) => area,
};
let (viewport_area, cursor_pos) = match options.viewport {
Viewport::Fullscreen => (size, (0, 0)),
Viewport::Inline(height) => compute_inline_size(&mut backend, height, size, 0)?,
Viewport::Fixed(area) => (area, (area.left(), area.top())),
};
Ok(Terminal {
backend,
buffers: [Buffer::empty(viewport_area), Buffer::empty(viewport_area)],
current: 0,
hidden_cursor: false,
viewport: options.viewport,
viewport_area,
last_known_size: size,
last_known_cursor_pos: cursor_pos,
frame_count: 0,
})
}
/// Get a Frame object which provides a consistent view into the terminal state for rendering.
pub fn get_frame(&mut self) -> Frame {
let count = self.frame_count;
Frame {
cursor_position: None,
viewport_area: self.viewport_area,
buffer: self.current_buffer_mut(),
count,
}
}
/// Gets the current buffer as a mutable reference.
pub fn current_buffer_mut(&mut self) -> &mut Buffer {
&mut self.buffers[self.current]
}
/// Gets the backend
pub fn backend(&self) -> &B {
&self.backend
}
/// Gets the backend as a mutable reference
pub fn backend_mut(&mut self) -> &mut B {
&mut self.backend
}
/// Obtains a difference between the previous and the current buffer and passes it to the
/// current backend for drawing.
pub fn flush(&mut self) -> io::Result<()> {
let previous_buffer = &self.buffers[1 - self.current];
let current_buffer = &self.buffers[self.current];
let updates = previous_buffer.diff(current_buffer);
if let Some((col, row, _)) = updates.last() {
self.last_known_cursor_pos = (*col, *row);
}
self.backend.draw(updates.into_iter())
}
/// Updates the Terminal so that internal buffers match the requested size.
///
/// Requested size will be saved so the size can remain consistent when rendering. This leads
/// to a full clear of the screen.
pub fn resize(&mut self, size: Rect) -> io::Result<()> {
let next_area = match self.viewport {
Viewport::Fullscreen => size,
Viewport::Inline(height) => {
let offset_in_previous_viewport = self
.last_known_cursor_pos
.1
.saturating_sub(self.viewport_area.top());
compute_inline_size(&mut self.backend, height, size, offset_in_previous_viewport)?.0
}
Viewport::Fixed(area) => area,
};
self.set_viewport_area(next_area);
self.clear()?;
self.last_known_size = size;
Ok(())
}
fn set_viewport_area(&mut self, area: Rect) {
self.buffers[self.current].resize(area);
self.buffers[1 - self.current].resize(area);
self.viewport_area = area;
}
/// Queries the backend for size and resizes if it doesn't match the previous size.
pub fn autoresize(&mut self) -> io::Result<()> {
// fixed viewports do not get autoresized
if matches!(self.viewport, Viewport::Fullscreen | Viewport::Inline(_)) {
let size = self.size()?;
if size != self.last_known_size {
self.resize(size)?;
}
};
Ok(())
}
/// Synchronizes terminal size, calls the rendering closure, flushes the current internal state
/// and prepares for the next draw call.
///
/// This is the main entry point for drawing to the terminal.
///
/// # Examples
///
/// ```rust,no_run
/// # use std::io::stdout;
/// # use ratatui::{prelude::*, widgets::Paragraph};
/// let backend = CrosstermBackend::new(stdout());
/// let mut terminal = Terminal::new(backend)?;
/// terminal.draw(|frame| {
/// let area = frame.size();
/// frame.render_widget(Paragraph::new("Hello World!"), area);
/// frame.set_cursor(0, 0);
/// })?;
/// # std::io::Result::Ok(())
/// ```
pub fn draw<F>(&mut self, f: F) -> io::Result<CompletedFrame>
where
F: FnOnce(&mut Frame),
{
// Autoresize - otherwise we get glitches if shrinking or potential desync between widgets
// and the terminal (if growing), which may OOB.
self.autoresize()?;
let mut frame = self.get_frame();
f(&mut frame);
// We can't change the cursor position right away because we have to flush the frame to
// stdout first. But we also can't keep the frame around, since it holds a &mut to
// Buffer. Thus, we're taking the important data out of the Frame and dropping it.
let cursor_position = frame.cursor_position;
// Draw to stdout
self.flush()?;
match cursor_position {
None => self.hide_cursor()?,
Some((x, y)) => {
self.show_cursor()?;
self.set_cursor(x, y)?;
}
}
self.swap_buffers();
// Flush
self.backend.flush()?;
let completed_frame = CompletedFrame {
buffer: &self.buffers[1 - self.current],
area: self.last_known_size,
count: self.frame_count,
};
// increment frame count before returning from draw
self.frame_count = self.frame_count.wrapping_add(1);
Ok(completed_frame)
}
/// Hides the cursor.
pub fn hide_cursor(&mut self) -> io::Result<()> {
self.backend.hide_cursor()?;
self.hidden_cursor = true;
Ok(())
}
/// Shows the cursor.
pub fn show_cursor(&mut self) -> io::Result<()> {
self.backend.show_cursor()?;
self.hidden_cursor = false;
Ok(())
}
/// Gets the current cursor position.
///
/// This is the position of the cursor after the last draw call and is returned as a tuple of
/// `(x, y)` coordinates.
pub fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
self.backend.get_cursor()
}
/// Sets the cursor position.
pub fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
self.backend.set_cursor(x, y)?;
self.last_known_cursor_pos = (x, y);
Ok(())
}
/// Clear the terminal and force a full redraw on the next draw call.
pub fn clear(&mut self) -> io::Result<()> {
match self.viewport {
Viewport::Fullscreen => self.backend.clear_region(ClearType::All)?,
Viewport::Inline(_) => {
self.backend
.set_cursor(self.viewport_area.left(), self.viewport_area.top())?;
self.backend.clear_region(ClearType::AfterCursor)?;
}
Viewport::Fixed(area) => {
for row in area.top()..area.bottom() {
self.backend.set_cursor(0, row)?;
self.backend.clear_region(ClearType::AfterCursor)?;
}
}
}
// Reset the back buffer to make sure the next update will redraw everything.
self.buffers[1 - self.current].reset();
Ok(())
}
/// Clears the inactive buffer and swaps it with the current buffer
pub fn swap_buffers(&mut self) {
self.buffers[1 - self.current].reset();
self.current = 1 - self.current;
}
/// Queries the real size of the backend.
pub fn size(&self) -> io::Result<Rect> {
self.backend.size()
}
/// Insert some content before the current inline viewport. This has no effect when the
/// viewport is fullscreen.
///
/// This function scrolls down the current viewport by the given height. The newly freed space
/// is then made available to the `draw_fn` closure through a writable `Buffer`.
///
/// Before:
/// ```ignore
/// +-------------------+
/// | |
/// | viewport |
/// | |
/// +-------------------+
/// ```
///
/// After:
/// ```ignore
/// +-------------------+
/// | buffer |
/// +-------------------+
/// +-------------------+
/// | |
/// | viewport |
/// | |
/// +-------------------+
/// ```
///
/// # Examples
///
/// ## Insert a single line before the current viewport
///
/// ```rust
/// # use ratatui::{backend::TestBackend, prelude::*, widgets::*};
/// # let backend = TestBackend::new(10, 10);
/// # let mut terminal = Terminal::new(backend).unwrap();
/// terminal.insert_before(1, |buf| {
/// 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);
/// });
/// ```
pub fn insert_before<F>(&mut self, height: u16, draw_fn: F) -> io::Result<()>
where
F: FnOnce(&mut Buffer),
{
if !matches!(self.viewport, Viewport::Inline(_)) {
return Ok(());
}
// Clear the viewport off the screen
self.clear()?;
// 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: 0,
width: self.viewport_area.width,
height,
};
let mut buffer = Buffer::empty(area);
draw_fn(&mut buffer);
// 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;
self.backend
.append_lines(self.viewport_area.height.saturating_sub(1) + chunk_size)?;
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(())
}
}
fn compute_inline_size<B: Backend>(
backend: &mut B,
height: u16,
size: Rect,
offset_in_previous_viewport: u16,
) -> io::Result<(Rect, (u16, u16))> {
let pos = backend.get_cursor()?;
let mut row = pos.1;
let max_height = size.height.min(height);
let lines_after_cursor = height
.saturating_sub(offset_in_previous_viewport)
.saturating_sub(1);
backend.append_lines(lines_after_cursor)?;
let available_lines = size.height.saturating_sub(row).saturating_sub(1);
let missing_lines = lines_after_cursor.saturating_sub(available_lines);
if missing_lines > 0 {
row = row.saturating_sub(missing_lines);
}
row = row.saturating_sub(offset_in_previous_viewport);
Ok((
Rect {
x: 0,
y: row,
width: size.width,
height: max_height,
},
pos,
))
}

54
src/terminal/viewport.rs Normal file
View File

@@ -0,0 +1,54 @@
use std::fmt;
use crate::prelude::*;
/// Represents the viewport of the terminal. The viewport is the area of the terminal that is
/// currently visible to the user. It can be either fullscreen, inline or fixed.
///
/// When the viewport is fullscreen, the whole terminal is used to draw the application.
///
/// When the viewport is inline, it is drawn inline with the rest of the terminal. The height of
/// the viewport is fixed, but the width is the same as the terminal width.
///
/// When the viewport is fixed, it is drawn in a fixed area of the terminal. The area is specified
/// by a [`Rect`].
///
/// See [`Terminal::with_options`] for more information.
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub enum Viewport {
/// The viewport is fullscreen
#[default]
Fullscreen,
/// The viewport is inline with the rest of the terminal.
///
/// The viewport's height is fixed and specified in number of lines. The width is the same as
/// the terminal's width. The viewport is drawn below the cursor position.
Inline(u16),
/// The viewport is drawn in a fixed area of the terminal. The area is specified by a [`Rect`].
Fixed(Rect),
}
impl fmt::Display for Viewport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Viewport::Fullscreen => write!(f, "Fullscreen"),
Viewport::Inline(height) => write!(f, "Inline({})", height),
Viewport::Fixed(area) => write!(f, "Fixed({})", area),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn viewport_to_string() {
assert_eq!(Viewport::Fullscreen.to_string(), "Fullscreen");
assert_eq!(Viewport::Inline(5).to_string(), "Inline(5)");
assert_eq!(
Viewport::Fixed(Rect::new(0, 0, 5, 5)).to_string(),
"Fixed(5x5+0+0)"
);
}
}

View File

@@ -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,80 +259,61 @@ 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>`]).
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # 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 mut styled_line = Line::from(vec![
/// Span::styled("My", style),
/// Span::styled(" text", style),
/// ]);
/// let line = Line::styled("My text", Modifier::ITALIC);
///
/// assert_ne!(raw_line, styled_line);
/// let styled_line = Line::styled("My text", (Color::Yellow, Modifier::ITALIC));
///
/// raw_line.patch_style(style);
/// assert_eq!(raw_line, styled_line);
/// assert_eq!(styled_line, line.patch_style(Color::Yellow));
/// ```
pub fn patch_style(&mut self, style: Style) {
for span in &mut self.spans {
span.patch_style(style);
}
#[must_use = "method moves the value of self and returns the modified value"]
pub fn patch_style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = self.style.patch(style);
self
}
/// Resets the style of each Span in the Line.
///
/// Equivalent to calling `patch_style(Style::reset())`.
///
/// ## Examples
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// let mut line = Line::from(vec![
/// Span::styled("My", Style::default().fg(Color::Yellow)),
/// Span::styled(" text", Style::default().add_modifier(Modifier::BOLD)),
/// ]);
/// # let style = Style::default().yellow();
/// let line = Line::styled("My text", style);
///
/// line.reset_style();
/// assert_eq!(Style::reset(), line.spans[0].style);
/// assert_eq!(Style::reset(), line.spans[1].style);
/// assert_eq!(Style::reset(), line.reset_style().style);
/// ```
pub fn reset_style(&mut self) {
for span in &mut self.spans {
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
}
#[must_use = "method moves the value of self and returns the modified value"]
pub fn reset_style(self) -> Self {
self.patch_style(Style::reset())
}
}
@@ -202,16 +353,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,50 +476,40 @@ mod tests {
}
#[test]
fn test_patch_style() {
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 styled_line = Line::from(vec![
Span::styled("My", style),
Span::styled(" text", style),
]);
fn patch_style() {
let raw_line = Line::styled("foobar", Color::Yellow);
let styled_line = Line::styled("foobar", (Color::Yellow, Modifier::ITALIC));
assert_ne!(raw_line, styled_line);
raw_line.patch_style(style);
let raw_line = raw_line.patch_style(Modifier::ITALIC);
assert_eq!(raw_line, styled_line);
}
#[test]
fn test_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)),
]);
fn reset_style() {
let line =
Line::styled("foobar", Style::default().yellow().on_red().italic()).reset_style();
line.reset_style();
assert_eq!(Style::reset(), line.spans[0].style);
assert_eq!(Style::reset(), line.spans[1].style);
assert_eq!(Style::reset(), line.style);
}
#[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 +519,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 +535,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 +565,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,16 +133,104 @@ 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(),
}
}
/// Sets the content of the span.
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// Accepts any type that can be converted to [`Cow<str>`] (e.g. `&str`, `String`, `&String`,
/// etc.).
///
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// let mut span = Span::default().content("content");
/// ```
#[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>`]).
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Example
///
/// ```rust
/// # use ratatui::prelude::*;
/// let span = Span::styled("test content", Style::new().green().italic())
/// .patch_style(Style::new().red().on_yellow().bold());
/// assert_eq!(span.style, Style::new().red().on_yellow().italic().bold());
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn patch_style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = self.style.patch(style);
self
}
/// Resets the style of the Span.
///
/// This is Equivalent to calling `patch_style(Style::reset())`.
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Example
///
/// ```rust
/// # use ratatui::prelude::*;
/// let span = Span::styled(
/// "Test Content",
/// Style::new().dark_gray().on_yellow().italic(),
/// )
/// .reset_style();
/// assert_eq!(span.style, Style::reset());
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn reset_style(self) -> 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()
@@ -110,16 +241,21 @@ impl<'a> Span<'a> {
/// `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>>(),
/// 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()),
@@ -128,48 +264,16 @@ impl<'a> Span<'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.content
.as_ref()
.graphemes(true)
.filter(|g| *g != "\n")
.map(move |g| StyledGrapheme {
symbol: g,
style: base_style.patch(self.style),
})
}
/// Patches the style of the Span, adding modifiers from the given style.
///
/// # Example
///
/// ```rust
/// # use ratatui::prelude::*;
/// let mut span = Span::styled("test content", Style::new().green().italic());
/// span.patch_style(Style::new().red().on_yellow().bold());
/// assert_eq!(span.style, Style::new().red().on_yellow().italic().bold());
/// ```
pub fn patch_style(&mut self, style: Style) {
self.style = self.style.patch(style);
}
/// Resets the style of the Span.
///
/// This is Equivalent to calling `patch_style(Style::reset())`.
///
/// # Example
///
/// ```rust
/// # use ratatui::prelude::*;
/// let mut span = Span::styled("Test Content", Style::new().green().on_yellow().italic());
/// span.reset_style();
/// assert_eq!(span.style, Style::reset());
/// ```
pub fn reset_style(&mut self) {
self.patch_style(Style::reset());
.map(move |g| StyledGrapheme { symbol: g, style })
}
}
@@ -189,16 +293,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 +374,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";
@@ -273,15 +420,14 @@ mod tests {
#[test]
fn reset_style() {
let mut span = Span::styled("test content", Style::new().green());
span.reset_style();
let span = Span::styled("test content", Style::new().green()).reset_style();
assert_eq!(span.style, Style::reset());
}
#[test]
fn patch_style() {
let mut span = Span::styled("test content", Style::new().green().on_yellow());
span.patch_style(Style::new().red().bold());
let span = Span::styled("test content", Style::new().green().on_yellow())
.patch_style(Style::new().red().bold());
assert_eq!(span.style, Style::new().red().on_yellow().bold());
}
@@ -303,4 +449,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,21 +59,25 @@ 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);
text
Text::raw(content).patch_style(style)
}
/// Returns the max width of all the lines.
@@ -103,45 +108,63 @@ 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>`]).
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// 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);
/// let mut raw_text = Text::styled("The first line\nThe second line", Modifier::ITALIC);
/// let styled_text = Text::styled(
/// String::from("The first line\nThe second line"),
/// (Color::Yellow, Modifier::ITALIC),
/// );
/// assert_ne!(raw_text, styled_text);
///
/// raw_text.patch_style(style);
/// let raw_text = raw_text.patch_style(Color::Yellow);
/// assert_eq!(raw_text, styled_text);
/// ```
pub fn patch_style(&mut self, style: Style) {
for line in &mut self.lines {
line.patch_style(style);
}
#[must_use = "method moves the value of self and returns the modified value"]
pub fn patch_style<S: Into<Style>>(mut self, style: S) -> Self {
let style = style.into();
self.lines = self
.lines
.into_iter()
.map(|line| line.patch_style(style))
.collect();
self
}
/// Resets the style of the Text.
/// Equivalent to calling `patch_style(Style::reset())`.
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// ## Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
/// let mut text = Text::styled("The first line\nThe second line", style);
/// let style = Style::default()
/// .fg(Color::Yellow)
/// .add_modifier(Modifier::ITALIC);
/// let text = Text::styled("The first line\nThe second line", style);
///
/// text.reset_style();
/// let text = text.reset_style();
/// for line in &text.lines {
/// for span in &line.spans {
/// assert_eq!(Style::reset(), span.style);
/// }
/// assert_eq!(Style::reset(), line.style);
/// }
/// ```
pub fn reset_style(&mut self) {
for line in &mut self.lines {
line.reset_style();
}
#[must_use = "method moves the value of self and returns the modified value"]
pub fn reset_style(mut self) -> Self {
self.lines = self
.lines
.into_iter()
.map(|line| line.reset_style())
.collect();
self
}
}
@@ -223,8 +246,8 @@ mod tests {
assert_eq!(
text.lines,
vec![
Line::from(Span::styled("The first line", style)),
Line::from(Span::styled("The second line", style))
Line::styled("The first line", style),
Line::styled("The second line", style)
]
);
}
@@ -245,15 +268,14 @@ mod tests {
fn patch_style() {
let style = Style::new().yellow().italic();
let style2 = Style::new().red().underlined();
let mut text = Text::styled("The first line\nThe second line", style);
let text = Text::styled("The first line\nThe second line", style).patch_style(style2);
text.patch_style(style2);
let expected_style = Style::new().red().italic().underlined();
assert_eq!(
text.lines,
vec![
Line::from(Span::styled("The first line", expected_style)),
Line::from(Span::styled("The second line", expected_style))
Line::styled("The first line", expected_style),
Line::styled("The second line", expected_style)
]
);
}
@@ -261,14 +283,13 @@ mod tests {
#[test]
fn reset_style() {
let style = Style::new().yellow().italic();
let mut text = Text::styled("The first line\nThe second line", style);
let text = Text::styled("The first line\nThe second line", style).reset_style();
text.reset_style();
assert_eq!(
text.lines,
vec![
Line::from(Span::styled("The first line", Style::reset())),
Line::from(Span::styled("The second line", Style::reset()))
Line::styled("The first line", Style::reset()),
Line::styled("The second line", Style::reset())
]
);
}

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")
@@ -266,6 +267,13 @@ impl<'a> Block<'a> {
}
}
/// Create a new block with [all borders](Borders::ALL) shown
pub const fn bordered() -> Self {
let mut block = Block::new();
block.borders = Borders::ALL;
block
}
/// Adds a title to the block.
///
/// The `title` function allows you to add a title to the block. You can call this function
@@ -301,7 +309,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 +339,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 +358,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,17 +370,12 @@ 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
}
#[deprecated(since = "0.22.0", note = "You should use a `title_position` instead.")]
/// This method just calls `title_position` with Position::Bottom
pub fn title_on_bottom(self) -> Block<'a> {
self.title_position(Position::Bottom)
}
/// Sets the default [`Position`] for all block [titles](Title).
///
/// Titles that explicitly set a [`Position`] will ignore this.
@@ -372,7 +385,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 +397,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 +407,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 +419,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 +431,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 +458,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 +475,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 +503,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 +539,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 +564,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 +600,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 +805,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)
}
}
@@ -782,6 +821,12 @@ mod tests {
style::{Color, Modifier, Stylize},
};
#[test]
fn create_with_all_borders() {
let block = Block::bordered();
assert_eq!(block.borders, Borders::all());
}
#[test]
fn inner_takes_into_account_the_borders() {
// No borders
@@ -939,6 +984,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 +1114,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();
@@ -1053,17 +1215,6 @@ mod tests {
}
}
#[test]
fn title_on_bottom() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 2));
#[allow(deprecated)]
Block::default()
.title("test")
.title_on_bottom()
.render(buffer.area, &mut buffer);
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" ", "test"]));
}
#[test]
fn title_position() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 2));

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;

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]

743
src/widgets/list.rs Normal file → Executable file
View File

@@ -1,42 +1,156 @@
#![warn(missing_docs)]
use strum::{Display, EnumString};
use unicode_width::UnicodeWidthStr;
use crate::{
buffer::Buffer,
layout::{Alignment, Corner, Rect},
style::{Style, Styled},
text::Text,
prelude::*,
widgets::{Block, HighlightSpacing, StatefulWidget, Widget},
};
/// State of the [`List`] widget
///
/// This state can be used to scroll through items and select one. When the list is rendered as a
/// stateful widget, the selected item will be highlighted and the list will be shifted to ensure
/// that the selected item is visible. This will modify the [`ListState`] object passed to the
/// [`Frame::render_stateful_widget`](crate::terminal::Frame::render_stateful_widget) method.
///
/// The state consists of two fields:
/// - [`offset`]: the index of the first item to be displayed
/// - [`selected`]: the index of the selected item, which can be `None` if no item is selected
///
/// [`offset`]: ListState::offset()
/// [`selected`]: ListState::selected()
///
/// See the [list example] for a more in depth example of the various configuration options and
/// for how to handle state.
///
/// [list example]: https://github.com/ratatui-org/ratatui/blob/main/examples/list.rs
///
/// # Example
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # fn ui(frame: &mut Frame) {
/// # let area = Rect::default();
/// # let items = vec!["Item 1"];
/// let list = List::new(items);
///
/// // This should be stored outside of the function in your application state.
/// let mut state = ListState::default();
///
/// *state.offset_mut() = 1; // display the second item and onwards
/// state.select(Some(3)); // select the forth item (0-indexed)
///
/// frame.render_stateful_widget(list, area, &mut state);
/// # }
/// ```
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ListState {
offset: usize,
selected: Option<usize>,
}
impl ListState {
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
}
/// Sets the index of the first item 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 = ListState::default().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 item
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let state = ListState::default().with_selected(Some(1));
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn with_selected(mut self, selected: Option<usize>) -> Self {
self.selected = selected;
self
}
/// Index of the first item to be displayed
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let state = ListState::default();
/// assert_eq!(state.offset(), 0);
/// ```
pub fn offset(&self) -> usize {
self.offset
}
/// Mutable reference to the index of the first item to be displayed
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let mut state = ListState::default();
/// *state.offset_mut() = 1;
/// ```
pub fn offset_mut(&mut self) -> &mut usize {
&mut self.offset
}
/// Index of the selected item
///
/// Returns `None` if no item is selected
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let state = TableState::default();
/// assert_eq!(state.selected(), None);
/// ```
pub fn selected(&self) -> Option<usize> {
self.selected
}
/// Mutable reference to the index of the selected item
///
/// Returns `None` if no item is selected
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let mut state = ListState::default();
/// *state.selected_mut() = Some(1);
/// ```
pub fn selected_mut(&mut self) -> &mut Option<usize> {
&mut self.selected
}
/// Sets the index of the selected item
///
/// Set to `None` if no item is selected. This will also reset the offset to `0`.
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let mut state = ListState::default();
/// state.select(Some(1));
/// ```
pub fn select(&mut self, index: Option<usize>) {
self.selected = index;
if index.is_none() {
@@ -45,6 +159,51 @@ impl ListState {
}
}
/// A single item in a [`List`]
///
/// The item's height is defined by the number of lines it contains. This can be queried using
/// [`ListItem::height`]. Similarly, [`ListItem::width`] will return the maximum width of all
/// lines.
///
/// You can set the style of an item with [`ListItem::style`] or using the [`Stylize`] trait.
/// This [`Style`] will be combined with the [`Style`] of the inner [`Text`]. The [`Style`]
/// of the [`Text`] will be added to the [`Style`] of the [`ListItem`].
///
/// # Examples
///
/// You can create [`ListItem`]s from simple `&str`
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let item = ListItem::new("Item 1");
/// ```
///
/// Anything that can be converted to [`Text`] can be a [`ListItem`].
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let item1: ListItem = "Item 1".into();
/// let item2: ListItem = Line::raw("Item 2").into();
/// ```
///
/// A [`ListItem`] styled with [`Stylize`]
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let item = ListItem::new("Item 1").red().on_white();
/// ```
///
/// If you need more control over the item's style, you can explicitly style the underlying
/// [`Text`]
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let mut text = Text::default();
/// text.extend(["Item".blue(), Span::raw(" "), "1".bold().red()]);
/// let item = ListItem::new(text);
/// ```
///
/// [`Stylize`]: crate::style::Stylize
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct ListItem<'a> {
content: Text<'a>,
@@ -52,6 +211,37 @@ pub struct ListItem<'a> {
}
impl<'a> ListItem<'a> {
/// Creates a new [`ListItem`]
///
/// The `content` parameter accepts any value that can be converted into [`Text`].
///
/// # Examples
///
/// You can create [`ListItem`]s from simple `&str`
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let item = ListItem::new("Item 1");
/// ```
///
/// Anything that can be converted to [`Text`] can be a [`ListItem`].
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let item1: ListItem = "Item 1".into();
/// let item2: ListItem = Line::raw("Item 2").into();
/// ```
///
/// You can also create multilines item
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let item = ListItem::new("Multi-line\nitem");
/// ```
///
/// # See also
///
/// - [`List::new`] to create a list of items that can be converted to [`ListItem`]
pub fn new<T>(content: T) -> ListItem<'a>
where
T: Into<Text<'a>>,
@@ -62,41 +252,172 @@ impl<'a> ListItem<'a> {
}
}
pub fn style(mut self, style: Style) -> ListItem<'a> {
self.style = style;
/// Sets the item style
///
/// `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 the [`Text`] content.
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Example
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let item = ListItem::new("Item 1").style(Style::new().red().italic());
/// ```
///
/// `ListItem` also implements the [`Styled`] trait, which means you can use style shorthands
/// from the [`Stylize`](crate::style::Stylize) trait to set the style of the widget more
/// concisely.
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let item = ListItem::new("Item 1").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) -> ListItem<'a> {
self.style = style.into();
self
}
/// Returns the item height
///
/// # Examples
///
/// One line item
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let item = ListItem::new("Item 1");
/// assert_eq!(item.height(), 1);
/// ```
///
/// Two lines item (note the `\n`)
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let item = ListItem::new("Multi-line\nitem");
/// assert_eq!(item.height(), 2);
/// ```
pub fn height(&self) -> usize {
self.content.height()
}
/// Returns the max width of all the lines
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let item = ListItem::new("12345");
/// assert_eq!(item.width(), 5);
/// ```
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let item = ListItem::new("12345\n1234567");
/// assert_eq!(item.width(), 7);
/// ```
pub fn width(&self) -> usize {
self.content.width()
}
}
impl<'a, T> From<T> for ListItem<'a>
where
T: Into<Text<'a>>,
{
fn from(value: T) -> Self {
ListItem::new(value)
}
}
/// A widget to display several items among which one can be selected (optional)
///
/// A list is a collection of [`ListItem`]s.
///
/// This is different from a [`Table`] because it does not handle columns or headers and the item's
/// height is automatically determined. A `List` can also be put in reverse order (i.e. *bottom to
/// top*) whereas a [`Table`] cannot.
///
/// [`Table`]: crate::widgets::Table
///
/// [`List`] implements [`Widget`] and so it can be drawn using
/// [`Frame::render_widget`](crate::terminal::Frame::render_widget).
///
/// [`List`] is also a [`StatefulWidget`], which means you can use it with [`ListState`] to allow
/// the user to [scroll](ListState::offset) through items and [select](ListState::select) one of
/// them.
///
/// See the [list example] for a more in depth example of the various configuration options and for
/// how to handle state.
///
/// [list example]: https://github.com/ratatui-org/ratatui/blob/main/examples/list.rs
///
/// # Fluent setters
///
/// - [`List::highlight_style`] sets the style of the selected item.
/// - [`List::highlight_symbol`] sets the symbol to be displayed in front of the selected item.
/// - [`List::repeat_highlight_symbol`] sets whether to repeat the symbol and style over selected
/// multi-line items
/// - [`List::direction`] sets the list direction
///
/// # Examples
///
/// ```
/// use ratatui::{prelude::*, widgets::*};
///
/// let items = [ListItem::new("Item 1"), ListItem::new("Item 2"), ListItem::new("Item 3")];
/// List::new(items)
/// # fn ui(frame: &mut Frame) {
/// # let area = Rect::default();
/// let items = ["Item 1", "Item 2", "Item 3"];
/// let list = List::new(items)
/// .block(Block::default().title("List").borders(Borders::ALL))
/// .style(Style::default().fg(Color::White))
/// .highlight_style(Style::default().add_modifier(Modifier::ITALIC))
/// .highlight_symbol(">>");
/// .highlight_symbol(">>")
/// .repeat_highlight_symbol(true)
/// .direction(ListDirection::BottomToTop);
///
/// frame.render_widget(list, area);
/// # }
/// ```
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
///
/// # Stateful example
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # fn ui(frame: &mut Frame) {
/// # let area = Rect::default();
/// // This should be stored outside of the function in your application state.
/// let mut state = ListState::default();
/// let items = ["Item 1", "Item 2", "Item 3"];
/// let list = List::new(items)
/// .block(Block::default().title("List").borders(Borders::ALL))
/// .highlight_style(Style::new().add_modifier(Modifier::REVERSED))
/// .highlight_symbol(">>")
/// .repeat_highlight_symbol(true);
///
/// frame.render_stateful_widget(list, area, &mut state);
/// # }
/// ```
///
/// In addition to `List::new`, any iterator whose element is convertible to `ListItem` can be
/// collected into `List`.
///
/// ```
/// use ratatui::widgets::List;
///
/// (0..5).map(|i| format!("Item{i}")).collect::<List>();
/// ```
#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)]
pub struct List<'a> {
block: Option<Block<'a>>,
items: Vec<ListItem<'a>>,
/// Style used as a base style for the widget
style: Style,
start_corner: Corner,
/// List display direction
direction: ListDirection,
/// Style used to render selected item
highlight_style: Style,
/// Symbol in front of the selected item (Shift all items to the right)
@@ -107,43 +428,193 @@ pub struct List<'a> {
highlight_spacing: HighlightSpacing,
}
/// Defines the direction in which the list will be rendered.
///
/// If there are too few items to fill the screen, the list will stick to the starting edge.
///
/// See [`List::direction`].
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
pub enum ListDirection {
/// The first value is on the top, going to the bottom
#[default]
TopToBottom,
/// The first value is on the bottom, going to the top.
BottomToTop,
}
impl<'a> List<'a> {
/// Creates a new list from [`ListItem`]s
///
/// The `items` parameter accepts any value that can be converted into an iterator of
/// [`Into<ListItem>`]. This includes arrays of [`&str`] or [`Vec`]s of [`Text`].
///
/// # Example
///
/// From a slice of [`&str`]
///
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// let list = List::new(["Item 1", "Item 2"]);
/// ```
///
/// From [`Text`]
///
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// let list = List::new([
/// Text::styled("Item 1", Style::default().red()),
/// Text::styled("Item 2", Style::default().red()),
/// ]);
/// ```
///
/// You can also create an empty list using the [`Default`] implementation and use the
/// [`List::items`] fluent setter.
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let empty_list = List::default();
/// let filled_list = empty_list.items(["Item 1"]);
/// ```
pub fn new<T>(items: T) -> List<'a>
where
T: Into<Vec<ListItem<'a>>>,
T: IntoIterator,
T::Item: Into<ListItem<'a>>,
{
List {
block: None,
style: Style::default(),
items: items.into(),
start_corner: Corner::TopLeft,
highlight_style: Style::default(),
highlight_symbol: None,
repeat_highlight_symbol: false,
highlight_spacing: HighlightSpacing::default(),
items: items.into_iter().map(|i| i.into()).collect(),
direction: ListDirection::default(),
..Self::default()
}
}
/// Set the items
///
/// The `items` parameter accepts any value that can be converted into an iterator of
/// [`Into<ListItem>`]. This includes arrays of [`&str`] or [`Vec`]s of [`Text`].
///
/// This is a fluent setter method which must be chained or used as it consumes self.
///
/// # Example
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let list = List::default().items(["Item 1", "Item 2"]);
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn items<T>(mut self, items: T) -> Self
where
T: IntoIterator,
T::Item: Into<ListItem<'a>>,
{
self.items = items.into_iter().map(|i| i.into()).collect();
self
}
/// Wraps the list with a custom [`Block`] widget.
///
/// The `block` parameter holds the specified [`Block`] to be created around the [`List`]
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # let items = vec!["Item 1"];
/// let block = Block::default().title("List").borders(Borders::ALL);
/// let list = List::new(items).block(block);
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn block(mut self, block: Block<'a>) -> List<'a> {
self.block = Some(block);
self
}
pub fn style(mut self, style: Style) -> List<'a> {
self.style = style;
/// Sets the base style of the widget
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// All text rendered by the widget will use this style, unless overridden by [`Block::style`],
/// [`ListItem::style`], or the styles of the [`ListItem`]'s content.
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # let items = vec!["Item 1"];
/// let list = List::new(items).style(Style::new().red().italic());
/// ```
///
/// `List` 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.
///
/// [`Stylize`]: crate::style::Stylize
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # let items = vec!["Item 1"];
/// let list = List::new(items).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) -> List<'a> {
self.style = style.into();
self
}
/// Set the symbol to be displayed in front of the selected item
///
/// By default there are no highlight symbol.
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # let items = vec!["Item 1", "Item 2"];
/// let list = List::new(items).highlight_symbol(">>");
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn highlight_symbol(mut self, highlight_symbol: &'a str) -> List<'a> {
self.highlight_symbol = Some(highlight_symbol);
self
}
pub fn highlight_style(mut self, style: Style) -> List<'a> {
self.highlight_style = style;
/// Set the style of the selected item
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// This style will be applied to the entire item, including the
/// [highlight symbol](List::highlight_symbol) if it is displayed, and will override any style
/// set on the item or on the individual cells.
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # let items = vec!["Item 1", "Item 2"];
/// let list = List::new(items).highlight_style(Style::new().red().italic());
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn highlight_style<S: Into<Style>>(mut self, style: S) -> List<'a> {
self.highlight_style = style.into();
self
}
/// Set whether to repeat the highlight symbol and style over selected multi-line items
///
/// This is `false` by default.
///
/// This is a fluent setter method which must be chained or used as it consumes self
#[must_use = "method moves the value of self and returns the modified value"]
pub fn repeat_highlight_symbol(mut self, repeat: bool) -> List<'a> {
self.repeat_highlight_symbol = repeat;
self
@@ -151,21 +622,105 @@ impl<'a> List<'a> {
/// Set when to show the highlight spacing
///
/// See [`HighlightSpacing`] about which variant affects spacing in which way
/// The highlight spacing is the spacing that is allocated for the selection symbol (if enabled)
/// and is used to shift the list when an item is selected. This method allows you to configure
/// when this spacing is allocated.
///
/// - [`HighlightSpacing::Always`] will always allocate the spacing, regardless of whether an
/// item is selected or not. This means that the table will never change size, regardless of
/// if an item is selected or not.
/// - [`HighlightSpacing::WhenSelected`] will only allocate the spacing if an itemis selected.
/// This means that the table will shift when an item is selected. This is the default setting
/// for backwards compatibility, but it is recommended to use `HighlightSpacing::Always` for a
/// better user experience.
/// - [`HighlightSpacing::Never`] will never allocate the spacing, regardless of whether an item
/// is selected or not. This means that the highlight symbol will never be drawn.
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # let items = vec!["Item 1"];
/// let list = List::new(items).highlight_spacing(HighlightSpacing::Always);
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn highlight_spacing(mut self, value: HighlightSpacing) -> Self {
self.highlight_spacing = value;
self
}
pub fn start_corner(mut self, corner: Corner) -> List<'a> {
self.start_corner = corner;
/// Defines the list direction (up or down)
///
/// Defines if the `List` is displayed *top to bottom* (default) or *bottom to top*.
/// If there is too few items to fill the screen, the list will stick to the starting edge.
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Example
///
/// Bottom to top
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # let items = vec!["Item 1"];
/// let list = List::new(items).direction(ListDirection::BottomToTop);
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn direction(mut self, direction: ListDirection) -> List<'a> {
self.direction = direction;
self
}
/// Defines the list direction (up or down)
///
/// Defines if the `List` is displayed *top to bottom* (default) or *bottom to top*. Use
/// [`Corner::BottomLeft`] to go *bottom to top*. **Any** other variant will go *top to bottom*.
/// If there is too few items to fill the screen, the list will stick to the starting edge.
///
/// This is set to [`Corner::TopLeft`] by default.
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// ## Note
///
/// Despite its name, this method doesn't change the horizontal alignment, i.e. the `List`
/// **won't** start in a corner.
///
/// # Example
///
/// Same as default, i.e. *top to bottom*. Despite the name implying otherwise.
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # let items = vec!["Item 1"];
/// let list = List::new(items).start_corner(Corner::BottomRight);
/// ```
///
/// Bottom to top
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # let items = vec!["Item 1"];
/// let list = List::new(items).start_corner(Corner::BottomLeft);
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
#[deprecated(since = "0.25.0", note = "You should use `List::direction` instead.")]
pub fn start_corner(self, corner: Corner) -> List<'a> {
if corner == Corner::BottomLeft {
self.direction(ListDirection::BottomToTop)
} else {
self.direction(ListDirection::TopToBottom)
}
}
/// Returns the number of [`ListItem`]s in the list
pub fn len(&self) -> usize {
self.items.len()
}
/// Returns true if the list contains no elements.
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
@@ -247,7 +802,7 @@ impl<'a> StatefulWidget for List<'a> {
.skip(state.offset)
.take(end - start)
{
let (x, y) = if self.start_corner == Corner::BottomLeft {
let (x, y) = if self.direction == ListDirection::BottomToTop {
current_height += item.height() as u16;
(list_area.left(), list_area.bottom() - current_height)
} else {
@@ -316,7 +871,7 @@ impl<'a> Styled for List<'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)
}
}
@@ -328,11 +883,20 @@ impl<'a> Styled for ListItem<'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)
}
}
impl<'a, Item> FromIterator<Item> for List<'a>
where
Item: Into<ListItem<'a>>,
{
fn from_iter<Iter: IntoIterator<Item = Item>>(iter: Iter) -> Self {
List::new(iter)
}
}
#[cfg(test)]
mod tests {
use std::borrow::Cow;
@@ -430,6 +994,38 @@ mod tests {
assert_eq!(item.style, Style::default());
}
#[test]
fn test_str_into_list_item() {
let s = "Test item";
let item: ListItem = s.into();
assert_eq!(item.content, Text::from(s));
assert_eq!(item.style, Style::default());
}
#[test]
fn test_string_into_list_item() {
let s = String::from("Test item");
let item: ListItem = s.clone().into();
assert_eq!(item.content, Text::from(s));
assert_eq!(item.style, Style::default());
}
#[test]
fn test_span_into_list_item() {
let s = Span::from("Test item");
let item: ListItem = s.clone().into();
assert_eq!(item.content, Text::from(s));
assert_eq!(item.style, Style::default());
}
#[test]
fn test_vec_lines_into_list_item() {
let lines = vec![Line::raw("l1"), Line::raw("l2")];
let item: ListItem = lines.clone().into();
assert_eq!(item.content, Text::from(lines));
assert_eq!(item.style, Style::default());
}
#[test]
fn test_list_item_style() {
let item = ListItem::new("Test item").style(Style::default().bg(Color::Red));
@@ -478,7 +1074,7 @@ mod tests {
#[test]
fn test_list_does_not_render_in_small_space() {
let items = list_items(vec!["Item 0", "Item 1", "Item 2"]);
let items = vec!["Item 0", "Item 1", "Item 2"];
let list = List::new(items.clone()).highlight_symbol(">>");
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
@@ -718,6 +1314,21 @@ mod tests {
);
}
#[test]
fn test_list_items_setter() {
let list = List::default().items(["Item 0", "Item 1", "Item 2"]);
assert_buffer_eq!(
render_widget(list, 10, 5),
Buffer::with_lines(vec![
"Item 0 ",
"Item 1 ",
"Item 2 ",
" ",
" ",
])
);
}
#[test]
fn test_list_with_empty_strings() {
let items = list_items(vec!["Item 0", "", "", "Item 1", "Item 2"]);
@@ -736,6 +1347,13 @@ mod tests {
assert_buffer_eq!(buffer, expected);
}
#[test]
fn test_collect_list_from_iterator() {
let collected: List = (0..3).map(|i| format!("Item{i}")).collect();
let expected = List::new(["Item0", "Item1", "Item2"]);
assert_eq!(collected, expected);
}
#[test]
fn test_list_block() {
let items = list_items(vec!["Item 0", "Item 1", "Item 2"]);
@@ -942,9 +1560,40 @@ mod tests {
);
}
#[test]
fn test_list_direction_top_to_bottom() {
let items = list_items(vec!["Item 0", "Item 1", "Item 2"]);
let list = List::new(items).direction(ListDirection::TopToBottom);
let buffer = render_widget(list, 10, 5);
let expected = Buffer::with_lines(vec![
"Item 0 ",
"Item 1 ",
"Item 2 ",
" ",
" ",
]);
assert_buffer_eq!(buffer, expected);
}
#[test]
fn test_list_direction_bottom_to_top() {
let items = list_items(vec!["Item 0", "Item 1", "Item 2"]);
let list = List::new(items).direction(ListDirection::BottomToTop);
let buffer = render_widget(list, 10, 5);
let expected = Buffer::with_lines(vec![
" ",
" ",
"Item 2 ",
"Item 1 ",
"Item 0 ",
]);
assert_buffer_eq!(buffer, expected);
}
#[test]
fn test_list_start_corner_top_left() {
let items = list_items(vec!["Item 0", "Item 1", "Item 2"]);
#[allow(deprecated)] // For start_corner
let list = List::new(items).start_corner(Corner::TopLeft);
let buffer = render_widget(list, 10, 5);
let expected = Buffer::with_lines(vec![
@@ -960,6 +1609,7 @@ mod tests {
#[test]
fn test_list_start_corner_bottom_left() {
let items = list_items(vec!["Item 0", "Item 1", "Item 2"]);
#[allow(deprecated)] // For start_corner
let list = List::new(items).start_corner(Corner::BottomLeft);
let buffer = render_widget(list, 10, 5);
let expected = Buffer::with_lines(vec![
@@ -1053,7 +1703,12 @@ mod tests {
#[test]
fn list_can_be_stylized() {
assert_eq!(
List::new(vec![]).black().on_white().bold().not_dim().style,
List::new::<Vec<&str>>(vec![])
.black()
.on_white()
.bold()
.not_dim()
.style,
Style::default()
.fg(Color::Black)
.bg(Color::White)

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> {
@@ -249,8 +319,11 @@ impl<'a> Widget for Paragraph<'a> {
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)) =
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, area.width, current_line_alignment);
@@ -283,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)
}
}
@@ -295,7 +368,7 @@ mod test {
backend::TestBackend,
style::{Color, Modifier, Stylize},
text::{Line, Span},
widgets::Borders,
widgets::{block::Position, Borders},
Terminal,
};
@@ -478,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.";
@@ -793,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},
};
@@ -44,6 +42,7 @@ pub enum ScrollDirection {
/// If you don't have multi-line content, you can leave the `viewport_content_length` set to the
/// default of 0 and it'll use the track size as a `viewport_content_length`.
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ScrollbarState {
// The total length of the scrollable content.
content_length: usize,
@@ -62,18 +61,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 +149,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 +166,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 +212,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 +224,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 +311,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 +327,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 +339,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

@@ -4,10 +4,7 @@ use std::cmp::min;
use strum::{Display, EnumString};
use crate::{
buffer::Buffer,
layout::Rect,
style::{Style, Styled},
symbols,
prelude::*,
widgets::{Block, Widget},
};
@@ -18,7 +15,7 @@ use crate::{
/// `Sparkline` can be styled either using [`Sparkline::style`] or preferably using the methods
/// provided by the [`Stylize`](crate::style::Stylize) trait.
///
/// # Builder methods
/// # Setter methods
///
/// - [`Sparkline::block`] wraps the sparkline in a [`Block`]
/// - [`Sparkline::data`] defines the dataset, you'll almost always want to use it
@@ -81,6 +78,7 @@ 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
@@ -88,9 +86,13 @@ impl<'a> Sparkline<'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>`]).
///
/// The foreground corresponds to the bars while the background is everything else.
pub fn style(mut self, style: Style) -> Sparkline<'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) -> Sparkline<'a> {
self.style = style.into();
self
}
@@ -106,6 +108,7 @@ impl<'a> Sparkline<'a> {
/// 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
@@ -115,6 +118,7 @@ impl<'a> Sparkline<'a> {
///
/// 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
@@ -124,6 +128,7 @@ impl<'a> Sparkline<'a> {
///
/// 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
@@ -132,6 +137,7 @@ impl<'a> Sparkline<'a> {
/// 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
@@ -145,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,169 +1,17 @@
use std::iter;
#![warn(missing_docs)]
use itertools::Itertools;
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)]
@@ -173,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
///
@@ -187,692 +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: Vec<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: vec![],
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
}
/// Set the widths of the columns of the [`Table`] widget.
///
/// The `widths` parameter accepts `AsRef<[Constraint]>`` which can be an array, slice, or Vec.
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let table = Table::new(vec![]).widths([Constraint::Length(5), Constraint::Length(5)]);
/// let table = Table::new(vec![]).widths(&[Constraint::Length(5), Constraint::Length(5)]);
///
/// // widths could also be computed at runtime
/// let widths = vec![Constraint::Length(5), Constraint::Length(5)];
/// let table = Table::new(vec![]).widths(widths.clone());
/// let table = Table::new(vec![]).widths(&widths);
/// ```
pub fn widths<T: AsRef<[Constraint]>>(mut self, widths: T) -> Self {
let widths = widths.as_ref().to_vec();
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 constraints = iter::once(Constraint::Length(selection_width))
.chain(Itertools::intersperse(
self.widths.iter().cloned(),
Constraint::Length(self.column_spacing),
))
.collect_vec();
let layout = Layout::default()
.direction(Direction::Horizontal)
.constraints(constraints)
.segment_size(SegmentSize::None)
.split(Rect::new(0, 0, max_width, 1));
layout
.iter()
.skip(1) // skip selection column
.step_by(2) // skip spacing between columns
.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]
fn widths_conversions() {
let array = [Constraint::Percentage(100)];
let table = Table::new(vec![]).widths(array);
assert_eq!(table.widths, vec![Constraint::Percentage(100)], "array");
let array_ref = &[Constraint::Percentage(100)];
let table = Table::new(vec![]).widths(array_ref);
assert_eq!(table.widths, vec![Constraint::Percentage(100)], "array ref");
let vec = vec![Constraint::Percentage(100)];
let slice = vec.as_slice();
let table = Table::new(vec![]).widths(slice);
assert_eq!(table.widths, vec![Constraint::Percentage(100)], "slice");
let vec = vec![Constraint::Percentage(100)];
let table = Table::new(vec![]).widths(vec);
assert_eq!(table.widths, vec![Constraint::Percentage(100)], "vec");
let vec_ref = &vec![Constraint::Percentage(100)];
let table = Table::new(vec![]).widths(vec_ref);
assert_eq!(table.widths, vec![Constraint::Percentage(100)], "vec ref");
}
// 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() {

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