Compare commits

..

142 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
Valentin271
1229b96e42 feat(Rect): add offset method (#533)
The offset method creates a new Rect that is moved by the amount
specified in the x and y direction. These values can be positive or
negative. This is useful for manual layout tasks.

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

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

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

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

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

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

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

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

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

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

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

The following calls now all succeed:

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

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

- Adds TestBackend::append_lines() and TestBackend::clear_region() methods to
  support testing the changes
2023-11-08 10:04:35 -08:00
Josh McKinney
59b9c32fbc ci(codecov): adjust threshold and noise settings (#615)
Fixes https://github.com/ratatui-org/ratatui/issues/612
2023-11-05 10:21:11 +01:00
isinstance
9f37100096 Update README.md and fix the bug that demo2 cannot run (#595)
Fixes https://github.com/ratatui-org/ratatui/issues/594
2023-10-25 14:01:04 -07:00
a-kenji
a2f2bd5df5 fix: MSRV is now 1.70.0 (#593) 2023-10-25 02:44:36 -07:00
Orhun Parmaksız
c597b87f72 chore(release): prepare for 0.24.0 (#588) 2023-10-23 04:06:53 -07:00
Orhun Parmaksız
82a0d01a42 chore(changelog): skip dependency updates in changelog (#582) 2023-10-23 04:04:26 -07:00
Orhun Parmaksız
0e573cd6c7 docs(github): update code owners (#587) 2023-10-23 04:03:51 -07:00
Josh McKinney
b07000835f docs(readme): fix link to demo2 image (#589) 2023-10-23 12:28:59 +02:00
Orhun Parmaksız
c6c3f88a79 feat(backend): implement common traits for WindowSize (#586) 2023-10-23 10:40:09 +02:00
dependabot[bot]
a20bd6adb5 chore(deps): update lru requirement from 0.11.1 to 0.12.0 (#581)
Updates the requirements on [lru](https://github.com/jeromefroe/lru-rs) to permit the latest version.
- [Changelog](https://github.com/jeromefroe/lru-rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/jeromefroe/lru-rs/compare/0.11.1...0.12.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-21 09:25:05 -04:00
dependabot[bot]
5213f78d25 chore(deps): bump actions/checkout from 3 to 4 (#580)
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-21 12:19:52 +02:00
Josh McKinney
12f92911c7 chore(github): create dependabot.yml (#575)
* chore: Create dependabot.yml

* Update .github/dependabot.yml

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

* Update .github/dependabot.yml

---------

Co-authored-by: Orhun Parmaksız <orhun@archlinux.org>
2023-10-21 12:17:47 +02:00
Josh McKinney
ad2dc5646d docs(examples): update examples readme (#576)
remove VHS bug info, tweak colors_rgb image, update some of the instructions. add demo2
2023-10-20 21:41:36 +02:00
Linda_pp
6cbdb06fd8 chore(examples): refactor some examples (#578)
* chore(examples): Simplify timeout calculation with `Duration::saturating_sub`

Signed-off-by: rhysd <lin90162@yahoo.co.jp>

* fix(examples): Do not ignore errors from `poll_input` call

Signed-off-by: rhysd <lin90162@yahoo.co.jp>

---------

Signed-off-by: rhysd <lin90162@yahoo.co.jp>
2023-10-20 21:31:52 +02:00
blu
27c5637675 docs(README): fix links to CONTRIBUTING.md and BREAKING-CHANGES.md (#577) 2023-10-20 20:29:04 +02:00
Josh McKinney
0c52ff431a fix(gauge): fix gauge widget colors (#572)
The background colors of the gauge had a workaround for the issue we had
with VHS / TTYD rendering the background color of the gauge. This
workaround is no longer necessary in the updated versions of VHS / TTYD.

Fixes https://github.com/ratatui-org/ratatui/issues/501
2023-10-19 04:29:53 -07:00
Josh McKinney
8d507c43fa fix(backend): add feature flag for underline-color (#570)
Windows 7 doesn't support the underline color attribute, so we need to
make it optional. This commit adds a feature flag for the underline
color attribute - it is enabled by default, but can be disabled by
passing `--no-default-features` to cargo.

We could specically check for Windows 7 and disable the feature flag
automatically, but I think it's better for this check to be done by the
crossterm crate, since it's the one that actually knows about the
underlying terminal.

To disable the feature flag in an application that supports Windows 7,
add the following to your Cargo.toml:

```toml
ratatui = { version = "0.24.0", default-features = false, features = ["crossterm"] }
```

Fixes https://github.com/ratatui-org/ratatui/issues/555
2023-10-18 13:52:43 -07:00
Josh McKinney
b61f65bc20 docs(examples): udpate theme to Aardvark Blue (#574)
This is a nicer theme that makes the colors pop
2023-10-18 13:35:51 +02:00
Orhun Parmaksız
3a57e76ed1 chore(github): add contact links for issues (#567) 2023-10-11 17:42:48 -07:00
tz629
6c7bef8d11 docs: replace colons with dashes in README.md for consistency (#566) 2023-10-07 23:37:09 -07:00
Dheepak Krishnamurthy
88ae3485c2 docs: Update Frame docstring to remove reference to generic backend (#564) 2023-10-06 17:31:36 -04:00
Dheepak Krishnamurthy
e5caf170c8 docs(custom_widget): make button sticky when clicking with mouse (#561) 2023-10-06 11:17:14 +02:00
Dheepak Krishnamurthy
089f8ba66a docs: Add double quotes to instructions for features (#560) 2023-10-05 04:06:28 -04:00
Josh McKinney
fbf1a451c8 chore: simplify constraints (#556)
Use bare arrays rather than array refs / Vecs for all constraint
examples.

Ref: https://github.com/ratatui-org/ratatui-book/issues/94
2023-10-03 16:50:14 -07:00
Josh McKinney
4541336514 feat(canvas): implement half block marker (#550)
* feat(canvas): implement half block marker

A useful technique for the terminal is to use half blocks to draw a grid
of "pixels" on the screen. Because we can set two colors per cell, and
because terminal cells are about twice as tall as they are wide, we can
draw a grid of half blocks that looks like a grid of square pixels.

This commit adds a new `HalfBlock` marker that can be used in the Canvas
widget and the associated HalfBlockGrid.

Also updated demo2 to use the new marker as it looks much nicer.

Adds docs for many of the methods and structs on canvas.

Changes the grid resolution method to return the pixel count
rather than the index of the last pixel.
This is an internal detail with no user impact.
2023-09-30 06:03:03 -07:00
Josh McKinney
346e7b4f4d docs: add summary to breaking changes (#549) 2023-09-30 05:54:38 -07:00
Dheepak Krishnamurthy
15641c8475 feat: Add buffer_mut method on Frame (#548) 2023-09-30 05:53:37 -07:00
146 changed files with 16830 additions and 6076 deletions

2
.github/CODEOWNERS vendored
View File

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

View File

@@ -1 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Discord Chat
url: https://discord.gg/pMCEU9hNEj
about: Ask questions about ratatui on Discord
- name: Matrix Chat
url: https://matrix.to/#/#ratatui:matrix.org
about: Ask questions about ratatui on Matrix

18
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,18 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
# Maintain dependencies for Cargo
- package-ecosystem: "cargo"
directory: "/" # Location of package manifests
schedule:
interval: "weekly"
# Maintain dependencies for GitHub Actions
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: weekly
open-pull-requests-limit: 10

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

@@ -23,32 +23,12 @@ jobs:
if: ${{ !startsWith(github.event.ref, 'refs/tags/v') }}
steps:
- name: Checkout the repository
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
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
@@ -77,7 +57,7 @@ jobs:
if: ${{ startsWith(github.event.ref, 'refs/tags/v') }}
steps:
- name: Checkout the repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Publish on crates.io
uses: actions-rs/cargo@v1

View File

@@ -44,22 +44,44 @@ 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
fi
- name: Add label
if: steps.check_breaking_change.outputs.breaking_change == 'true'
uses: actions/github-script@v6
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |

View File

@@ -30,10 +30,10 @@ jobs:
steps:
- name: Checkout
if: github.event_name != 'pull_request'
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Checkout
if: github.event_name == 'pull_request'
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Install Rust nightly
@@ -60,7 +60,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
@@ -74,7 +74,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
@@ -96,11 +96,11 @@ jobs:
fail-fast: false
matrix:
os: [ ubuntu-latest, windows-latest, macos-latest ]
toolchain: [ "1.67.0", "stable" ]
toolchain: [ "1.70.0", "stable" ]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install Rust {{ matrix.toolchain }}
uses: dtolnay/rust-toolchain@master
with:
@@ -120,7 +120,7 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
- name: Install cargo-make
@@ -135,7 +135,7 @@ jobs:
fail-fast: false
matrix:
os: [ ubuntu-latest, windows-latest, macos-latest ]
toolchain: [ "1.67.0", "stable" ]
toolchain: [ "1.70.0", "stable" ]
backend: [ crossterm, termion, termwiz ]
exclude:
# termion is not supported on windows
@@ -144,13 +144,15 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install Rust ${{ matrix.toolchain }}}
uses: dtolnay/rust-toolchain@master
with:
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,12 +1,263 @@
# 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)
## Unreleased (0.24.0)
## Summary
This is a quick summary of the sections below:
- [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`
- `BorderType`: `line_symbols` is now `border_symbols` and returns `symbols::border::set`
- `Frame<'a, B: Backend>` is now `Frame<'a>`
- `Stylize` shorthands for `String` now consume the value and return `Span<'static>`
- `Spans` is removed
- [v0.23.0](#v0230)
- `Scrollbar`: `track_symbol` now takes `Option<&str>`
- `Scrollbar`: symbols moved to `symbols` module
- MSRV is now 1.67.0
- [v0.22.0](#v0220)
- serde representation of `Borders` and `Modifiers` has changed
- [v0.21.0](#v0210)
- MSRV is now 1.65.0
- `terminal::ViewPort` is now an enum
- `"".as_ref()` must be annotated to implement `Into<Text<'a>>`
- `Marker::Block` renders as a block char instead of a bar char
- [v0.20.0](#v0200)
- MSRV is now 1.63.0
- `List` no longer ignores empty strings
## v0.26.0 (unreleased)
### `Table::new()` now accepts `IntoIterator<Item: Into<Row<'a>>>` ([#774])
[#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
the `&`.
E.g.
```diff
- let table = Table::new(rows).widths(&[Constraint::Length(1)]);
// becomes
+ let table = Table::new(rows, [Constraint::Length(1)]);
```
### Layout::new() now accepts direction and constraint parameters ([#557])
[#557]: https://github.com/ratatui-org/ratatui/pull/557
Previously layout new took no parameters. Existing code should either use `Layout::default()` or
the new constructor.
```rust
let layout = layout::new()
.direction(Direction::Vertical)
.constraints([Constraint::Min(1), Constraint::Max(2)]);
// becomes either
let layout = layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(1), Constraint::Max(2)]);
// or
let layout = layout::new(Direction::Vertical, [Constraint::Min(1), Constraint::Max(2)]);
```
## [v0.24.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.24.0)
### ScrollbarState field type changed from `u16` to `usize` ([#456])
@@ -23,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])
@@ -37,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])
@@ -52,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])
@@ -68,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)
@@ -84,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])
@@ -98,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])
@@ -122,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
@@ -133,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),
});
```
@@ -150,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])
@@ -167,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)

File diff suppressed because it is too large Load Diff

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.23.0" # crate version
version = "0.25.0" # crate version
authors = ["Florian Dehau <work@fdehau.com>", "The Ratatui Developers"]
description = "A library that's all about cooking up terminal user interfaces"
documentation = "https://docs.rs/ratatui/latest/ratatui/"
@@ -18,33 +18,28 @@ exclude = [
]
autoexamples = true
edition = "2021"
rust-version = "1.67.0"
rust-version = "1.70.0"
[badges]
[dependencies]
#! The crate provides a set of optional features that can be enabled in your `cargo.toml` file.
#!
#! Generally an application will only use one backend, so you should only enable one of the following features:
## enables the [`CrosstermBackend`] backend and adds a dependency on the [Crossterm crate].
crossterm = { version = "0.27", optional = true }
## enables the [`TermionBackend`] backend and adds a dependency on the [Termion crate].
termion = { version = "2.0", optional = true }
## enables the [`TermwizBackend`] backend and adds a dependency on the [Termwiz crate].
termion = { version = "3.0", optional = true }
termwiz = { version = "0.20.0", optional = true }
serde = { version = "1", optional = true, features = ["derive"] }
bitflags = "2.3"
cassowary = "0.3"
indoc = "2.0"
itertools = "0.11"
itertools = "0.12"
paste = "1.0.2"
strum = { version = "0.25", features = ["derive"] }
time = { version = "0.3.11", optional = true, features = ["local-offset"] }
unicode-segmentation = "1.10"
unicode-width = "0.1"
document-features = { version = "0.2.7", optional = true }
lru = "0.11.1"
lru = "0.12.0"
stability = "0.1.1"
[dev-dependencies]
anyhow = "1.0.71"
@@ -53,14 +48,30 @@ 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]
default = ["crossterm"]
#! The crate provides a set of optional features that can be enabled in your `cargo.toml` file.
#!
## By default, we enable the crossterm backend as this is a reasonable choice for most applications
## as it is supported on Linux/Mac/Windows systems. We also enable the `underline-color` feature
## which allows you to set the underline color of text.
default = ["crossterm", "underline-color"]
#! Generally an application will only use one backend, so you should only enable one of the following features:
## enables the [`CrosstermBackend`] backend and adds a dependency on the [Crossterm crate].
crossterm = ["dep:crossterm"]
## enables the [`TermionBackend`] backend and adds a dependency on the [Termion crate].
termion = ["dep:termion"]
## enables the [`TermwizBackend`] backend and adds a dependency on the [Termwiz crate].
termwiz = ["dep:termwiz"]
#! The following optional features are available for all backends:
## enables serialization and deserialization of style and color types using the [Serde crate].
## This is useful if you want to save themes to a file.
@@ -77,6 +88,26 @@ all-widgets = ["widget-calendar"]
## enables the [`calendar`] widget module and adds a dependency on the [Time crate].
widget-calendar = ["dep:time"]
#! Underline color is only supported by the [`CrosstermBackend`] backend, and is not supported
#! on Windows 7.
## 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
@@ -95,6 +126,9 @@ harness = false
name = "list"
harness = false
[lib]
bench = false
[[bench]]
name = "paragraph"
harness = false
@@ -201,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"]
@@ -230,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

@@ -9,9 +9,9 @@ ALL_FEATURES = "all-widgets,macros,serde"
# Windows does not support building termion, so this avoids the build failure by providing two
# sets of flags, one for Windows and one for other platforms.
# Windows: --features=all-widgets,macros,serde,crossterm,termwiz
# Other: --all-features
ALL_FEATURES_FLAG = { source = "${CARGO_MAKE_RUST_TARGET_OS}", default_value = "--all-features", mapping = { "windows" = "--features=all-widgets,macros,serde,crossterm,termwiz" } }
# 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,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",

160
README.md
View File

@@ -21,31 +21,26 @@
<!-- cargo-rdme start -->
![Demo](https://raw.githubusercontent.com/ratatui-org/ratatui/aa09e59dc0058347f68d7c1e0c91f863c6f2b8c9/examples/demo2.gif)
![Demo](https://raw.githubusercontent.com/ratatui-org/ratatui/b33c878808c4c40591d7a2d9f9d94d6fee95a96f/examples/demo2.gif)
<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,20 +177,21 @@ Running this example produces the following output:
The library comes with a basic yet useful layout management object called [`Layout`] which
allows you to split the available space into multiple areas and then render widgets in each
area. This lets you describe a responsive terminal UI by nesting layouts. See the [Layout]
section of the [Ratatui Book] for more info.
section of the [Ratatui Website] for more info.
```rust
use ratatui::{prelude::*, widgets::*};
fn ui(frame: &mut Frame) {
let main_layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![
let main_layout = Layout::new(
Direction::Vertical,
[
Constraint::Length(1),
Constraint::Min(0),
Constraint::Length(1),
])
.split(frame.size());
],
)
.split(frame.size());
frame.render_widget(
Block::new().borders(Borders::TOP).title("Title Bar"),
main_layout[0],
@@ -205,10 +201,11 @@ fn ui(frame: &mut Frame) {
main_layout[2],
);
let inner_layout = Layout::default()
.direction(Direction::Horizontal)
.constraints(vec![Constraint::Percentage(50), Constraint::Percentage(50)])
.split(main_layout[1]);
let inner_layout = Layout::new(
Direction::Horizontal,
[Constraint::Percentage(50), Constraint::Percentage(50)],
)
.split(main_layout[1]);
frame.render_widget(
Block::default().borders(Borders::ALL).title("Left"),
inner_layout[0],
@@ -234,22 +231,23 @@ The [`style` module] provides types that represent the various styling options.
important one is [`Style`] which represents the foreground and background colors and the text
attributes of a [`Span`]. The [`style` module] also provides a [`Stylize`] trait that allows
short-hand syntax to apply a style to widgets and text. See the [Styling Text] section of the
[Ratatui Book] for more info.
[Ratatui Website] for more info.
```rust
use ratatui::{prelude::*, widgets::*};
fn ui(frame: &mut Frame) {
let areas = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![
let areas = Layout::new(
Direction::Vertical,
[
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
Constraint::Min(0),
])
.split(frame.size());
],
)
.split(frame.size());
let span1 = Span::raw("Hello ");
let span2 = Span::styled(
@@ -285,26 +283,28 @@ Running this example produces the following output:
![docsrs-styling]
[Ratatui Book]: https://ratatui.rs
[Installation]: https://ratatui.rs/installation.html
[Rendering]: https://ratatui.rs/concepts/rendering/index.html
[Application Patterns]: https://ratatui.rs/concepts/application_patterns/index.html
[Hello World tutorial]: https://ratatui.rs/tutorial/hello_world.html
[Backends]: https://ratatui.rs/concepts/backends/index.html
[Widgets]: https://ratatui.rs/how-to/widgets/index.html
[Handling Events]: https://ratatui.rs/concepts/event_handling.html
[Layout]: https://ratatui.rs/how-to/layout/index.html
[Styling Text]: https://ratatui.rs/how-to/render/style-text.html
[rust-tui-template]: https://github.com/ratatui-org/rust-tui-template
[ratatui-async-template]: https://ratatui-org.github.io/ratatui-async-template/
[simple-tui-rs]: https://github.com/pmsanford/simple-tui-rs
[Ratatui Website]: https://ratatui.rs/
[Installation]: https://ratatui.rs/installation/
[Rendering]: https://ratatui.rs/concepts/rendering/
[Application Patterns]: https://ratatui.rs/concepts/application-patterns/
[Hello World tutorial]: https://ratatui.rs/tutorials/hello-world/
[Backends]: https://ratatui.rs/concepts/backends/
[Widgets]: https://ratatui.rs/how-to/widgets/
[Handling Events]: https://ratatui.rs/concepts/event-handling/
[Layout]: https://ratatui.rs/how-to/layout/
[Styling Text]: https://ratatui.rs/how-to/render/style-text/
[template]: https://github.com/ratatui-org/template
[async-template]: https://ratatui-org.github.io/async-template
[Examples]: https://github.com/ratatui-org/ratatui/tree/main/examples
[git-cliff]: https://github.com/orhun/git-cliff
[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
[Contributing]: https://github.com/ratatui-org/ratatui/blob/main/CONTRIBUTING.md
[Breaking Changes]: https://github.com/ratatui-org/ratatui/blob/main/BREAKING-CHANGES.md
[docsrs-hello]: https://github.com/ratatui-org/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-hello.png?raw=true
[docsrs-layout]: https://github.com/ratatui-org/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-layout.png?raw=true
[docsrs-styling]: https://github.com/ratatui-org/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-styling.png?raw=true
@@ -321,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 -->
@@ -402,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
@@ -412,21 +416,21 @@ be installed with `cargo install cargo-make`).
with a React/Elm inspired approach
- [tui-realm-treeview](https://github.com/veeso/tui-realm-treeview) — Treeview component for
Tui-realm
- [tui-rs-tree-widgets](https://github.com/EdJoPaTo/tui-rs-tree-widget): Widget for tree data
- [tui-rs-tree-widgets](https://github.com/EdJoPaTo/tui-rs-tree-widget) Widget for tree data
structures.
- [tui-windows](https://github.com/markatk/tui-windows-rs) — Tui-rs abstraction to handle multiple
windows and their rendering
- [tui-textarea](https://github.com/rhysd/tui-textarea): Simple yet powerful multi-line text editor
- [tui-textarea](https://github.com/rhysd/tui-textarea) Simple yet powerful multi-line text editor
widget supporting several key shortcuts, undo/redo, text search, etc.
- [tui-input](https://github.com/sayanarijit/tui-input): TUI input library supporting multiple
- [tui-input](https://github.com/sayanarijit/tui-input) TUI input library supporting multiple
backends and tui-rs.
- [tui-term](https://github.com/a-kenji/tui-term): A pseudoterminal widget library
- [tui-term](https://github.com/a-kenji/tui-term) A pseudoterminal widget library
that enables the rendering of terminal applications as ratatui widgets.
## 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

View File

@@ -17,7 +17,6 @@ actions](.github/workflows/cd.yml) and triggered by pushing a tag.
Avoid adding the gif to the git repo as binary files tend to bloat repositories.
1. Bump the version in [Cargo.toml](Cargo.toml).
1. Bump versions in the doc comments of [lib.rs](src/lib.rs).
1. Ensure [CHANGELOG.md](CHANGELOG.md) is updated. [git-cliff](https://github.com/orhun/git-cliff)
can be used for generating the entries.
1. Ensure that any breaking changes are documented in [BREAKING-CHANGES.md](./BREAKING-CHANGES.md)

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

@@ -80,6 +80,8 @@ commit_parsers = [
{ message = "^chore\\(release\\): prepare for", skip = true },
{ message = "^chore\\(pr\\)", skip = true },
{ message = "^chore\\(pull\\)", skip = true },
{ message = "^chore\\(deps\\)", skip = true },
{ message = "^chore\\(changelog\\)", skip = true },
{ message = "^[cC]hore", group = "<!-- 07 -->Miscellaneous Tasks" },
{ body = ".*security", group = "<!-- 08 -->Security" },
{ message = "^build", group = "<!-- 09 -->Build" },

View File

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

View File

@@ -1,10 +1,32 @@
# Examples
These gifs were created using [Charm VHS](https://github.com/charmbracelet/vhs).
> [!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).
VHS has a problem rendering some background color transitions, which shows up in several examples
below. See <https://github.com/charmbracelet/vhs/issues/344> for more info. These problems don't
occur in a terminal.
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.
## Demo2
This is the demo example from the main README and crate page. Source: [demo2](./demo2/).
```shell
cargo run --example=demo2 --features="crossterm widget-calendar"
```
![Demo2][demo2.gif]
## Demo
@@ -58,7 +80,7 @@ Demonstrates the [`Calendar`](https://docs.rs/ratatui/latest/ratatui/widgets/cal
widget. Source: [calendar.rs](./calendar.rs).
```shell
cargo run --example=calendar --features=crossterm widget-calendar
cargo run --example=calendar --features="crossterm widget-calendar"
```
![Calendar][calendar.gif]
@@ -102,19 +124,23 @@ cargo run --example=colors --features=crossterm
Demonstrates the available RGB
[`Color`](https://docs.rs/ratatui/latest/ratatui/style/enum.Color.html) options. These can be used
in any style field. Source: [colors_rgb.rs](./colors_rgb.rs).
in any style field. Source: [colors_rgb.rs](./colors_rgb.rs). Uses a half block technique to render
two square-ish pixels in the space of a single rectangular terminal cell.
```shell
cargo run --example=colors_rgb --features=crossterm
```
![Colors RGB][colors_rgb.gif]
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
Demonstrates how to implement the
[`Widget`](https://docs.rs/ratatui/latest/ratatui/widgets/trait.Widget.html) trait. Source:
[custom_widget.rs](./custom_widget.rs).
[`Widget`](https://docs.rs/ratatui/latest/ratatui/widgets/trait.Widget.html) trait. Also shows mouse
interaction. Source: [custom_widget.rs](./custom_widget.rs).
```shell
cargo run --example=custom_widget --features=crossterm
@@ -127,10 +153,6 @@ cargo run --example=custom_widget --features=crossterm
Demonstrates the [`Gauge`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Gauge.html) widget.
Source: [gauge.rs](./gauge.rs).
> [!NOTE] The backgrounds render poorly when we generate this example using VHS. This problem
> doesn't generally happen during normal rendering in a terminal. See
> [vhs#344](https://github.com/charmbracelet/vhs/issues/344) for more details.
```shell
cargo run --example=gauge --features=crossterm
```
@@ -139,7 +161,7 @@ cargo run --example=gauge --features=crossterm
## Inline
Demonstrates the
Demonstrates how to use the
[`Inline`](https://docs.rs/ratatui/latest/ratatui/terminal/enum.Viewport.html#variant.Inline)
Viewport mode for ratatui apps. Source: [inline.rs](./inline.rs).
@@ -216,12 +238,20 @@ Demonstrates how to render a widget over the top of previously rendered widgets
cargo run --example=popup --features=crossterm
```
> [!NOTE] The background renders poorly after the popup when we generate this example using VHS.
> This problem doesn't generally happen during normal rendering in a terminal. See
> [vhs#344](https://github.com/charmbracelet/vhs/issues/344) for more details.
![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)
@@ -238,10 +268,6 @@ cargo run --example=scrollbar --features=crossterm
Demonstrates the [`Sparkline`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Sparkline.html)
widget. Source: [sparkline.rs](./sparkline.rs).
> [!NOTE] The background renders poorly in the second sparkline when we generate this example using
> VHS. This problem doesn't generally happen during normal rendering in a terminal. See
> [vhs#344](https://github.com/charmbracelet/vhs/issues/344) for more details.
```shell
cargo run --example=sparkline --features=crossterm
```
@@ -274,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
@@ -292,15 +319,16 @@ To update these examples in bulk:
examples/generate.bash
```
-->
[barchart.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/barchart.gif?raw=true
[block.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/block.gif?raw=true
[calendar.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/calendar.gif?raw=true
[canvas.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/canvas.gif?raw=true
[chart.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/chart.gif?raw=true
[colors.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/colors.gif?raw=true
[colors_rgb.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/colors_rgb.gif?raw=true
[custom_widget.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/custom_widget.gif?raw=true
[demo.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/demo.gif?raw=true
[demo2.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/demo2.gif?raw=true
[gauge.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/gauge.gif?raw=true
[hello_world.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/hello_world.gif?raw=true
[inline.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/inline.gif?raw=true
@@ -310,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

@@ -119,9 +119,7 @@ fn run_app<B: Backend>(
loop {
terminal.draw(|f| ui(f, &app))?;
let timeout = tick_rate
.checked_sub(last_tick.elapsed())
.unwrap_or_else(|| Duration::from_secs(0));
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if crossterm::event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
if let KeyCode::Char('q') = key.code {
@@ -136,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)].as_ref())
.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))
@@ -148,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)].as_ref())
.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

@@ -1,7 +1,7 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/barchart.tape`
Output "target/barchart.gif"
Set Theme "OceanicMaterial"
Set Theme "Aardvark Blue"
Set Width 1200
Set Height 800
Hide

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(vec![Constraint::Length(1), Constraint::Min(0)])
.split(area);
let title_area = layout[0];
let main_areas = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![Constraint::Max(4); 9])
.split(layout[1])
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(vec![Constraint::Percentage(50), Constraint::Percentage(50)])
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(area)
.to_vec()
})

View File

@@ -1,7 +1,7 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/block.tape`
Output "target/block.gif"
Set Theme "OceanicMaterial"
Set Theme "Aardvark Blue"
Set Width 1200
Set Height 1200
Hide

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,49 +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),
]
.as_ref(),
);
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),
]
.as_ref(),
);
list_layout.split(*area)
}
fn make_dates(current_year: i32) -> CalendarEventStore {
let mut list = CalendarEventStore::today(
Style::default()
@@ -181,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),
@@ -194,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));
@@ -204,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));
@@ -215,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)
@@ -231,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);
@@ -247,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);
@@ -261,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

@@ -1,7 +1,7 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/calendar.tape`
Output "target/calendar.gif"
Set Theme "OceanicMaterial"
Set Theme "Aardvark Blue"
Set Width 1200
Set Height 800
Hide

View File

@@ -1,28 +1,29 @@
use std::{
error::Error,
io,
io::{self, stdout, Stdout},
time::{Duration, Instant},
};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
event::{self, Event, KeyCode},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
};
use ratatui::{
prelude::*,
widgets::{canvas::*, *},
};
fn main() -> io::Result<()> {
App::run()
}
struct App {
x: f64,
y: f64,
ball: Rectangle,
ball: Circle,
playground: Rect,
vx: f64,
vy: f64,
dir_x: bool,
dir_y: bool,
tick_count: u64,
marker: Marker,
}
@@ -32,155 +33,162 @@ impl App {
App {
x: 0.0,
y: 0.0,
ball: Rectangle {
x: 10.0,
y: 30.0,
width: 10.0,
height: 10.0,
ball: Circle {
x: 20.0,
y: 40.0,
radius: 10.0,
color: Color::Yellow,
},
playground: Rect::new(10, 10, 100, 100),
playground: Rect::new(10, 10, 200, 100),
vx: 1.0,
vy: 1.0,
dir_x: true,
dir_y: true,
tick_count: 0,
marker: Marker::Dot,
}
}
fn on_tick(&mut self) {
self.tick_count += 1;
// only change marker every 4 ticks (1s) to avoid stroboscopic effect
if (self.tick_count % 4) == 0 {
self.marker = match self.marker {
Marker::Dot => Marker::Block,
Marker::Block => Marker::Bar,
Marker::Bar => Marker::Braille,
Marker::Braille => Marker::Dot,
};
}
if self.ball.x < self.playground.left() as f64
|| self.ball.x + self.ball.width > self.playground.right() as f64
{
self.dir_x = !self.dir_x;
}
if self.ball.y < self.playground.top() as f64
|| self.ball.y + self.ball.height > self.playground.bottom() as f64
{
self.dir_y = !self.dir_y;
}
if self.dir_x {
self.ball.x += self.vx;
} else {
self.ball.x -= self.vx;
}
if self.dir_y {
self.ball.y += self.vy;
} else {
self.ball.y -= self.vy
}
}
}
fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
// create app and run it
let tick_rate = Duration::from_millis(250);
let app = App::new();
let res = run_app(&mut terminal, app, tick_rate);
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
}
Ok(())
}
fn run_app<B: Backend>(
terminal: &mut Terminal<B>,
mut app: App,
tick_rate: Duration,
) -> io::Result<()> {
let mut last_tick = Instant::now();
loop {
terminal.draw(|f| ui(f, &app))?;
let timeout = tick_rate
.checked_sub(last_tick.elapsed())
.unwrap_or_else(|| Duration::from_secs(0));
if event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Char('q') => {
return Ok(());
pub fn run() -> io::Result<()> {
let mut terminal = init_terminal()?;
let mut app = App::new();
let mut last_tick = Instant::now();
let tick_rate = Duration::from_millis(16);
loop {
let _ = terminal.draw(|frame| app.ui(frame));
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Char('q') => break,
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,
_ => {}
}
KeyCode::Down => {
app.y += 1.0;
}
KeyCode::Up => {
app.y -= 1.0;
}
KeyCode::Right => {
app.x += 1.0;
}
KeyCode::Left => {
app.x -= 1.0;
}
_ => {}
}
}
if last_tick.elapsed() >= tick_rate {
app.on_tick();
last_tick = Instant::now();
}
}
restore_terminal()
}
fn on_tick(&mut self) {
self.tick_count += 1;
// only change marker every 180 ticks (3s) to avoid stroboscopic effect
if (self.tick_count % 180) == 0 {
self.marker = match self.marker {
Marker::Dot => Marker::Braille,
Marker::Braille => Marker::Block,
Marker::Block => Marker::HalfBlock,
Marker::HalfBlock => Marker::Bar,
Marker::Bar => Marker::Dot,
};
}
// bounce the ball by flipping the velocity vector
let ball = &self.ball;
let playground = self.playground;
if ball.x - ball.radius < playground.left() as f64
|| ball.x + ball.radius > playground.right() as f64
{
self.vx = -self.vx;
}
if ball.y - ball.radius < playground.top() as f64
|| ball.y + ball.radius > playground.bottom() as f64
{
self.vy = -self.vy;
}
if last_tick.elapsed() >= tick_rate {
app.on_tick();
last_tick = Instant::now();
}
self.ball.x += self.vx;
self.ball.y += self.vy;
}
fn ui(&self, frame: &mut Frame) {
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);
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 + '_ {
Canvas::default()
.block(Block::default().borders(Borders::ALL).title("World"))
.marker(self.marker)
.paint(|ctx| {
ctx.draw(&Map {
color: Color::Green,
resolution: MapResolution::High,
});
ctx.print(self.x, -self.y, "You are here".yellow());
})
.x_bounds([-180.0, 180.0])
.y_bounds([-90.0, 90.0])
}
fn pong_canvas(&self) -> impl Widget + '_ {
Canvas::default()
.block(Block::default().borders(Borders::ALL).title("Pong"))
.marker(self.marker)
.paint(|ctx| {
ctx.draw(&self.ball);
})
.x_bounds([10.0, 210.0])
.y_bounds([10.0, 110.0])
}
fn boxes_canvas(&self, area: Rect) -> impl Widget {
let (left, right, bottom, top) =
(0.0, area.width as f64, 0.0, area.height as f64 * 2.0 - 4.0);
Canvas::default()
.block(Block::default().borders(Borders::ALL).title("Rects"))
.marker(self.marker)
.x_bounds([left, right])
.y_bounds([bottom, top])
.paint(|ctx| {
for i in 0..=11 {
ctx.draw(&Rectangle {
x: (i * i + 3 * i) as f64 / 2.0 + 2.0,
y: 2.0,
width: i as f64,
height: i as f64,
color: Color::Red,
});
ctx.draw(&Rectangle {
x: (i * i + 3 * i) as f64 / 2.0 + 2.0,
y: 21.0,
width: i as f64,
height: i as f64,
color: Color::Blue,
});
}
for i in 0..100 {
if i % 10 != 0 {
ctx.print(i as f64 + 1.0, 0.0, format!("{i}", i = i % 10));
}
if i % 2 == 0 && i % 10 != 0 {
ctx.print(0.0, i as f64, format!("{i}", i = i % 10));
}
}
})
}
}
fn ui(f: &mut Frame, app: &App) {
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(f.size());
let canvas = Canvas::default()
.block(Block::default().borders(Borders::ALL).title("World"))
.marker(app.marker)
.paint(|ctx| {
ctx.draw(&Map {
color: Color::White,
resolution: MapResolution::High,
});
ctx.print(app.x, -app.y, "You are here".yellow());
})
.x_bounds([-180.0, 180.0])
.y_bounds([-90.0, 90.0]);
f.render_widget(canvas, chunks[0]);
let canvas = Canvas::default()
.block(Block::default().borders(Borders::ALL).title("Pong"))
.marker(app.marker)
.paint(|ctx| {
ctx.draw(&app.ball);
})
.x_bounds([10.0, 110.0])
.y_bounds([10.0, 110.0]);
f.render_widget(canvas, chunks[1]);
fn init_terminal() -> io::Result<Terminal<CrosstermBackend<Stdout>>> {
enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
Terminal::new(CrosstermBackend::new(stdout()))
}
fn restore_terminal() -> io::Result<()> {
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
Ok(())
}

View File

@@ -1,12 +1,13 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/canvas.tape`
Output "target/canvas.gif"
Set Theme "OceanicMaterial"
Set Theme "Aardvark Blue"
Set FontSize 12
Set Width 1200
Set Height 800
Hide
Type "cargo run --example=canvas --features=crossterm"
Enter
Sleep 1s
Sleep 2s
Show
Sleep 5s
Sleep 30s

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 {
@@ -125,9 +117,7 @@ fn run_app<B: Backend>(
loop {
terminal.draw(|f| ui(f, &app))?;
let timeout = tick_rate
.checked_sub(last_tick.elapsed())
.unwrap_or_else(|| Duration::from_secs(0));
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if crossterm::event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
if let KeyCode::Char('q') = key.code {
@@ -142,19 +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),
]
.as_ref(),
)
.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]),
@@ -199,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

@@ -1,7 +1,7 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/chart.tape`
Output "target/chart.gif"
Set Theme "OceanicMaterial"
Set Theme "Aardvark Blue"
Set Width 1200
Set Height 800
Hide

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(vec![
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(vec![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(vec![Constraint::Length(1); 2])
let layout = Layout::vertical([Constraint::Length(1); 2])
.split(inner)
.iter()
.flat_map(|area| {
Layout::default()
.direction(Direction::Horizontal)
.constraints(vec![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(vec![Constraint::Length(1); 2])
let layout = Layout::vertical([Constraint::Length(1); 2])
.split(inner)
.iter()
.flat_map(|area| {
Layout::default()
.direction(Direction::Horizontal)
.constraints(vec![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(vec![
Constraint::Length(1), // 0 - 15
Constraint::Length(1), // blank
Constraint::Min(6), // 16 - 123
Constraint::Length(1), // blank
Constraint::Min(6), // 124 - 231
Constraint::Length(1), // blank
])
.split(inner);
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(vec![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(vec![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(vec![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(vec![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(vec![
Constraint::Length(1), // 232 - 243
Constraint::Length(1), // 244 - 255
])
.split(area)
.iter()
.flat_map(|area| {
Layout::default()
.direction(Direction::Horizontal)
.constraints(vec![Constraint::Length(6); 12])
.split(*area)
.to_vec()
})
.collect_vec();
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

@@ -1,13 +1,7 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/colors.tape`
Output "target/colors.gif"
# The OceanicMaterial theme is a good choice for this example (Obsidian is almost as good) because:
# - Black is dark and distinct from the default background
# - White is light and distinct from the default foreground
# - Normal and bright colors are distinct
# - Black and DarkGray are distinct
# - White and Gray are distinct
Set Theme "OceanicMaterial"
Set Theme "Aardvark Blue"
Set Width 1200
Set Height 1410
Hide

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(vec![Constraint::Length(1), Constraint::Min(0)])
.split(area)
}
fn render_title(area: Rect, buf: &mut Buffer) {
Paragraph::new("colors_rgb example. Press q to quit")
.dark_gray()
.alignment(Alignment::Center)
.render(area, buf);
}
/// Render a colored grid of half block characters (`"▀"`) each with a different RGB color.
fn render_colors(area: Rect, buf: &mut Buffer) {
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,18 +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"
# The OceanicMaterial theme is a good choice for this example (Obsidian is almost as good) because:
# - Black is dark and distinct from the default background
# - White is light and distinct from the default foreground
# - Normal and bright colors are distinct
# - Black and DarkGray are distinct
# - White and Gray are distinct
Set Theme "OceanicMaterial"
Set Theme "Aardvark Blue"
Set Width 1200
Set Height 1410
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"
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(vec![
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(vec![
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;
@@ -252,8 +244,12 @@ fn handle_mouse_event(
_ => 2,
};
if old_selected_button != *selected_button {
button_states[old_selected_button] = State::Normal;
button_states[*selected_button] = State::Selected;
if button_states[old_selected_button] != State::Active {
button_states[old_selected_button] = State::Normal;
}
if button_states[*selected_button] != State::Active {
button_states[*selected_button] = State::Selected;
}
}
}
MouseEventKind::Down(MouseButton::Left) => {

View File

@@ -1,7 +1,7 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/custom_widget.tape`
Output "target/custom_widget.gif"
Set Theme "OceanicMaterial"
Set Theme "Aardvark Blue"
Set Width 760
Set Height 260
Hide

View File

@@ -1,7 +1,7 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/demo.tape`
Output "target/demo.gif"
Set Theme "OceanicMaterial"
Set Theme "Aardvark Blue"
Set Width 1200
Set Height 1200
Set PlaybackSpeed 0.5

View File

@@ -50,18 +50,16 @@ fn run_app<B: Backend>(
loop {
terminal.draw(|f| ui::draw(f, &mut app))?;
let timeout = tick_rate
.checked_sub(last_tick.elapsed())
.unwrap_or_else(|| Duration::from_secs(0));
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if crossterm::event::poll(timeout)? {
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

@@ -1,6 +1,5 @@
use std::{
error::Error,
io,
time::{Duration, Instant},
};
@@ -32,26 +31,24 @@ fn run_app(
terminal: &mut Terminal<TermwizBackend>,
mut app: App,
tick_rate: Duration,
) -> io::Result<()> {
) -> Result<(), Box<dyn Error>> {
let mut last_tick = Instant::now();
loop {
terminal.draw(|f| ui::draw(f, &mut app))?;
let timeout = tick_rate
.checked_sub(last_tick.elapsed())
.unwrap_or_else(|| Duration::from_secs(0));
if let Ok(Some(input)) = terminal
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if let Some(input) = terminal
.backend_mut()
.buffered_terminal_mut()
.terminal()
.poll_input(Some(timeout))
.poll_input(Some(timeout))?
{
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)].as_ref())
.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,33 +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),
]
.as_ref(),
)
.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),
]
.as_ref(),
)
.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);
@@ -102,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)].as_ref())
let chunks = Layout::vertical([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(chunks[0]);
{
let chunks = Layout::default()
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.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
@@ -279,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)].as_ref())
.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)
@@ -295,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()
@@ -365,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,
@@ -399,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

@@ -2,7 +2,7 @@
# To run this script, install vhs and run `vhs ./examples/demo.tape`
Output "target/demo2-social.gif"
Set Theme "OceanicMaterial"
Set Theme "Aardvark Blue"
# Github social preview size (1280x640 with 80px padding) and must be < 1MB
# This puts some constraints on the amount of interactivity we can do.
Set Width 1280

View File

@@ -2,7 +2,7 @@
# To run this script, install vhs and run `vhs ./examples/demo.tape`
# NOTE: Requires VHS 0.6.1 or later for Screenshot support
Output "target/demo2.gif"
Set Theme "OceanicMaterial"
Set Theme "Aardvark Blue"
# The reason for this strange size is that the social preview image for this
# demo is 1280x64 with 80 pixels of padding on each side. We want a version
# without the padding for README.md, etc.

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(vec![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,
@@ -111,11 +110,11 @@ fn render_map(selected_row: usize, area: Rect, buf: &mut Buffer) {
.padding(Padding::new(1, 0, 1, 0))
.style(theme.style),
)
.marker(Marker::Dot)
.marker(Marker::HalfBlock)
// picked to show Australia for the demo as it's the most interesting part of the map
// (and the only part with hops ;))
.x_bounds([113.0, 154.0])
.y_bounds([-42.0, -11.0])
.x_bounds([112.0, 155.0])
.y_bounds([-46.0, -11.0])
.paint(|context| {
context.draw(&map);
if let Some(path) = path {

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(vec![
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(vec![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(vec![
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

@@ -2,7 +2,7 @@
# To run this script, install vhs and run `vhs ./examples/demo.tape`
# NOTE: Requires VHS 0.6.1 or later for Screenshot support
Output "target/docsrs.gif"
Set Theme "OceanicMaterial"
Set Theme "Aardvark Blue"
# The reason for this strange size is that the social preview image for this
# demo is 1280x64 with 80 pixels of padding on each side. We want a version
# without the padding for README.md, etc.

View File

@@ -1,17 +1,31 @@
use std::{
error::Error,
io,
time::{Duration, Instant},
io::{self, stdout, Stdout},
time::Duration,
};
use anyhow::Result;
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
event::{self, Event, KeyCode, KeyEventKind},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
};
use ratatui::{prelude::*, widgets::*};
use ratatui::{
prelude::*,
widgets::{block::Title, *},
};
fn main() -> Result<()> {
App::run()
}
struct App {
term: Term,
should_quit: bool,
state: AppState,
}
#[derive(Debug, Default, Clone, Copy)]
struct AppState {
progress1: u16,
progress2: u16,
progress3: f64,
@@ -19,141 +33,142 @@ struct App {
}
impl App {
fn new() -> App {
App {
progress1: 0,
progress2: 0,
progress3: 0.45,
progress4: 0,
fn run() -> Result<()> {
// run at ~10 fps minus the time it takes to draw
let timeout = Duration::from_secs_f32(1.0 / 10.0);
let mut app = Self::start()?;
while !app.should_quit {
app.update();
app.draw()?;
app.handle_events(timeout)?;
}
app.stop()?;
Ok(())
}
fn on_tick(&mut self) {
self.progress1 += 1;
if self.progress1 > 100 {
self.progress1 = 0;
}
self.progress2 += 2;
if self.progress2 > 100 {
self.progress2 = 0;
}
self.progress3 += 0.001;
if self.progress3 > 1.0 {
self.progress3 = 0.0;
}
self.progress4 += 1;
if self.progress4 > 100 {
self.progress4 = 0;
}
}
}
fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
// create app and run it
let tick_rate = Duration::from_millis(250);
let app = App::new();
let res = run_app(&mut terminal, app, tick_rate);
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
fn start() -> Result<Self> {
Ok(App {
term: Term::start()?,
should_quit: false,
state: AppState {
progress1: 0,
progress2: 0,
progress3: 0.0,
progress4: 0,
},
})
}
Ok(())
}
fn stop(&mut self) -> Result<()> {
Term::stop()?;
Ok(())
}
fn run_app<B: Backend>(
terminal: &mut Terminal<B>,
mut app: App,
tick_rate: Duration,
) -> io::Result<()> {
let mut last_tick = Instant::now();
loop {
terminal.draw(|f| ui(f, &app))?;
fn update(&mut self) {
self.state.progress1 = (self.state.progress1 + 4).min(100);
self.state.progress2 = (self.state.progress2 + 3).min(100);
self.state.progress3 = (self.state.progress3 + 0.02).min(1.0);
self.state.progress4 = (self.state.progress4 + 1).min(100);
}
let timeout = tick_rate
.checked_sub(last_tick.elapsed())
.unwrap_or_else(|| Duration::from_secs(0));
if crossterm::event::poll(timeout)? {
fn draw(&mut self) -> Result<()> {
self.term.draw(|frame| {
let state = self.state;
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]);
Self::render_gauge4(state.progress4, frame, layout[3]);
})?;
Ok(())
}
fn handle_events(&mut self, timeout: Duration) -> io::Result<()> {
if event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
if let KeyCode::Char('q') = key.code {
return Ok(());
if key.kind == KeyEventKind::Press {
if let KeyCode::Char('q') = key.code {
self.should_quit = true;
}
}
}
}
if last_tick.elapsed() >= tick_rate {
app.on_tick();
last_tick = Instant::now();
}
Ok(())
}
fn render_gauge1(progress: u16, frame: &mut Frame, area: Rect) {
let title = Self::title_block("Gauge with percentage progress");
let gauge = Gauge::default()
.block(title)
.gauge_style(Style::new().light_red())
.percent(progress);
frame.render_widget(gauge, area);
}
fn render_gauge2(progress: u16, frame: &mut Frame, area: Rect) {
let title = Self::title_block("Gauge with percentage progress and custom label");
let label = format!("{}/100", progress);
let gauge = Gauge::default()
.block(title)
.gauge_style(Style::new().blue().on_light_blue())
.percent(progress)
.label(label);
frame.render_widget(gauge, area);
}
fn render_gauge3(progress: f64, frame: &mut Frame, area: Rect) {
let title =
Self::title_block("Gauge with ratio progress, custom label with style, and unicode");
let label = Span::styled(
format!("{:.2}%", progress * 100.0),
Style::new().red().italic().bold(),
);
let gauge = Gauge::default()
.block(title)
.gauge_style(Style::default().fg(Color::Yellow))
.ratio(progress)
.label(label)
.use_unicode(true);
frame.render_widget(gauge, area);
}
fn render_gauge4(progress: u16, frame: &mut Frame, area: Rect) {
let title = Self::title_block("Gauge with percentage progress and label");
let label = format!("{}/100", progress);
let gauge = Gauge::default()
.block(title)
.gauge_style(Style::new().green().italic())
.percent(progress)
.label(label);
frame.render_widget(gauge, area);
}
fn title_block(title: &str) -> Block {
let title = Title::from(title).alignment(Alignment::Center);
Block::default().title(title).borders(Borders::TOP)
}
}
fn ui(f: &mut Frame, app: &App) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
]
.as_ref(),
)
.split(f.size());
let gauge = Gauge::default()
.block(Block::default().title("Gauge1").borders(Borders::ALL))
.gauge_style(Style::default().fg(Color::Yellow))
.percent(app.progress1);
f.render_widget(gauge, chunks[0]);
let label = format!("{}/100", app.progress2);
let gauge = Gauge::default()
.block(Block::default().title("Gauge2").borders(Borders::ALL))
.gauge_style(Style::default().fg(Color::Magenta).bg(Color::Green))
.percent(app.progress2)
.label(label);
f.render_widget(gauge, chunks[1]);
let label = Span::styled(
format!("{:.2}%", app.progress3 * 100.0),
Style::default()
.fg(Color::Red)
.add_modifier(Modifier::ITALIC | Modifier::BOLD),
);
let gauge = Gauge::default()
.block(Block::default().title("Gauge3").borders(Borders::ALL))
.gauge_style(Style::default().fg(Color::Yellow))
.ratio(app.progress3)
.label(label)
.use_unicode(true);
f.render_widget(gauge, chunks[2]);
let label = format!("{}/100", app.progress4);
let gauge = Gauge::default()
.block(Block::default().title("Gauge4").borders(Borders::ALL))
.gauge_style(
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::ITALIC),
)
.percent(app.progress4)
.label(label);
f.render_widget(gauge, chunks[3]);
struct Term {
terminal: Terminal<CrosstermBackend<Stdout>>,
}
impl Term {
pub fn start() -> io::Result<Term> {
stdout().execute(EnterAlternateScreen)?;
enable_raw_mode()?;
let terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
Ok(Self { terminal })
}
pub fn stop() -> io::Result<()> {
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
Ok(())
}
fn draw(&mut self, frame: impl FnOnce(&mut Frame)) -> Result<()> {
self.terminal.draw(frame)?;
Ok(())
}
}

View File

@@ -1,9 +1,9 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/gauge.tape`
Output "target/gauge.gif"
Set Theme "OceanicMaterial"
Set Theme "Aardvark Blue"
Set Width 1200
Set Height 600
Set Height 550
Hide
Type "cargo run --example=gauge --features=crossterm"
Enter

View File

@@ -5,7 +5,11 @@
# - cargo: https://doc.rust-lang.org/cargo/getting-started/installation.html
# - gh: https://github.com/cli/cli
# - git: https://git-scm.com/
# - vhs: https://github.com/charmbracelet/vhs
# - vhs: https://github.com/charmbracelet/vhs - currently this needs to be installed from the
# main branch, as the latest release doesn't support the theme we use or the Screenshot
# command. Install using `go install github.com/charmbracelet/vhs@main``
# - go: https://golang.org/doc/install
# - ttyd: https://github.com/tsl0922/ttyd
# Exit on error. Append "|| true" if you expect an error.
set -o errexit
@@ -21,11 +25,10 @@ set -o pipefail
# ensure that running each example doesn't have to wait for the build
cargo build --examples --features=crossterm,all-widgets
for tape in examples/*.tape
do
gif=${tape/examples\/}
for tape in examples/*.tape; do
gif=${tape/examples\//}
gif=${gif/.tape/.gif}
vhs $tape --quiet
~/go/bin/vhs $tape --quiet
# this can be pasted into the examples README.md
echo "[${gif}]: https://github.com/ratatui-org/ratatui/blob/images/examples/${gif}?raw=true"
done
@@ -35,4 +38,4 @@ cp target/*.gif examples/
git add examples/*.gif
git commit -m 'docs(examples): update images'
gh pr create
git sw main
git sw -

View File

@@ -1,7 +1,7 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/hello_world.tape`
Output "target/hello_world.gif"
Set Theme "OceanicMaterial"
Set Theme "Aardvark Blue"
Set Width 1200
Set Height 200
Hide

View File

@@ -98,9 +98,7 @@ fn input_handling(tx: mpsc::Sender<Event>) {
let mut last_tick = Instant::now();
loop {
// poll for tick rate duration, if no events, sent tick event.
let timeout = tick_rate
.checked_sub(last_tick.elapsed())
.unwrap_or_else(|| Duration::from_secs(0));
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if crossterm::event::poll(timeout).unwrap() {
match crossterm::event::read().unwrap() {
crossterm::event::Event::Key(key) => tx.send(Event::Input(key)).unwrap(),
@@ -217,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(vec![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();
@@ -233,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(vec![Constraint::Percentage(20), Constraint::Percentage(80)])
.split(chunks[1]);
f.render_widget(progress, progress_area);
// in progress downloads
let items: Vec<ListItem> = downloads
@@ -261,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

@@ -1,7 +1,7 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/inline.tape`
Output "target/inline.gif"
Set Theme "OceanicMaterial"
Set Theme "Aardvark Blue"
Set Width 1200
Set Height 600
Type "cargo run --example=inline --features=crossterm"

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(vec![
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(vec![
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(vec![
Length(14),
Length(14),
Length(14),
Length(14),
Length(14),
Min(0), // fills remaining space
])
.split(*area)
.iter()
.copied()
.take(5) // ignore Min(0)
.collect_vec()
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

@@ -1,7 +1,7 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/layout.tape`
Output "target/layout.gif"
Set Theme "OceanicMaterial"
Set Theme "Aardvark Blue"
Set Width 1200
Set Height 1410
Hide

View File

@@ -175,17 +175,15 @@ fn run_app<B: Backend>(
loop {
terminal.draw(|f| ui(f, &mut app))?;
let timeout = tick_rate
.checked_sub(last_tick.elapsed())
.unwrap_or_else(|| Duration::from_secs(0));
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if crossterm::event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
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(),
_ => {}
}
}
@@ -200,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)].as_ref())
.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
@@ -234,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.
@@ -266,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,
@@ -275,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

@@ -1,7 +1,7 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/list.tape`
Output "target/list.gif"
Set Theme "OceanicMaterial"
Set Theme "Aardvark Blue"
Set Width 1200
Set Height 600
Hide

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(vec![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(vec![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(vec![Constraint::Percentage(20); 5])
Layout::horizontal([Constraint::Percentage(20); 5])
.split(*area)
.to_vec()
})

View File

@@ -1,7 +1,7 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/modifiers.tape`
Output "target/modifiers.gif"
Set Theme "OceanicMaterial"
Set Theme "Aardvark Blue"
Set Width 1200
Set Height 1460
Hide

View File

@@ -1,7 +1,7 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/panic.tape`
Output "target/panic.gif"
Set Theme "OceanicMaterial"
Set Theme "Aardvark Blue"
Set Width 1200
Set Height 600
Type "cargo run --example=panic --features=crossterm"

View File

@@ -64,9 +64,7 @@ fn run_app<B: Backend>(
loop {
terminal.draw(|f| ui(f, &app))?;
let timeout = tick_rate
.checked_sub(last_tick.elapsed())
.unwrap_or_else(|| Duration::from_secs(0));
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if crossterm::event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
if let KeyCode::Char('q') = key.code {
@@ -92,18 +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),
]
.as_ref(),
)
.split(size);
let layout = Layout::vertical([Constraint::Ratio(1, 4); 4]).split(size);
let text = vec![
Line::from("This is a line "),
@@ -134,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))
@@ -155,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

@@ -1,7 +1,7 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/paragraph.tape`
Output "target/paragraph.gif"
Set Theme "OceanicMaterial"
Set Theme "Aardvark Blue"
Set Width 1200
Set Height 1800
Hide

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)].as_ref())
.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,27 +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),
]
.as_ref(),
)
.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),
]
.as_ref(),
)
.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]
}

View File

@@ -1,7 +1,7 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/popup.tape`
Output "target/popup.gif"
Set Theme "OceanicMaterial"
Set Theme "Aardvark Blue"
Set Width 1200
Set Height 600
Hide

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

@@ -57,9 +57,7 @@ fn run_app<B: Backend>(
loop {
terminal.draw(|f| ui(f, &mut app))?;
let timeout = tick_rate
.checked_sub(last_tick.elapsed())
.unwrap_or_else(|| Duration::from_secs(0));
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if crossterm::event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
match key.code {
@@ -102,22 +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),
]
.as_ref(),
)
.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 "),
@@ -150,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

@@ -1,7 +1,7 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/scrollbar.tape`
Output "target/scrollbar.gif"
Set Theme "OceanicMaterial"
Set Theme "Aardvark Blue"
Set Width 1200
Set Height 1200
Hide

View File

@@ -109,9 +109,7 @@ fn run_app<B: Backend>(
loop {
terminal.draw(|f| ui(f, &app))?;
let timeout = tick_rate
.checked_sub(last_tick.elapsed())
.unwrap_or_else(|| Duration::from_secs(0));
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if crossterm::event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
if let KeyCode::Char('q') = key.code {
@@ -127,17 +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),
]
.as_ref(),
)
.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,7 +1,7 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/sparkline.tape`
Output "target/sparkline.gif"
Set Theme "OceanicMaterial"
Set Theme "Aardvark Blue"
Set Width 1200
Set Height 600
Hide

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)].as_ref())
.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

@@ -1,16 +1,19 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/table.tape`
Output "target/table.gif"
Set Theme "OceanicMaterial"
Set 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)].as_ref())
.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

@@ -1,7 +1,7 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/tabs.tape`
Output "target/tabs.gif"
Set Theme "OceanicMaterial"
Set Theme "Aardvark Blue"
Set Width 1200
Set Height 300
Hide

View File

@@ -172,17 +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),
]
.as_ref(),
)
.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 => (
@@ -206,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 {
@@ -217,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
@@ -229,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,
)
}
}
@@ -247,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

@@ -1,7 +1,7 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/user_input.tape`
Output "target/user_input.gif"
Set Theme "OceanicMaterial"
Set Theme "Aardvark Blue"
Set Width 1200
Set Height 600
Hide

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};
@@ -140,6 +141,7 @@ pub enum ClearType {
}
/// The window size in characters (columns / rows) as well as pixels.
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub struct WindowSize {
/// Size of the window in characters (columns / rows).
pub columns_rows: Size,
@@ -227,7 +229,7 @@ pub trait Backend {
/// [`get_cursor`]: Backend::get_cursor
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()>;
/// Clears the whole terminal scree
/// Clears the whole terminal screen
///
/// # Example
///

View File

@@ -4,12 +4,14 @@
//! [Crossterm]: https://crates.io/crates/crossterm
use std::io::{self, Write};
#[cfg(feature = "underline-color")]
use crossterm::style::SetUnderlineColor;
use crossterm::{
cursor::{Hide, MoveTo, Show},
execute, queue,
style::{
Attribute as CAttribute, Color as CColor, Print, SetAttribute, SetBackgroundColor,
SetForegroundColor, SetUnderlineColor,
Attribute as CAttribute, Attributes as CAttributes, Color as CColor, ContentStyle, Print,
SetAttribute, SetBackgroundColor, SetForegroundColor,
},
terminal::{self, Clear},
};
@@ -19,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.
@@ -41,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,
@@ -124,6 +127,7 @@ where
{
let mut fg = Color::Reset;
let mut bg = Color::Reset;
#[cfg(feature = "underline-color")]
let mut underline_color = Color::Reset;
let mut modifier = Modifier::empty();
let mut last_pos: Option<(u16, u16)> = None;
@@ -151,22 +155,31 @@ where
queue!(self.writer, SetBackgroundColor(color))?;
bg = cell.bg;
}
#[cfg(feature = "underline-color")]
if cell.underline_color != underline_color {
let color = CColor::from(cell.underline_color);
queue!(self.writer, SetUnderlineColor(color))?;
underline_color = cell.underline_color;
}
queue!(self.writer, Print(&cell.symbol))?;
queue!(self.writer, Print(cell.symbol()))?;
}
queue!(
#[cfg(feature = "underline-color")]
return queue!(
self.writer,
SetForegroundColor(CColor::Reset),
SetBackgroundColor(CColor::Reset),
SetUnderlineColor(CColor::Reset),
SetAttribute(CAttribute::Reset)
)
SetAttribute(CAttribute::Reset),
);
#[cfg(not(feature = "underline-color"))]
return queue!(
self.writer,
SetForegroundColor(CColor::Reset),
SetBackgroundColor(CColor::Reset),
SetAttribute(CAttribute::Reset),
);
}
fn hide_cursor(&mut self) -> io::Result<()> {
@@ -262,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.
@@ -332,3 +371,303 @@ impl ModifierDiff {
Ok(())
}
}
impl From<CAttribute> for Modifier {
fn from(value: CAttribute) -> Self {
// `Attribute*s*` (note the *s*) contains multiple `Attribute`
// We convert `Attribute` to `Attribute*s*` (containing only 1 value) to avoid implementing
// the conversion again
Modifier::from(CAttributes::from(value))
}
}
impl From<CAttributes> for Modifier {
fn from(value: CAttributes) -> Self {
let mut res = Modifier::empty();
if value.has(CAttribute::Bold) {
res |= Modifier::BOLD;
}
if value.has(CAttribute::Dim) {
res |= Modifier::DIM;
}
if value.has(CAttribute::Italic) {
res |= Modifier::ITALIC;
}
if value.has(CAttribute::Underlined)
|| value.has(CAttribute::DoubleUnderlined)
|| value.has(CAttribute::Undercurled)
|| value.has(CAttribute::Underdotted)
|| value.has(CAttribute::Underdashed)
{
res |= Modifier::UNDERLINED;
}
if value.has(CAttribute::SlowBlink) {
res |= Modifier::SLOW_BLINK;
}
if value.has(CAttribute::RapidBlink) {
res |= Modifier::RAPID_BLINK;
}
if value.has(CAttribute::Reverse) {
res |= Modifier::REVERSED;
}
if value.has(CAttribute::Hidden) {
res |= Modifier::HIDDEN;
}
if value.has(CAttribute::CrossedOut) {
res |= Modifier::CROSSED_OUT;
}
res
}
}
impl From<ContentStyle> for Style {
fn from(value: ContentStyle) -> Self {
let mut sub_modifier = Modifier::empty();
if value.attributes.has(CAttribute::NoBold) {
sub_modifier |= Modifier::BOLD;
}
if value.attributes.has(CAttribute::NoItalic) {
sub_modifier |= Modifier::ITALIC;
}
if value.attributes.has(CAttribute::NotCrossedOut) {
sub_modifier |= Modifier::CROSSED_OUT;
}
if value.attributes.has(CAttribute::NoUnderline) {
sub_modifier |= Modifier::UNDERLINED;
}
if value.attributes.has(CAttribute::NoHidden) {
sub_modifier |= Modifier::HIDDEN;
}
if value.attributes.has(CAttribute::NoBlink) {
sub_modifier |= Modifier::RAPID_BLINK | Modifier::SLOW_BLINK;
}
if value.attributes.has(CAttribute::NoReverse) {
sub_modifier |= Modifier::REVERSED;
}
Self {
fg: value.foreground_color.map(|c| c.into()),
bg: value.background_color.map(|c| c.into()),
#[cfg(feature = "underline-color")]
underline_color: value.underline_color.map(|c| c.into()),
add_modifier: value.attributes.into(),
sub_modifier,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_crossterm_color() {
assert_eq!(Color::from(CColor::Reset), Color::Reset);
assert_eq!(Color::from(CColor::Black), Color::Black);
assert_eq!(Color::from(CColor::DarkGrey), Color::DarkGray);
assert_eq!(Color::from(CColor::Red), Color::LightRed);
assert_eq!(Color::from(CColor::DarkRed), Color::Red);
assert_eq!(Color::from(CColor::Green), Color::LightGreen);
assert_eq!(Color::from(CColor::DarkGreen), Color::Green);
assert_eq!(Color::from(CColor::Yellow), Color::LightYellow);
assert_eq!(Color::from(CColor::DarkYellow), Color::Yellow);
assert_eq!(Color::from(CColor::Blue), Color::LightBlue);
assert_eq!(Color::from(CColor::DarkBlue), Color::Blue);
assert_eq!(Color::from(CColor::Magenta), Color::LightMagenta);
assert_eq!(Color::from(CColor::DarkMagenta), Color::Magenta);
assert_eq!(Color::from(CColor::Cyan), Color::LightCyan);
assert_eq!(Color::from(CColor::DarkCyan), Color::Cyan);
assert_eq!(Color::from(CColor::White), Color::White);
assert_eq!(Color::from(CColor::Grey), Color::Gray);
assert_eq!(
Color::from(CColor::Rgb { r: 0, g: 0, b: 0 }),
Color::Rgb(0, 0, 0)
);
assert_eq!(
Color::from(CColor::Rgb {
r: 10,
g: 20,
b: 30
}),
Color::Rgb(10, 20, 30)
);
assert_eq!(Color::from(CColor::AnsiValue(32)), Color::Indexed(32));
assert_eq!(Color::from(CColor::AnsiValue(37)), Color::Indexed(37));
}
mod modifier {
use super::*;
#[test]
fn from_crossterm_attribute() {
assert_eq!(Modifier::from(CAttribute::Reset), Modifier::empty());
assert_eq!(Modifier::from(CAttribute::Bold), Modifier::BOLD);
assert_eq!(Modifier::from(CAttribute::Italic), Modifier::ITALIC);
assert_eq!(Modifier::from(CAttribute::Underlined), Modifier::UNDERLINED);
assert_eq!(
Modifier::from(CAttribute::DoubleUnderlined),
Modifier::UNDERLINED
);
assert_eq!(
Modifier::from(CAttribute::Underdotted),
Modifier::UNDERLINED
);
assert_eq!(Modifier::from(CAttribute::Dim), Modifier::DIM);
assert_eq!(
Modifier::from(CAttribute::NormalIntensity),
Modifier::empty()
);
assert_eq!(
Modifier::from(CAttribute::CrossedOut),
Modifier::CROSSED_OUT
);
assert_eq!(Modifier::from(CAttribute::NoUnderline), Modifier::empty());
assert_eq!(Modifier::from(CAttribute::OverLined), Modifier::empty());
assert_eq!(Modifier::from(CAttribute::SlowBlink), Modifier::SLOW_BLINK);
assert_eq!(
Modifier::from(CAttribute::RapidBlink),
Modifier::RAPID_BLINK
);
assert_eq!(Modifier::from(CAttribute::Hidden), Modifier::HIDDEN);
assert_eq!(Modifier::from(CAttribute::NoHidden), Modifier::empty());
assert_eq!(Modifier::from(CAttribute::Reverse), Modifier::REVERSED);
}
#[test]
fn from_crossterm_attributes() {
assert_eq!(
Modifier::from(CAttributes::from(CAttribute::Bold)),
Modifier::BOLD
);
assert_eq!(
Modifier::from(CAttributes::from(
[CAttribute::Bold, CAttribute::Italic].as_ref()
)),
Modifier::BOLD | Modifier::ITALIC
);
assert_eq!(
Modifier::from(CAttributes::from(
[CAttribute::Bold, CAttribute::NotCrossedOut].as_ref()
)),
Modifier::BOLD
);
assert_eq!(
Modifier::from(CAttributes::from(
[CAttribute::Dim, CAttribute::Underdotted].as_ref()
)),
Modifier::DIM | Modifier::UNDERLINED
);
assert_eq!(
Modifier::from(CAttributes::from(
[CAttribute::Dim, CAttribute::SlowBlink, CAttribute::Italic].as_ref()
)),
Modifier::DIM | Modifier::SLOW_BLINK | Modifier::ITALIC
);
assert_eq!(
Modifier::from(CAttributes::from(
[
CAttribute::Hidden,
CAttribute::NoUnderline,
CAttribute::NotCrossedOut
]
.as_ref()
)),
Modifier::HIDDEN
);
assert_eq!(
Modifier::from(CAttributes::from(CAttribute::Reverse)),
Modifier::REVERSED
);
assert_eq!(
Modifier::from(CAttributes::from(CAttribute::Reset)),
Modifier::empty()
);
assert_eq!(
Modifier::from(CAttributes::from(
[CAttribute::RapidBlink, CAttribute::CrossedOut].as_ref()
)),
Modifier::RAPID_BLINK | Modifier::CROSSED_OUT
);
}
}
#[test]
fn from_crossterm_content_style() {
assert_eq!(Style::from(ContentStyle::default()), Style::default());
assert_eq!(
Style::from(ContentStyle {
foreground_color: Some(CColor::DarkYellow),
..Default::default()
}),
Style::default().fg(Color::Yellow)
);
assert_eq!(
Style::from(ContentStyle {
background_color: Some(CColor::DarkYellow),
..Default::default()
}),
Style::default().bg(Color::Yellow)
);
assert_eq!(
Style::from(ContentStyle {
attributes: CAttributes::from(CAttribute::Bold),
..Default::default()
}),
Style::default().add_modifier(Modifier::BOLD)
);
assert_eq!(
Style::from(ContentStyle {
attributes: CAttributes::from(CAttribute::NoBold),
..Default::default()
}),
Style::default().remove_modifier(Modifier::BOLD)
);
assert_eq!(
Style::from(ContentStyle {
attributes: CAttributes::from(CAttribute::Italic),
..Default::default()
}),
Style::default().add_modifier(Modifier::ITALIC)
);
assert_eq!(
Style::from(ContentStyle {
attributes: CAttributes::from(CAttribute::NoItalic),
..Default::default()
}),
Style::default().remove_modifier(Modifier::ITALIC)
);
assert_eq!(
Style::from(ContentStyle {
attributes: CAttributes::from([CAttribute::Bold, CAttribute::Italic].as_ref()),
..Default::default()
}),
Style::default()
.add_modifier(Modifier::BOLD)
.add_modifier(Modifier::ITALIC)
);
assert_eq!(
Style::from(ContentStyle {
attributes: CAttributes::from([CAttribute::NoBold, CAttribute::NoItalic].as_ref()),
..Default::default()
}),
Style::default()
.remove_modifier(Modifier::BOLD)
.remove_modifier(Modifier::ITALIC)
);
}
#[test]
#[cfg(feature = "underline-color")]
fn from_crossterm_content_style_underline() {
assert_eq!(
Style::from(ContentStyle {
underline_color: Some(CColor::DarkRed),
..Default::default()
}),
Style::default().underline_color(Color::Red)
)
}
}

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

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

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

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

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

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

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

File diff suppressed because it is too large Load Diff

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

@@ -6,6 +6,11 @@ use std::{
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
/// area they are supposed to render to.
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
@@ -21,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)
@@ -106,6 +163,26 @@ impl Rect {
}
}
/// Moves the `Rect` without modifying its size.
///
/// Moves the `Rect` according to the given offset without modifying its [`width`](Rect::width)
/// or [`height`](Rect::height).
/// - Positive `x` moves the whole `Rect` to the right, negative to the left.
/// - Positive `y` moves the whole `Rect` to the bottom, negative to the top.
///
/// See [`Offset`] for details.
pub fn offset(self, offset: Offset) -> Rect {
Rect {
x: i32::from(self.x)
.saturating_add(offset.x)
.clamp(0, (u16::MAX - self.width) as i32) as u16,
y: i32::from(self.y)
.saturating_add(offset.y)
.clamp(0, (u16::MAX - self.height) as i32) as u16,
..self
}
}
/// Returns a new rect that contains both the current one and the given one.
pub fn union(self, other: Rect) -> Rect {
let x1 = min(self.x, other.x);
@@ -131,8 +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),
}
}
@@ -143,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]
@@ -207,6 +400,39 @@ mod tests {
);
}
#[test]
fn offset() {
assert_eq!(
Rect::new(1, 2, 3, 4).offset(Offset { x: 5, y: 6 }),
Rect::new(6, 8, 3, 4),
);
}
#[test]
fn negative_offset() {
assert_eq!(
Rect::new(4, 3, 3, 4).offset(Offset { x: -2, y: -1 }),
Rect::new(2, 2, 3, 4),
);
}
#[test]
fn negative_offset_saturate() {
assert_eq!(
Rect::new(1, 2, 3, 4).offset(Offset { x: -5, y: -6 }),
Rect::new(0, 0, 3, 4),
);
}
/// Offsets a [`Rect`] making it go outside [`u16::MAX`], it should keep its size.
#[test]
fn offset_saturate_max() {
assert_eq!(
Rect::new(u16::MAX - 500, u16::MAX - 500, 100, 100).offset(Offset { x: 1000, y: 1000 }),
Rect::new(u16::MAX - 100, u16::MAX - 100, 100, 100),
);
}
#[test]
fn union() {
assert_eq!(
@@ -223,6 +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)));
@@ -288,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
}
);
}
}

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

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

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

@@ -1,30 +1,24 @@
#![forbid(unsafe_code)]
//! ![Demo](https://raw.githubusercontent.com/ratatui-org/ratatui/aa09e59dc0058347f68d7c1e0c91f863c6f2b8c9/examples/demo2.gif)
//! ![Demo](https://raw.githubusercontent.com/ratatui-org/ratatui/b33c878808c4c40591d7a2d9f9d94d6fee95a96f/examples/demo2.gif)
//!
//! <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,20 +155,21 @@
//! The library comes with a basic yet useful layout management object called [`Layout`] which
//! allows you to split the available space into multiple areas and then render widgets in each
//! area. This lets you describe a responsive terminal UI by nesting layouts. See the [Layout]
//! section of the [Ratatui Book] for more info.
//! section of the [Ratatui Website] for more info.
//!
//! ```rust,no_run
//! use ratatui::{prelude::*, widgets::*};
//!
//! fn ui(frame: &mut Frame) {
//! let main_layout = Layout::default()
//! .direction(Direction::Vertical)
//! .constraints(vec![
//! let main_layout = Layout::new(
//! Direction::Vertical,
//! [
//! Constraint::Length(1),
//! Constraint::Min(0),
//! Constraint::Length(1),
//! ])
//! .split(frame.size());
//! ],
//! )
//! .split(frame.size());
//! frame.render_widget(
//! Block::new().borders(Borders::TOP).title("Title Bar"),
//! main_layout[0],
@@ -184,10 +179,11 @@
//! main_layout[2],
//! );
//!
//! let inner_layout = Layout::default()
//! .direction(Direction::Horizontal)
//! .constraints(vec![Constraint::Percentage(50), Constraint::Percentage(50)])
//! .split(main_layout[1]);
//! let inner_layout = Layout::new(
//! Direction::Horizontal,
//! [Constraint::Percentage(50), Constraint::Percentage(50)],
//! )
//! .split(main_layout[1]);
//! frame.render_widget(
//! Block::default().borders(Borders::ALL).title("Left"),
//! inner_layout[0],
@@ -213,22 +209,23 @@
//! important one is [`Style`] which represents the foreground and background colors and the text
//! attributes of a [`Span`]. The [`style` module] also provides a [`Stylize`] trait that allows
//! short-hand syntax to apply a style to widgets and text. See the [Styling Text] section of the
//! [Ratatui Book] for more info.
//! [Ratatui Website] for more info.
//!
//! ```rust,no_run
//! use ratatui::{prelude::*, widgets::*};
//!
//! fn ui(frame: &mut Frame) {
//! let areas = Layout::default()
//! .direction(Direction::Vertical)
//! .constraints(vec![
//! let areas = Layout::new(
//! Direction::Vertical,
//! [
//! Constraint::Length(1),
//! Constraint::Length(1),
//! Constraint::Length(1),
//! Constraint::Length(1),
//! Constraint::Min(0),
//! ])
//! .split(frame.size());
//! ],
//! )
//! .split(frame.size());
//!
//! let span1 = Span::raw("Hello ");
//! let span2 = Span::styled(
@@ -282,26 +279,28 @@
doc = "[`calendar`]: widgets::calendar::Monthly"
)]
//!
//! [Ratatui Book]: https://ratatui.rs
//! [Installation]: https://ratatui.rs/installation.html
//! [Rendering]: https://ratatui.rs/concepts/rendering/index.html
//! [Application Patterns]: https://ratatui.rs/concepts/application_patterns/index.html
//! [Hello World tutorial]: https://ratatui.rs/tutorial/hello_world.html
//! [Backends]: https://ratatui.rs/concepts/backends/index.html
//! [Widgets]: https://ratatui.rs/how-to/widgets/index.html
//! [Handling Events]: https://ratatui.rs/concepts/event_handling.html
//! [Layout]: https://ratatui.rs/how-to/layout/index.html
//! [Styling Text]: https://ratatui.rs/how-to/render/style-text.html
//! [rust-tui-template]: https://github.com/ratatui-org/rust-tui-template
//! [ratatui-async-template]: https://ratatui-org.github.io/ratatui-async-template/
//! [simple-tui-rs]: https://github.com/pmsanford/simple-tui-rs
//! [Ratatui Website]: https://ratatui.rs/
//! [Installation]: https://ratatui.rs/installation/
//! [Rendering]: https://ratatui.rs/concepts/rendering/
//! [Application Patterns]: https://ratatui.rs/concepts/application-patterns/
//! [Hello World tutorial]: https://ratatui.rs/tutorials/hello-world/
//! [Backends]: https://ratatui.rs/concepts/backends/
//! [Widgets]: https://ratatui.rs/how-to/widgets/
//! [Handling Events]: https://ratatui.rs/concepts/event-handling/
//! [Layout]: https://ratatui.rs/how-to/layout/
//! [Styling Text]: https://ratatui.rs/how-to/render/style-text/
//! [template]: https://github.com/ratatui-org/template
//! [async-template]: https://ratatui-org.github.io/async-template
//! [Examples]: https://github.com/ratatui-org/ratatui/tree/main/examples
//! [git-cliff]: https://github.com/orhun/git-cliff
//! [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
//! [Contributing]: https://github.com/ratatui-org/ratatui/blob/main/CONTRIBUTING.md
//! [Breaking Changes]: https://github.com/ratatui-org/ratatui/blob/main/BREAKING-CHANGES.md
//! [docsrs-hello]: https://github.com/ratatui-org/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-hello.png?raw=true
//! [docsrs-layout]: https://github.com/ratatui-org/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-layout.png?raw=true
//! [docsrs-styling]: https://github.com/ratatui-org/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-styling.png?raw=true
@@ -318,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))]

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