Compare commits

...

23 Commits

Author SHA1 Message Date
Josh McKinney
1ff292742c fix: change Cell::EMPTY to Cell::empty() and fix underline_color bug 2024-10-14 18:25:06 -07:00
Josh McKinney
e084c9f013 add breaking change doc 2024-10-14 18:25:06 -07:00
Josh McKinney
f08640c53b feat: cache the symbol width in the cell
This leads to more than a 50% speedup
2024-10-14 18:25:06 -07:00
Josh McKinney
2b391ac15d perf: only calculate current symbol width once
This is a 20% performance improvement on buffer diff on my M2 MBP.

```
buffer/diff             time:   [100.26 µs 100.69 µs 101.15 µs]
                        change: [-18.007% -17.489% -16.929%] (p = 0.00 < 0.05)
                        Performance has improved.
```
2024-10-14 18:25:06 -07:00
Josh McKinney
99ef8651aa perf: add buffer diff benchmark 2024-10-14 18:23:55 -07:00
Neal Fachan
d72968d86b feat(scrolling-regions)!: use terminal scrolling regions to stop Terminal::insert_before from flickering (#1341)
The current implementation of Terminal::insert_before causes the
viewport to flicker. This is described in #584 .

This PR removes that flickering by using terminal scrolling regions
(sometimes called "scroll regions"). A terminal can have its scrolling
region set to something other than the whole screen. When a scroll ANSI
sequence is sent to the terminal and it has a non-default scrolling
region, the terminal will scroll just inside of that region.

We use scrolling regions to implement insert_before. We create a region
on the screen above the viewport, scroll that up to make room for the
newly inserted lines, and then draw the new lines. We may need to repeat
this process depending on how much space there is and how many lines we
need to draw.

When the viewport takes up the entire screen, we take a modified
approach. We create a scrolling region of just the top line (could be
more) of the viewport, then use that to draw the lines we want to
output. When we're done, we scroll it up by one line, into the
scrollback history, and then redraw the top line from the viewport.

A final edge case is when the viewport hasn't yet reached the bottom of
the screen. This case, we set up a different scrolling region, where the
top is the top of the viewport, and the bottom is the viewport's bottom
plus the number of lines we want to scroll by. We then scroll this
region down to open up space above the viewport for drawing the inserted
lines.

Regardless of what we do, we need to reset the scrolling region. This PR
takes the approach of always resetting the scrolling region after every
operation. So the Backend gets new scroll_region_up and
scroll_region_down methods instead of set_scrolling_region, scroll_up,
scroll_down, and reset_scrolling_region methods. We chose that approach
for two reasons. First, we don't want Ratatui to have to remember that
state and then reset the scrolling region when tearing down. Second, the
pre-Windows-10 console code doesn't support scrolling regio

This PR:
- Adds a new scrolling-regions feature.
- Adds two new Backend methods: scroll_region_up and scroll_region_down.
- Implements those Backend methods on all backends in the codebase.
- The crossterm and termion implementations use raw ANSI escape
sequences. I'm trying to merge changes into those two projects
separately to support these functions.
- Adds code to Terminal::insert_before to choose between
insert_before_scrolling_regions and insert_before_no_scrolling_regions.
The latter is the old implementation.
- Adds lots of tests to the TestBackend to for the
scrolling-region-related Backend methods.
- Adds versions of terminal tests that show that insert_before doesn't
clobber the viewport. This is a change in behavior from before.
2024-10-14 15:46:13 -07:00
FujiApple
7bdccce3d5 feat!: add an impl of DoubleEndedIterator for Columns and Rows (#1358)
BREAKING-CHANGE: The `pub` modifier has been removed from fields on the
`layout::rect::Columns` and `layout::rect::Rows` iterators. These fields
were not intended to be public and should not have been accessed
directly.

Fixes: #1357
2024-10-14 15:41:39 -07:00
Josh McKinney
3df685e114 fix(rect)!: Rect::area now returns u32 and Rect::new() no longer clamps area to u16::MAX (#1378)
This change fixes the unexpected behavior of the Rect::new() function to
be more intuitive. The Rect::new() function now clamps the width and
height of the rectangle to keep each bound within u16::MAX. The
Rect::area() function now returns a u32 instead of a u16 to allow for
larger areas to be calculated.

Previously, the Rect::new() function would clamp the total area of the
rectangle to u16::MAX, by preserving the aspect ratio of the rectangle.

BREAKING CHANGE: Rect::area() now returns a u32 instead of a u16.

Fixes: <https://github.com/ratatui/ratatui/issues/1375>

Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>
2024-10-14 15:04:56 -07:00
Josh McKinney
4069aa8274 docs: fix missing breaking changes link (#1416) 2024-10-14 17:46:23 +03:00
Josh McKinney
e5a7609588 feat(line)!: impl From<Cow<str>> for Line (#1373)
BREAKING-CHANGES: `Line` now implements `From<Cow<str>`

As this adds an extra conversion, ambiguous infered values may no longer
compile.

```rust
// given:
struct Foo { ... }
impl From<Foo> for String { ... }
impl From<Foo> for Cow<str> { ... }

let foo = Foo { ... };
let line = Line::from(foo); // now fails due to ambiguous type inference
// replace with
let line = Line::from(String::from(foo));
```

Fixes: https://github.com/ratatui/ratatui/issues/1367

---------

Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>
2024-10-14 17:02:57 +03:00
Orhun Parmaksız
69e0cd2fc4 chore(deny): allow Zlib license in cargo-deny configuration (#1411) 2024-10-14 02:48:30 -07:00
Josh McKinney
ab6b1feaec feat(tabs)!: allow tabs to be deselected (#1413)
`Tabs::select()` now accepts `Into<Option<usize>>` instead of `usize`.
This allows tabs to be deselected by passing `None`.

`Tabs::default()` is now also implemented manually instead of deriving
`Default`, and a new method `Tabs::titles()` is added to set the titles
of the tabs.

Fixes: <https://github.com/ratatui/ratatui/pull/1412>

BREAKING CHANGE: `Tabs::select()` now accepts `Into<Option<usize>>`
which breaks any code already using parameter type inference:

```diff
let selected = 1u8;
- let tabs = Tabs::new(["A", "B"]).select(selected.into())
+ let tabs = Tabs::new(["A", "B"]).select(selected as usize)
```
2024-10-14 02:44:58 -07:00
du-ob
3a43274881 feat(color): add hsluv support (#1333)
closes #763

---------

Co-authored-by: Orhun Parmaksız <orhun@archlinux.org>
2024-10-13 14:08:20 +03:00
Tayfun Bocek
dc8d0587ec feat(table)!: add support for selecting column and cell (#1331)
Fixes https://github.com/ratatui-org/ratatui/issues/1250

Adds support for selecting a column and cell in `TableState`. The
selected column, and cells style can be set by
`Table::column_highlight_style` and `Table::cell_highlight_style`
respectively.

The table example has also been updated to display the new
functionality:


https://github.com/user-attachments/assets/e5fd2858-4931-4ce1-a2f6-a5ea1eacbecc

BREAKING CHANGE: The Serialized output of the state will now include the
"selected_column" field. Software that manually parse the serialized the
output (with anything other than the `Serialize` implementation on
`TableState`) may have to be refactored if the "selected_column" field
is not accounted for. This does not affect users who rely on the
`Deserialize`, or `Serialize` implementation on the state.

BREAKING CHANGE: The `Table::highlight_style` is now deprecated in favor
of `Table::row_highlight_style`.

---------

Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>
2024-10-13 14:06:29 +03:00
Josh McKinney
23c0d52c29 feat(text): improve concise debug view for Span,Line,Text,Style (#1410)
Improves https://github.com/ratatui/ratatui/pull/1383

The following now round trips when formatted for debug.
This will make it easier to use insta when testing text related views of
widgets.

```rust
Text::from_iter([
    Line::from("Hello, world!"),
    Line::from("How are you?").bold().left_aligned(),
    Line::from_iter([
        Span::from("I'm "),
        Span::from("doing ").italic(),
        Span::from("great!").bold(),
    ]),
]).on_blue().italic().centered()
```
2024-10-08 09:08:21 +03:00
Tayfun Bocek
c32baa7cd8 chore: add benchmark for Table (#1408) 2024-10-07 12:32:05 -07:00
Raffaele Cataldo
1153a9ebaf refactor: Consistent result expected in layout tests (#1406)
Fixes #1399 
I've looked through all the `assert_eq` and made sure that they follow
the `expected, result` pattern. I wasn't sure if it was desired to
actually pass result and expected as variables to the assert_eq
statements, so I've left everything that seems to have followed the
pattern as is.
2024-10-07 00:09:42 -04:00
Josh McKinney
2805dddf05 feat(logo): Add a Ratatui logo widget
This is a simple logo widget that can be used to render the Ratatui logo
in the terminal. It is used in the `examples/ratatui-logo.rs` example,
and may be used in your applications' help or about screens.

```rust
use ratatui::{Frame, widgets::RatatuiLogo};

fn draw(frame: &mut Frame) {
    frame.render_widget(RatatuiLogo::tiny(), frame.area());
}
```
2024-10-05 17:35:43 -07:00
dependabot[bot]
baf047f556 chore(deps): update rstest requirement from 0.22.0 to 0.23.0 (#1394)
Updates the requirements on [rstest](https://github.com/la10736/rstest)
to permit the latest version.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/la10736/rstest/releases">rstest's
releases</a>.</em></p>
<blockquote>
<h2>0.23.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Upgrade to async-std v1.13 by <a
href="https://github.com/jayvdb"><code>@​jayvdb</code></a> in <a
href="https://redirect.github.com/la10736/rstest/pull/274">la10736/rstest#274</a></li>
<li>Allow environment variables in #[files] attributes by <a
href="https://github.com/hansl"><code>@​hansl</code></a> in <a
href="https://redirect.github.com/la10736/rstest/pull/277">la10736/rstest#277</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/jayvdb"><code>@​jayvdb</code></a> made
their first contribution in <a
href="https://redirect.github.com/la10736/rstest/pull/274">la10736/rstest#274</a></li>
<li><a href="https://github.com/hansl"><code>@​hansl</code></a> made
their first contribution in <a
href="https://redirect.github.com/la10736/rstest/pull/277">la10736/rstest#277</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/la10736/rstest/compare/v0.22.0...v0.23.0">https://github.com/la10736/rstest/compare/v0.22.0...v0.23.0</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/la10736/rstest/blob/master/CHANGELOG.md">rstest's
changelog</a>.</em></p>
<blockquote>
<h2>[0.23.0] 2024/9/29</h2>
<h3>Add</h3>
<ul>
<li>You can now use environment variables in <code>#[files]</code> with
an optional default value (see <a
href="https://redirect.github.com/la10736/rstest/pull/277">#277</a>).</li>
<li>You can now set a base_dir for <code>#[files]</code> with the
<code>$[base_dir = &quot;...&quot;]</code> attribute (see <a
href="https://redirect.github.com/la10736/rstest/pull/277">#277</a>).</li>
</ul>
<h2>[0.22.0] 2024/8/4</h2>
<h3>Changed</h3>
<ul>
<li>Now it's possible destructuring input values both for cases, values
and fixtures. See <a
href="https://redirect.github.com/la10736/rstest/issues/231">#231</a>
for details</li>
</ul>
<h3>Add</h3>
<ul>
<li>Implemented <code>#[ignore]</code> attribute to ignore test
parameters during fixtures resolution/injection. See <a
href="https://redirect.github.com/la10736/rstest/issues/228">#228</a>
for details</li>
</ul>
<h3>Fixed</h3>
<ul>
<li>Lot of typo in code</li>
</ul>
<h2>[0.21.0] 2024/6/1</h2>
<h3>Changed</h3>
<ul>
<li>Add feature <code>crate-name</code> enabled by default to opt-in
crate rename
support. See <a
href="https://redirect.github.com/la10736/rstest/issues/258">#258</a></li>
</ul>
<h2>[0.20.0] 2024/5/30</h2>
<h3>Add</h3>
<ul>
<li>Implemented <code>#[by_ref]</code> attribute to take get a local
lifetime for test arguments.
See <a
href="https://redirect.github.com/la10736/rstest/issues/241">#241</a>
for more details. Thanks to
<a href="https://github.com/narpfel"><code>@​narpfel</code></a> for
suggesting it and useful discussions.</li>
<li>Support for import <code>rstest</code> with another name. See <a
href="https://redirect.github.com/la10736/rstest/issues/221">#221</a></li>
</ul>
<h3>Fixed</h3>
<ul>
<li>Don't remove Lifetimes from test function if any. See <a
href="https://redirect.github.com/la10736/rstest/issues/230">#230</a>
<a href="https://redirect.github.com/la10736/rstest/issues/241">#241</a>
for more details.</li>
<li><a
href="https://doc.rust-lang.org/std/path/struct.PathBuf.html"><code>PathBuf</code></a>
does no longer need to be
in scope when using <code>#[files]</code> (see <a
href="https://redirect.github.com/la10736/rstest/pull/242">#242</a>)</li>
<li><code>#[from(now::accept::also::path::for::fixture)]</code> See <a
href="https://redirect.github.com/la10736/rstest/issues/246">#246</a>
for more details</li>
</ul>
<h2>[0.19.0] 2024/4/9</h2>
<h3>Changed</h3>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="da11d4e48a"><code>da11d4e</code></a>
Update dependency and prepare the release</li>
<li><a
href="0c6e203b53"><code>0c6e203</code></a>
Update checkout list</li>
<li><a
href="20e8858611"><code>20e8858</code></a>
Make clippy happy</li>
<li><a
href="57a93425a5"><code>57a9342</code></a>
Playground should use dev package</li>
<li><a
href="8a04803580"><code>8a04803</code></a>
Removed the useless build rerun variable</li>
<li><a
href="8c232bce88"><code>8c232bc</code></a>
Add a test for invalid base_dir value</li>
<li><a
href="3520861ca5"><code>3520861</code></a>
Add tests for replace_env_vars</li>
<li><a
href="465a4012af"><code>465a401</code></a>
Add unit tests for parsing attributes, and add base_dir</li>
<li><a
href="a970613f43"><code>a970613</code></a>
Add a test for declared environment variables</li>
<li><a
href="c65d1c002c"><code>c65d1c0</code></a>
Add tests and #[ignore_missing_env_vars]</li>
<li>Additional commits viewable in <a
href="https://github.com/la10736/rstest/compare/v0.22.0...v0.23.0">compare
view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-05 23:49:25 +03:00
dependabot[bot]
6745a10508 chore(deps): update octocrab requirement from 0.40.0 to 0.41.0 (#1393)
Updates the requirements on
[octocrab](https://github.com/XAMPPRocky/octocrab) to permit the latest
version.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/XAMPPRocky/octocrab/releases">octocrab's
releases</a>.</em></p>
<blockquote>
<h2>v0.41.0</h2>
<h3>Added</h3>
<ul>
<li>Implement getting users and reopos by their respective IDs (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/690">#690</a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li><em>(installation)</em> [<strong>breaking</strong>] Return Result
instead of panicking in <code>Octocrab::installation</code> (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/687">#687</a>)</li>
</ul>
<h3>Other</h3>
<ul>
<li>Update tower-http requirement from 0.5.1 to 0.6.1 (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/701">#701</a>)</li>
<li>add additional webhook model fields (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/696">#696</a>)</li>
<li>Bump hyper-rustls version. (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/699">#699</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/XAMPPRocky/octocrab/blob/main/CHANGELOG.md">octocrab's
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/XAMPPRocky/octocrab/compare/v0.40.0...v0.41.0">0.41.0</a>
- 2024-09-30</h2>
<h3>Added</h3>
<ul>
<li>Implement getting users and reopos by their respective IDs (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/690">#690</a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li><em>(installation)</em> [<strong>breaking</strong>] Return Result
instead of panicking in <code>Octocrab::installation</code> (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/687">#687</a>)</li>
</ul>
<h3>Other</h3>
<ul>
<li>Update tower-http requirement from 0.5.1 to 0.6.1 (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/701">#701</a>)</li>
<li>add additional webhook model fields (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/696">#696</a>)</li>
<li>Bump hyper-rustls version. (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/699">#699</a>)</li>
</ul>
<h2><a
href="https://github.com/XAMPPRocky/octocrab/compare/v0.39.0...v0.40.0">0.40.0</a>
- 2024-09-22</h2>
<h3>Added</h3>
<ul>
<li>Support <code>remove_assignees</code> on issue API (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/686">#686</a>)</li>
<li>add missing fields in <code>CreateForkBuilder</code> (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/682">#682</a>)</li>
<li>Add <code>Gist::public</code> field (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/678">#678</a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li><em>(refs)</em> [<strong>breaking</strong>] remove
<code>Reference::Commit</code> variant (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/697">#697</a>)</li>
</ul>
<h3>Other</h3>
<ul>
<li>Fix typo in cfg_attr statement (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/693">#693</a>)</li>
<li>Handle empty author object in pr_commits (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/656">#656</a>)</li>
<li>Add <code>DeviceCodes::poll_until_available</code> method (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/679">#679</a>)</li>
<li>Uncomment pr_commits function (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/680">#680</a>)</li>
<li>Only add base_path if req_pandq does not contain it (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/684">#684</a>)</li>
<li>Update code scanning alert (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/673">#673</a>)</li>
<li>Added <code>merged_by</code> and <code>closed_by</code> fields (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/674">#674</a>)</li>
<li>Update and Fixes to the Code Scanning Models &amp; Webhooks (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/675">#675</a>)</li>
</ul>
<h2><a
href="https://github.com/XAMPPRocky/octocrab/compare/v0.38.0...v0.39.0">0.39.0</a>
- 2024-07-30</h2>
<h3>Added</h3>
<ul>
<li>support permission on list_collaborators (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/630">#630</a>)</li>
<li>add check run pull requests and list parameters (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/494">#494</a>)</li>
<li>implement hook deliveries (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/668">#668</a>)</li>
<li>allow sending non String payload with execute (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/665">#665</a>)</li>
<li>added /user/blocks functionality (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/657">#657</a>)</li>
<li>add method to create repo webhook (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/640">#640</a>)</li>
</ul>
<h3>Fixed</h3>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="8f7929f120"><code>8f7929f</code></a>
chore: release (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/issues/700">#700</a>)</li>
<li><a
href="4164ea9ec6"><code>4164ea9</code></a>
fix(installation)!: Return Result instead of panicking in
`Octocrab::installa...</li>
<li><a
href="6ca41409d5"><code>6ca4140</code></a>
feat: Implement getting users and reopos by their respective IDs (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/issues/690">#690</a>)</li>
<li><a
href="5f1ee03a9b"><code>5f1ee03</code></a>
Update tower-http requirement from 0.5.1 to 0.6.1 (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/issues/701">#701</a>)</li>
<li><a
href="5ffd5313ee"><code>5ffd531</code></a>
add additional webhook model fields (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/issues/696">#696</a>)</li>
<li><a
href="53e3d4f403"><code>53e3d4f</code></a>
Bump hyper-rustls version. (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/issues/699">#699</a>)</li>
<li>See full diff in <a
href="https://github.com/XAMPPRocky/octocrab/compare/v0.40.0...v0.41.0">compare
view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-05 23:47:42 +03:00
dependabot[bot]
7799f4ff5b chore(deps): bump bnjbvr/cargo-machete from 0.6.2 to 0.7.0 (#1392)
Bumps [bnjbvr/cargo-machete](https://github.com/bnjbvr/cargo-machete)
from 0.6.2 to 0.7.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/bnjbvr/cargo-machete/releases">bnjbvr/cargo-machete's
releases</a>.</em></p>
<blockquote>
<h2>v0.7.0</h2>
<ul>
<li>Breaking change: Don't search in ignored files (those specified in
.ignore/.gitignore) by default. It's possible to use
<code>--no-ignore</code> to search in these directories by default (<a
href="https://redirect.github.com/bnjbvr/cargo-machete/issues/137">#137</a>).</li>
<li>Improved: fix false positives for multi dependencies single use
statements (<a
href="https://redirect.github.com/bnjbvr/cargo-machete/issues/120">#120</a>).
This improves precision at the cost of a small performance hit.</li>
<li>Improved: make usage of <code>--with-metadata</code> more accurate
(<a
href="https://redirect.github.com/bnjbvr/cargo-machete/issues/122">#122</a>,
<a
href="https://redirect.github.com/bnjbvr/cargo-machete/issues/132">#132</a>).</li>
<li>Improved: instead of displaying <code>.</code> for the current
directory, <code>cargo-machete</code> will now display <code>this
directory</code> (<a
href="https://redirect.github.com/bnjbvr/cargo-machete/issues/109">#109</a>).</li>
<li>Added: There's now an automated docker image build that publishes to
the <a
href="https://github.com/bnjbvr/cargo-machete/pkgs/container/cargo-machete">github
repository</a> (<a
href="https://redirect.github.com/bnjbvr/cargo-machete/issues/121">#121</a>).</li>
<li>Added: <code>--ignore</code> flag which make cargo-machete respect
.ignore and .gitignore files when searching for files (<a
href="https://redirect.github.com/bnjbvr/cargo-machete/issues/95">#95</a>).</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/bnjbvr/cargo-machete/blob/main/CHANGELOG.md">bnjbvr/cargo-machete's
changelog</a>.</em></p>
<blockquote>
<h1>0.7.0 (released on 2024-09-25)</h1>
<ul>
<li>Breaking change: Don't search in ignored files (those specified in
.ignore/.gitignore) by default. It's possible to use
<code>--no-ignore</code> to search in these directories by default (<a
href="https://redirect.github.com/bnjbvr/cargo-machete/issues/137">#137</a>).</li>
<li>Improved: fix false positives for multi dependencies single use
statements (<a
href="https://redirect.github.com/bnjbvr/cargo-machete/issues/120">#120</a>).
This improves precision at the cost of a small performance hit.</li>
<li>Improved: make usage of <code>--with-metadata</code> more accurate
(<a
href="https://redirect.github.com/bnjbvr/cargo-machete/issues/122">#122</a>,
<a
href="https://redirect.github.com/bnjbvr/cargo-machete/issues/132">#132</a>).</li>
<li>Improved: instead of displaying <code>.</code> for the current
directory, <code>cargo-machete</code> will now display <code>this
directory</code> (<a
href="https://redirect.github.com/bnjbvr/cargo-machete/issues/109">#109</a>).</li>
<li>Added: There's now an automated docker image build that publishes to
the <a
href="https://github.com/bnjbvr/cargo-machete/pkgs/container/cargo-machete">github
repository</a> (<a
href="https://redirect.github.com/bnjbvr/cargo-machete/issues/121">#121</a>).</li>
<li>Added: <code>--ignore</code> flag which make cargo-machete respect
.ignore and .gitignore files when searching for files (<a
href="https://redirect.github.com/bnjbvr/cargo-machete/issues/95">#95</a>).</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="5eaad10acf"><code>5eaad10</code></a>
ci: update OS versions in release task</li>
<li><a
href="a881c7261d"><code>a881c72</code></a>
Update CHANGELOG.md: address typo (thanks <a
href="https://github.com/signez"><code>@​signez</code></a>)</li>
<li><a
href="37beb07f34"><code>37beb07</code></a>
Release 0.7.0</li>
<li><a
href="ad0fea4b0b"><code>ad0fea4</code></a>
bump dependencies</li>
<li><a
href="426119c8f0"><code>426119c</code></a>
Fix typo.</li>
<li><a
href="a14e71ad3d"><code>a14e71a</code></a>
Don't search in ignored files by default</li>
<li><a
href="a767940ee9"><code>a767940</code></a>
Fix false positive for multi dep single use statements (<a
href="https://redirect.github.com/bnjbvr/cargo-machete/issues/120">#120</a>)</li>
<li><a
href="16e3e931d0"><code>16e3e93</code></a>
move regex-try to its own repository</li>
<li><a
href="bea9e7361a"><code>bea9e73</code></a>
nump dependencies</li>
<li><a
href="94a21474d2"><code>94a2147</code></a>
remove unnecessary clones, and some minor changes</li>
<li>Additional commits viewable in <a
href="https://github.com/bnjbvr/cargo-machete/compare/v0.6.2...v0.7.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=bnjbvr/cargo-machete&package-manager=github_actions&previous-version=0.6.2&new-version=0.7.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-05 23:43:00 +03:00
Dheepak Krishnamurthy
edcdc8a814 refactor(layout): rename element to segment in layout (#1397)
This PR renames `element` to `segment` in a couple of functions in the
layout calculations for clarity. `element` can refer to `segment`s or
`spacer`s and functions that take only `segment`s should use `segment`
as the variable names.
2024-10-02 22:34:07 +03:00
Josh McKinney
5ad623c29b chore: remove usage of prelude (#1390)
This helps make the doc examples more explicit about what is being used.
It will also makes it a bit easier to do future refactoring of Ratatui,
into several crates, as the ambiguity of where types are coming from
will be reduced.

Additionally, several doc examples have been simplified to use Stylize,
and necessary imports are no longer hidden.

This doesn't remove the prelude. Only the internal usages.
2024-09-28 11:47:45 -07:00
82 changed files with 4204 additions and 955 deletions

View File

@@ -47,7 +47,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: Swatinem/rust-cache@v2
- uses: bnjbvr/cargo-machete@v0.6.2
- uses: bnjbvr/cargo-machete@v0.7.0
clippy:
runs-on: ubuntu-latest

View File

@@ -10,8 +10,14 @@ GitHub with a [breaking change] label.
This is a quick summary of the sections below:
- [v0.29.0](#unreleased)
- `Terminal`, `Buffer`, `Cell`, `Frame` are no longer `Sync` / `RefUnwindSafe`
- Removed public fields from `Rect` iterators
- `Line` now implements `From<Cow<str>`
- `Table::highlight_style` is now `Table::row_highlight_style`
- `Tabs::select` now accepts `Into<Option<usize>>`
- [v0.28.0](#v0280)
`Backend::size` returns `Size` instead of `Rect`
- `Backend::size` returns `Size` instead of `Rect`
- `Backend` trait migrates to `get/set_cursor_position`
- Ratatui now requires Crossterm 0.28.0
- `Axis::labels` now accepts `IntoIterator<Into<Line>>`
@@ -65,6 +71,77 @@ This is a quick summary of the sections below:
- MSRV is now 1.63.0
- `List` no longer ignores empty strings
## Unreleased
### `Terminal`, `Buffer`, `Cell`, `Frame` are no longer `Sync` / `RefUnwindSafe` [#1339]
[#1339]: https://github.com/ratatui/ratatui/pull/1339
In #1339, we added a cache of the Cell width which uses a std::cell::Cell. This causes `Cell` and
all types that contain this (`Terminal`, `Buffer`, `Frame`, `CompletedFrame`, `TestBackend`) to no
longer be `Sync`
This change is unlikely to cause problems as these types likely should not be sent between threads
regardless due to their interaction with various things which mutated externally (e.g. stdio).
### Removed public fields from `Rect` iterators ([#1358])
[#1358]: https://github.com/ratatui/ratatui/pull/1358
The `pub` modifier has been removed from fields on the `layout::rect::Columns` and
`layout::rect::Rows`. These fields were not intended to be public and should not have been accessed
directly.
### `Rect::area()` now returns u32 instead of u16 ([#1378])
[#1378]: https://github.com/ratatui/ratatui/pull/1378
This is likely to impact anything which relies on `Rect::area` maxing out at u16::MAX. It can now
return up to u16::MAX * u16::MAX (2^32 - 2^17 + 1).
### `Line` now implements `From<Cow<str>` ([#1373])
[#1373]: https://github.com/ratatui/ratatui/pull/1373
As this adds an extra conversion, ambiguous inferred expressions may no longer compile.
```rust
// given:
struct Foo { ... }
impl From<Foo> for String { ... }
impl From<Foo> for Cow<str> { ... }
let foo = Foo { ... };
let line = Line::from(foo); // now fails due to now ambiguous inferred type
// replace with e.g.
let line = Line::from(String::from(foo));
```
### `Tabs::select()` now accepts `Into<Option<usize>>` ([#1413])
[#1413]: https://github.com/ratatui/ratatui/pull/1413
Previously `Tabs::select()` accepted `usize`, but it now accepts `Into<Option<usize>>`. This breaks
any code already using parameter type inference:
```diff
let selected = 1u8;
- let tabs = Tabs::new(["A", "B"]).select(selected.into())
+ let tabs = Tabs::new(["A", "B"]).select(selected as usize)
```
### `Table::highlight_style` is now `Table::row_highlight_style` ([#1331])
[#1331]: https://github.com/ratatui/ratatui/pull/1331
The `Table::highlight_style` is now deprecated in favor of `Table::row_highlight_style`.
Also, the serialized output of the `TableState` will now include the "selected_column" field.
Software that manually parse the serialized the output (with anything other than the `Serialize`
implementation on `TableState`) may have to be refactored if the "selected_column" field is not
accounted for. This does not affect users who rely on the `Deserialize`, or `Serialize`
implementation on the state.
## v0.28.0
### `Backend::size` returns `Size` instead of `Rect` ([#1254])
@@ -134,7 +211,7 @@ are also named terminal, and confusion about module exports for newer Rust users
This change simplifies the trait and makes it easier to implement.
### `Frame::size` is deprecated and renamed to `Frame::area`
### `Frame::size` is deprecated and renamed to `Frame::area` ([#1293])
[#1293]: https://github.com/ratatui/ratatui/pull/1293

View File

@@ -27,6 +27,7 @@ cassowary = "0.3"
compact_str = "0.8.0"
crossterm = { version = "0.28.1", optional = true }
document-features = { version = "0.2.7", optional = true }
indoc = "2"
instability = "0.3.1"
itertools = "0.13"
lru = "0.12.0"
@@ -53,11 +54,11 @@ fakeit = "1.1"
font8x8 = "0.3.1"
futures = "0.3.30"
indoc = "2"
octocrab = "0.40.0"
octocrab = "0.41.0"
pretty_assertions = "1.4.0"
rand = "0.8.5"
rand_chacha = "0.3.1"
rstest = "0.22.0"
rstest = "0.23.0"
serde_json = "1.0.109"
tokio = { version = "1.39.2", features = [
"rt",
@@ -137,6 +138,10 @@ macros = []
## enables conversions from colors in the [`palette`] crate to [`Color`](crate::style::Color).
palette = ["dep:palette"]
## Use terminal scrolling regions to make some operations less prone to
## flickering. (i.e. Terminal::insert_before).
scrolling-regions = []
## enables all widgets.
all-widgets = ["widget-calendar"]

View File

@@ -7,6 +7,7 @@ pub mod main {
pub mod paragraph;
pub mod rect;
pub mod sparkline;
pub mod table;
}
pub use main::*;
@@ -18,5 +19,6 @@ criterion::criterion_main!(
list::benches,
paragraph::benches,
rect::benches,
sparkline::benches
sparkline::benches,
table::benches,
);

View File

@@ -2,8 +2,7 @@ use criterion::{criterion_group, Bencher, BenchmarkId, Criterion};
use rand::Rng;
use ratatui::{
buffer::Buffer,
layout::Rect,
prelude::Direction,
layout::{Direction, Rect},
widgets::{Bar, BarChart, BarGroup, Widget},
};

View File

@@ -1,19 +1,17 @@
use std::iter::zip;
use criterion::{black_box, BenchmarkId, Criterion};
use ratatui::{
buffer::{Buffer, Cell},
layout::Rect,
text::Line,
widgets::Widget,
};
criterion::criterion_group!(benches, empty, filled, with_lines);
criterion::criterion_group!(benches, empty, filled, with_lines, diff);
const fn rect(size: u16) -> Rect {
Rect {
x: 0,
y: 0,
width: size,
height: size,
}
Rect::new(0, 0, size, size)
}
fn empty(c: &mut Criterion) {
@@ -63,3 +61,37 @@ fn with_lines(c: &mut Criterion) {
}
group.finish();
}
fn diff(c: &mut Criterion) {
const AREA: Rect = Rect {
x: 0,
y: 0,
width: 200,
height: 50,
};
c.bench_function("buffer/diff", |b| {
let buffer_1 = create_random_buffer(AREA);
let buffer_2 = create_random_buffer(AREA);
b.iter(|| {
let _ = black_box(&buffer_1).diff(black_box(&buffer_2));
});
});
}
fn create_random_buffer(area: Rect) -> Buffer {
const PARAGRAPH_COUNT: i64 = 15;
const SENTENCE_COUNT: i64 = 5;
const WORD_COUNT: i64 = 20;
const SEPARATOR: &str = "\n\n";
let paragraphs = fakeit::words::paragraph(
PARAGRAPH_COUNT,
SENTENCE_COUNT,
WORD_COUNT,
SEPARATOR.to_string(),
);
let mut buffer = Buffer::empty(area);
for (line, row) in zip(paragraphs.lines(), area.rows()) {
Line::from(line).render(row, &mut buffer);
}
buffer
}

69
benches/main/table.rs Normal file
View File

@@ -0,0 +1,69 @@
use criterion::{criterion_group, BatchSize, Bencher, BenchmarkId, Criterion};
use ratatui::{
buffer::Buffer,
layout::{Constraint, Rect},
widgets::{Row, StatefulWidget, Table, TableState, Widget},
};
/// Benchmark for rendering a table.
/// It only benchmarks the render with a different number of rows, and columns.
fn table(c: &mut Criterion) {
let mut group = c.benchmark_group("table");
for row_count in [64, 2048, 16384] {
for col_count in [2, 4, 8] {
let bench_sizes = format!("{row_count}x{col_count}");
let rows: Vec<Row> = (0..row_count)
.map(|_| Row::new((0..col_count).map(|_| fakeit::words::quote())))
.collect();
// Render default table
group.bench_with_input(
BenchmarkId::new("render", &bench_sizes),
&Table::new(rows.clone(), [] as [Constraint; 0]),
render,
);
// Render with an offset to the middle of the table and a selected row
group.bench_with_input(
BenchmarkId::new("render_scroll_half", &bench_sizes),
&Table::new(rows, [] as [Constraint; 0]).highlight_symbol(">>"),
|b, table| {
render_stateful(
b,
table,
TableState::default()
.with_offset(row_count / 2)
.with_selected(Some(row_count / 2)),
);
},
);
}
}
group.finish();
}
fn render(bencher: &mut Bencher, table: &Table) {
let mut buffer = Buffer::empty(Rect::new(0, 0, 200, 50));
bencher.iter_batched(
|| table.to_owned(),
|bench_table| {
Widget::render(bench_table, buffer.area, &mut buffer);
},
BatchSize::LargeInput,
);
}
fn render_stateful(bencher: &mut Bencher, table: &Table, mut state: TableState) {
let mut buffer = Buffer::empty(Rect::new(0, 0, 200, 50));
bencher.iter_batched(
|| table.to_owned(),
|bench_table| {
StatefulWidget::render(bench_table, buffer.area, &mut buffer, &mut state);
},
BatchSize::LargeInput,
);
}
criterion_group!(benches, table);

View File

@@ -11,6 +11,7 @@ allow = [
"MIT",
"Unicode-DFS-2016",
"WTFPL",
"Zlib",
]
[advisories]

View File

@@ -244,7 +244,7 @@ impl Widget for &PullRequestListWidget {
.block(block)
.highlight_spacing(HighlightSpacing::Always)
.highlight_symbol(">>")
.highlight_style(Style::new().on_blue());
.row_highlight_style(Style::new().on_blue());
StatefulWidget::render(table, area, buf, &mut state.table_state);
}

View File

@@ -48,7 +48,7 @@ struct App {
}
impl App {
fn new() -> Self {
const fn new() -> Self {
Self {
x: 0.0,
y: 0.0,

View File

@@ -164,7 +164,7 @@ fn render_ingredients(selected_row: usize, area: Rect, buf: &mut Buffer) {
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))
.highlight_style(Style::new().light_yellow()),
.row_highlight_style(Style::new().light_yellow()),
area,
buf,
&mut state,

View File

@@ -60,7 +60,7 @@ fn render_hops(selected_row: usize, area: Rect, buf: &mut Buffer) {
StatefulWidget::render(
Table::new(rows, [Constraint::Max(100), Constraint::Length(15)])
.header(Row::new(vec!["Host", "Address"]).set_style(THEME.traceroute.header))
.highlight_style(THEME.traceroute.selected)
.row_highlight_style(THEME.traceroute.selected)
.block(block),
area,
buf,

View File

@@ -13,57 +13,42 @@
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
use std::{
io::{self},
thread::sleep,
time::Duration,
use std::env::args;
use color_eyre::Result;
use crossterm::event::{self, Event};
use ratatui::{
layout::{Constraint, Layout},
widgets::{RatatuiLogo, RatatuiLogoSize},
DefaultTerminal, TerminalOptions, Viewport,
};
use indoc::indoc;
use itertools::izip;
use ratatui::{widgets::Paragraph, TerminalOptions, Viewport};
/// A fun example of using half block characters to draw a logo
#[allow(clippy::many_single_char_names)]
fn logo() -> String {
let r = indoc! {"
▄▄▄
█▄▄▀
█ █
"};
let a = indoc! {"
▄▄
█▄▄█
█ █
"};
let t = indoc! {"
▄▄▄
"};
let u = indoc! {"
▄ ▄
█ █
▀▄▄▀
"};
let i = indoc! {"
"};
izip!(r.lines(), a.lines(), t.lines(), u.lines(), i.lines())
.map(|(r, a, t, u, i)| format!("{r:5}{a:5}{t:4}{a:5}{t:4}{u:5}{i:5}"))
.collect::<Vec<_>>()
.join("\n")
}
fn main() -> io::Result<()> {
let mut terminal = ratatui::init_with_options(TerminalOptions {
fn main() -> Result<()> {
color_eyre::install()?;
let terminal = ratatui::init_with_options(TerminalOptions {
viewport: Viewport::Inline(3),
});
terminal.draw(|frame| frame.render_widget(Paragraph::new(logo()), frame.area()))?;
sleep(Duration::from_secs(5));
let size = match args().nth(1).as_deref() {
Some("small") => RatatuiLogoSize::Small,
Some("tiny") => RatatuiLogoSize::Tiny,
_ => RatatuiLogoSize::default(),
};
let result = run(terminal, size);
ratatui::restore();
println!();
Ok(())
result
}
fn run(mut terminal: DefaultTerminal, size: RatatuiLogoSize) -> Result<()> {
loop {
terminal.draw(|frame| {
use Constraint::{Fill, Length};
let [top, bottom] = Layout::vertical([Length(1), Fill(1)]).areas(frame.area());
frame.render_widget("Powered by", top);
frame.render_widget(RatatuiLogo::new(size), bottom);
})?;
if matches!(event::read()?, Event::Key(_)) {
break Ok(());
}
}
}

View File

@@ -14,12 +14,13 @@
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
use color_eyre::Result;
use crossterm::event::KeyModifiers;
use itertools::Itertools;
use ratatui::{
crossterm::event::{self, Event, KeyCode, KeyEventKind},
layout::{Constraint, Layout, Margin, Rect},
style::{self, Color, Modifier, Style, Stylize},
text::{Line, Text},
text::Text,
widgets::{
Block, BorderType, Cell, HighlightSpacing, Paragraph, Row, Scrollbar, ScrollbarOrientation,
ScrollbarState, Table, TableState,
@@ -35,8 +36,10 @@ const PALETTES: [tailwind::Palette; 4] = [
tailwind::INDIGO,
tailwind::RED,
];
const INFO_TEXT: &str =
"(Esc) quit | (↑) move up | (↓) move down | () next color | () previous color";
const INFO_TEXT: [&str; 2] = [
"(Esc) quit | (↑) move up | (↓) move down | () move left | () move right",
"(Shift + →) next color | (Shift + ←) previous color",
];
const ITEM_HEIGHT: usize = 4;
@@ -52,7 +55,9 @@ struct TableColors {
header_bg: Color,
header_fg: Color,
row_fg: Color,
selected_style_fg: Color,
selected_row_style_fg: Color,
selected_column_style_fg: Color,
selected_cell_style_fg: Color,
normal_row_color: Color,
alt_row_color: Color,
footer_border_color: Color,
@@ -65,7 +70,9 @@ impl TableColors {
header_bg: color.c900,
header_fg: tailwind::SLATE.c200,
row_fg: tailwind::SLATE.c200,
selected_style_fg: color.c400,
selected_row_style_fg: color.c400,
selected_column_style_fg: color.c400,
selected_cell_style_fg: color.c600,
normal_row_color: tailwind::SLATE.c950,
alt_row_color: tailwind::SLATE.c900,
footer_border_color: color.c400,
@@ -118,8 +125,7 @@ impl App {
items: data_vec,
}
}
pub fn next(&mut self) {
pub fn next_row(&mut self) {
let i = match self.state.selected() {
Some(i) => {
if i >= self.items.len() - 1 {
@@ -134,7 +140,7 @@ impl App {
self.scroll_state = self.scroll_state.position(i * ITEM_HEIGHT);
}
pub fn previous(&mut self) {
pub fn previous_row(&mut self) {
let i = match self.state.selected() {
Some(i) => {
if i == 0 {
@@ -149,6 +155,14 @@ impl App {
self.scroll_state = self.scroll_state.position(i * ITEM_HEIGHT);
}
pub fn next_column(&mut self) {
self.state.select_next_column();
}
pub fn previous_column(&mut self) {
self.state.select_previous_column();
}
pub fn next_color(&mut self) {
self.color_index = (self.color_index + 1) % PALETTES.len();
}
@@ -168,12 +182,17 @@ impl App {
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
let shift_pressed = key.modifiers.contains(KeyModifiers::SHIFT);
match key.code {
KeyCode::Char('q') | KeyCode::Esc => return Ok(()),
KeyCode::Char('j') | KeyCode::Down => self.next(),
KeyCode::Char('k') | KeyCode::Up => self.previous(),
KeyCode::Char('l') | KeyCode::Right => self.next_color(),
KeyCode::Char('h') | KeyCode::Left => self.previous_color(),
KeyCode::Char('j') | KeyCode::Down => self.next_row(),
KeyCode::Char('k') | KeyCode::Up => self.previous_row(),
KeyCode::Char('l') | KeyCode::Right if shift_pressed => self.next_color(),
KeyCode::Char('h') | KeyCode::Left if shift_pressed => {
self.previous_color();
}
KeyCode::Char('l') | KeyCode::Right => self.next_column(),
KeyCode::Char('h') | KeyCode::Left => self.previous_column(),
_ => {}
}
}
@@ -182,7 +201,7 @@ impl App {
}
fn draw(&mut self, frame: &mut Frame) {
let vertical = &Layout::vertical([Constraint::Min(5), Constraint::Length(3)]);
let vertical = &Layout::vertical([Constraint::Min(5), Constraint::Length(4)]);
let rects = vertical.split(frame.area());
self.set_colors();
@@ -196,9 +215,13 @@ impl App {
let header_style = Style::default()
.fg(self.colors.header_fg)
.bg(self.colors.header_bg);
let selected_style = Style::default()
let selected_row_style = Style::default()
.add_modifier(Modifier::REVERSED)
.fg(self.colors.selected_style_fg);
.fg(self.colors.selected_row_style_fg);
let selected_col_style = Style::default().fg(self.colors.selected_column_style_fg);
let selected_cell_style = Style::default()
.add_modifier(Modifier::REVERSED)
.fg(self.colors.selected_cell_style_fg);
let header = ["Name", "Address", "Email"]
.into_iter()
@@ -229,7 +252,9 @@ impl App {
],
)
.header(header)
.highlight_style(selected_style)
.row_highlight_style(selected_row_style)
.column_highlight_style(selected_col_style)
.cell_highlight_style(selected_cell_style)
.highlight_symbol(Text::from(vec![
"".into(),
bar.into(),
@@ -256,7 +281,7 @@ impl App {
}
fn render_footer(&self, frame: &mut Frame, area: Rect) {
let info_footer = Paragraph::new(Line::from(INFO_TEXT))
let info_footer = Paragraph::new(Text::from_iter(INFO_TEXT))
.style(
Style::new()
.fg(self.colors.row_fg)

View File

@@ -10,3 +10,5 @@ Enter
Sleep 2s
Show
Sleep 2s
Hide
Escape

View File

@@ -80,7 +80,7 @@ impl App {
/// This allows the `App` type to be rendered as a widget. The `App` type owns several other widgets
/// that are rendered as part of the app. The `Widget` trait is implemented on a mutable reference
/// to the `App` type, which allows this to be rendered without consuming the `App` type, and allows
/// the sub-widgets to be mutatable.
/// the sub-widgets to be mutable.
impl Widget for &mut App {
fn render(self, area: Rect, buf: &mut Buffer) {
let constraints = Constraint::from_lengths([1, 1, 2, 1]);

View File

@@ -27,7 +27,7 @@
//! ```rust,no_run
//! use std::io::stdout;
//!
//! use ratatui::prelude::*;
//! use ratatui::{backend::CrosstermBackend, Terminal};
//!
//! let backend = CrosstermBackend::new(stdout());
//! let mut terminal = Terminal::new(backend)?;
@@ -187,8 +187,10 @@ pub trait Backend {
/// # Example
///
/// ```rust
/// # use ratatui::backend::{Backend, TestBackend};
/// # use ratatui::backend::{TestBackend};
/// # let mut backend = TestBackend::new(80, 25);
/// use ratatui::backend::Backend;
///
/// backend.hide_cursor()?;
/// // do something with hidden cursor
/// backend.show_cursor()?;
@@ -222,9 +224,10 @@ pub trait Backend {
/// # Example
///
/// ```rust
/// # use ratatui::backend::{Backend, TestBackend};
/// # use ratatui::layout::Position;
/// # use ratatui::backend::{TestBackend};
/// # let mut backend = TestBackend::new(80, 25);
/// use ratatui::{backend::Backend, layout::Position};
///
/// backend.set_cursor_position(Position { x: 10, y: 20 })?;
/// assert_eq!(backend.get_cursor_position()?, Position { x: 10, y: 20 });
/// # std::io::Result::Ok(())
@@ -254,8 +257,10 @@ pub trait Backend {
/// # Example
///
/// ```rust,no_run
/// # use ratatui::backend::{Backend, TestBackend};
/// # use ratatui::backend::{TestBackend};
/// # let mut backend = TestBackend::new(80, 25);
/// use ratatui::backend::Backend;
///
/// backend.clear()?;
/// # std::io::Result::Ok(())
/// ```
@@ -270,8 +275,10 @@ pub trait Backend {
/// # Example
///
/// ```rust,no_run
/// # use ratatui::{prelude::*, backend::{TestBackend, ClearType}};
/// # use ratatui::{backend::{TestBackend}};
/// # let mut backend = TestBackend::new(80, 25);
/// use ratatui::backend::{Backend, ClearType};
///
/// backend.clear_region(ClearType::All)?;
/// # std::io::Result::Ok(())
/// ```
@@ -302,8 +309,10 @@ pub trait Backend {
/// # Example
///
/// ```rust
/// # use ratatui::{prelude::*, backend::TestBackend};
/// let backend = TestBackend::new(80, 25);
/// # use ratatui::{backend::{TestBackend}};
/// # let backend = TestBackend::new(80, 25);
/// use ratatui::{backend::Backend, layout::Size};
///
/// assert_eq!(backend.size()?, Size::new(80, 25));
/// # std::io::Result::Ok(())
/// ```
@@ -318,6 +327,64 @@ pub trait Backend {
/// Flush any buffered content to the terminal screen.
fn flush(&mut self) -> io::Result<()>;
/// Scroll a region of the screen upwards, where a region is specified by a (half-open) range
/// of rows.
///
/// Each row in the region is replaced by the row `line_count` rows below it, except the bottom
/// `line_count` rows, which are replaced by empty rows. If `line_count` is equal to or larger
/// than the number of rows in the region, then all rows are replaced with empty rows.
///
/// If the region includes row 0, then `line_count` rows are copied into the bottom of the
/// scrollback buffer. These rows are first taken from the old contents of the region, starting
/// from the top. If there aren't sufficient rows in the region, then the remainder are empty
/// rows.
///
/// The position of the cursor afterwards is undefined.
///
/// The behavior is designed to match what ANSI terminals do when scrolling regions are
/// established. With ANSI terminals, a scrolling region can be established with the "^[[X;Yr"
/// sequence, where X and Y define the lines of the region. The scrolling region can be reset
/// to be the whole screen with the "^[[r" sequence.
///
/// When a scrolling region is established in an ANSI terminal, various operations' behaviors
/// are changed in such a way that the scrolling region acts like a "virtual screen". In
/// particular, the scrolling sequence "^[[NS", which scrolls lines up by a count of N.
///
/// On an ANSI terminal, this method will probably translate to something like:
/// "^[[X;Yr^[[NS^[[r". That is, set the scrolling region, scroll up, then reset the scrolling
/// region.
///
/// For examples of how this function is expected to work, refer to the tests for
/// [`TestBackend::scroll_region_up`].
#[cfg(feature = "scrolling-regions")]
fn scroll_region_up(&mut self, region: std::ops::Range<u16>, line_count: u16)
-> io::Result<()>;
/// Scroll a region of the screen downwards, where a region is specified by a (half-open) range
/// of rows.
///
/// Each row in the region is replaced by the row `line_count` rows above it, except the top
/// `line_count` rows, which are replaced by empty rows. If `line_count` is equal to or larger
/// than the number of rows in the region, then all rows are replaced with empty rows.
///
/// The position of the cursor afterwards is undefined.
///
/// See the documentation for [`Self::scroll_region_down`] for more information about how this
/// is expected to be implemented for ANSI terminals. All of that applies, except the ANSI
/// sequence to scroll down is "^[[NT".
///
/// This function is asymmetrical with regards to the scrollback buffer. The reason is that
/// this how terminals seem to implement things.
///
/// For examples of how this function is expected to work, refer to the tests for
/// [`TestBackend::scroll_region_down`].
#[cfg(feature = "scrolling-regions")]
fn scroll_region_down(
&mut self,
region: std::ops::Range<u16>,
line_count: u16,
) -> io::Result<()>;
}
#[cfg(test)]

View File

@@ -44,15 +44,11 @@ use crate::{
/// ```rust,no_run
/// use std::io::{stderr, stdout};
///
/// use ratatui::{
/// crossterm::{
/// terminal::{
/// disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
/// },
/// ExecutableCommand,
/// },
/// prelude::*,
/// use crossterm::{
/// terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
/// ExecutableCommand,
/// };
/// use ratatui::{backend::CrosstermBackend, Terminal};
///
/// let mut backend = CrosstermBackend::new(stdout());
/// // or
@@ -101,8 +97,10 @@ where
/// # Example
///
/// ```rust,no_run
/// # use std::io::stdout;
/// # use ratatui::prelude::*;
/// use std::io::stdout;
///
/// use ratatui::backend::CrosstermBackend;
///
/// let backend = CrosstermBackend::new(stdout());
/// ```
pub const fn new(writer: W) -> Self {
@@ -276,6 +274,32 @@ where
fn flush(&mut self) -> io::Result<()> {
self.writer.flush()
}
#[cfg(feature = "scrolling-regions")]
fn scroll_region_up(&mut self, region: std::ops::Range<u16>, amount: u16) -> io::Result<()> {
queue!(
self.writer,
ScrollUpInRegion {
first_row: region.start,
last_row: region.end.saturating_sub(1),
lines_to_scroll: amount,
}
)?;
self.writer.flush()
}
#[cfg(feature = "scrolling-regions")]
fn scroll_region_down(&mut self, region: std::ops::Range<u16>, amount: u16) -> io::Result<()> {
queue!(
self.writer,
ScrollDownInRegion {
first_row: region.start,
last_row: region.end.saturating_sub(1),
lines_to_scroll: amount,
}
)?;
self.writer.flush()
}
}
impl From<Color> for CColor {
@@ -487,6 +511,102 @@ impl From<ContentStyle> for Style {
}
}
/// A command that scrolls the terminal screen a given number of rows up in a specific scrolling
/// region.
///
/// This will hopefully be replaced by a struct in crossterm proper. There are two outstanding
/// crossterm PRs that will address this:
/// - [918](https://github.com/crossterm-rs/crossterm/pull/918)
/// - [923](https://github.com/crossterm-rs/crossterm/pull/923)
#[cfg(feature = "scrolling-regions")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct ScrollUpInRegion {
/// The first row of the scrolling region.
pub first_row: u16,
/// The last row of the scrolling region.
pub last_row: u16,
/// The number of lines to scroll up by.
pub lines_to_scroll: u16,
}
#[cfg(feature = "scrolling-regions")]
impl crate::crossterm::Command for ScrollUpInRegion {
fn write_ansi(&self, f: &mut impl std::fmt::Write) -> std::fmt::Result {
if self.lines_to_scroll != 0 {
// Set a scrolling region that contains just the desired lines.
write!(
f,
crate::crossterm::csi!("{};{}r"),
self.first_row.saturating_add(1),
self.last_row.saturating_add(1)
)?;
// Scroll the region by the desired count.
write!(f, crate::crossterm::csi!("{}S"), self.lines_to_scroll)?;
// Reset the scrolling region to be the whole screen.
write!(f, crate::crossterm::csi!("r"))?;
}
Ok(())
}
#[cfg(windows)]
fn execute_winapi(&self) -> io::Result<()> {
Err(io::Error::new(
io::ErrorKind::Unsupported,
"ScrollUpInRegion command not supported for winapi",
))
}
}
/// A command that scrolls the terminal screen a given number of rows down in a specific scrolling
/// region.
///
/// This will hopefully be replaced by a struct in crossterm proper. There are two outstanding
/// crossterm PRs that will address this:
/// - [918](https://github.com/crossterm-rs/crossterm/pull/918)
/// - [923](https://github.com/crossterm-rs/crossterm/pull/923)
#[cfg(feature = "scrolling-regions")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct ScrollDownInRegion {
/// The first row of the scrolling region.
pub first_row: u16,
/// The last row of the scrolling region.
pub last_row: u16,
/// The number of lines to scroll down by.
pub lines_to_scroll: u16,
}
#[cfg(feature = "scrolling-regions")]
impl crate::crossterm::Command for ScrollDownInRegion {
fn write_ansi(&self, f: &mut impl std::fmt::Write) -> std::fmt::Result {
if self.lines_to_scroll != 0 {
// Set a scrolling region that contains just the desired lines.
write!(
f,
crate::crossterm::csi!("{};{}r"),
self.first_row.saturating_add(1),
self.last_row.saturating_add(1)
)?;
// Scroll the region by the desired count.
write!(f, crate::crossterm::csi!("{}T"), self.lines_to_scroll)?;
// Reset the scrolling region to be the whole screen.
write!(f, crate::crossterm::csi!("r"))?;
}
Ok(())
}
#[cfg(windows)]
fn execute_winapi(&self) -> io::Result<()> {
Err(io::Error::new(
io::ErrorKind::Unsupported,
"ScrollDownInRegion command not supported for winapi",
))
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -40,8 +40,9 @@ use crate::{
/// use std::io::{stderr, stdout};
///
/// use ratatui::{
/// prelude::*,
/// backend::TermionBackend,
/// termion::{raw::IntoRawMode, screen::IntoAlternateScreen},
/// Terminal,
/// };
///
/// let writer = stdout().into_raw_mode()?.into_alternate_screen()?;
@@ -84,8 +85,10 @@ where
/// # Example
///
/// ```rust,no_run
/// # use std::io::stdout;
/// # use ratatui::prelude::*;
/// use std::io::stdout;
///
/// use ratatui::backend::TermionBackend;
///
/// let backend = TermionBackend::new(stdout());
/// ```
pub const fn new(writer: W) -> Self {
@@ -236,6 +239,30 @@ where
fn flush(&mut self) -> io::Result<()> {
self.writer.flush()
}
#[cfg(feature = "scrolling-regions")]
fn scroll_region_up(&mut self, region: std::ops::Range<u16>, amount: u16) -> io::Result<()> {
write!(
self.writer,
"{}{}{}",
SetRegion(region.start.saturating_add(1), region.end),
termion::scroll::Up(amount),
ResetRegion,
)?;
self.writer.flush()
}
#[cfg(feature = "scrolling-regions")]
fn scroll_region_down(&mut self, region: std::ops::Range<u16>, amount: u16) -> io::Result<()> {
write!(
self.writer,
"{}{}{}",
SetRegion(region.start.saturating_add(1), region.end),
termion::scroll::Down(amount),
ResetRegion,
)?;
self.writer.flush()
}
}
struct Fg(Color);
@@ -465,6 +492,26 @@ impl From<termion::style::Reset> for Modifier {
}
}
/// Set scrolling region.
#[derive(Copy, Clone, PartialEq, Eq)]
pub struct SetRegion(pub u16, pub u16);
impl fmt::Display for SetRegion {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "\x1B[{};{}r", self.0, self.1)
}
}
/// Reset scrolling region.
#[derive(Copy, Clone, PartialEq, Eq)]
pub struct ResetRegion;
impl fmt::Display for ResetRegion {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "\x1B[r")
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -38,7 +38,7 @@ use crate::{
/// # Example
///
/// ```rust,no_run
/// use ratatui::prelude::*;
/// use ratatui::{backend::TermwizBackend, Terminal};
///
/// let backend = TermwizBackend::new()?;
/// let mut terminal = Terminal::new(backend)?;
@@ -78,7 +78,8 @@ impl TermwizBackend {
/// # Example
///
/// ```rust,no_run
/// # use ratatui::prelude::*;
/// use ratatui::backend::TermwizBackend;
///
/// let backend = TermwizBackend::new()?;
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
@@ -249,6 +250,52 @@ impl Backend for TermwizBackend {
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
Ok(())
}
#[cfg(feature = "scrolling-regions")]
fn scroll_region_up(&mut self, region: std::ops::Range<u16>, amount: u16) -> io::Result<()> {
// termwiz doesn't have a command to just set the scrolling region. Instead, setting the
// scrolling region and scrolling are combined. However, this has the side-effect of
// leaving the scrolling region set. To reset the scrolling region, termwiz advises one to
// make a scrolling-region scroll command that contains the entire screen, but scrolls by 0
// lines. See [`Change::ScrollRegionUp`] for more details.
let (_, rows) = self.buffered_terminal.dimensions();
self.buffered_terminal.add_changes(vec![
Change::ScrollRegionUp {
first_row: region.start as usize,
region_size: region.len(),
scroll_count: amount as usize,
},
Change::ScrollRegionUp {
first_row: 0,
region_size: rows,
scroll_count: 0,
},
]);
Ok(())
}
#[cfg(feature = "scrolling-regions")]
fn scroll_region_down(&mut self, region: std::ops::Range<u16>, amount: u16) -> io::Result<()> {
// termwiz doesn't have a command to just set the scrolling region. Instead, setting the
// scrolling region and scrolling are combined. However, this has the side-effect of
// leaving the scrolling region set. To reset the scrolling region, termwiz advises one to
// make a scrolling-region scroll command that contains the entire screen, but scrolls by 0
// lines. See [`Change::ScrollRegionDown`] for more details.
let (_, rows) = self.buffered_terminal.dimensions();
self.buffered_terminal.add_changes(vec![
Change::ScrollRegionDown {
first_row: region.start as usize,
region_size: region.len(),
scroll_count: amount as usize,
},
Change::ScrollRegionDown {
first_row: 0,
region_size: rows,
scroll_count: 0,
},
]);
Ok(())
}
}
impl From<CellAttributes> for Style {

View File

@@ -6,8 +6,6 @@ use std::{
io, iter,
};
use unicode_width::UnicodeWidthStr;
use crate::{
backend::{Backend, ClearType, WindowSize},
buffer::{Buffer, Cell},
@@ -24,7 +22,7 @@ use crate::{
/// # Example
///
/// ```rust
/// use ratatui::{backend::TestBackend, prelude::*};
/// use ratatui::backend::{Backend, TestBackend};
///
/// let mut backend = TestBackend::new(10, 2);
/// backend.clear()?;
@@ -52,13 +50,13 @@ fn buffer_view(buffer: &Buffer) -> String {
let mut overwritten = vec![];
let mut skip: usize = 0;
view.push('"');
for (x, c) in cells.iter().enumerate() {
for (x, cell) in cells.iter().enumerate() {
if skip == 0 {
view.push_str(c.symbol());
view.push_str(cell.symbol());
} else {
overwritten.push((x, c.symbol()));
overwritten.push((x, cell.symbol()));
}
skip = std::cmp::max(skip, c.symbol().width()).saturating_sub(1);
skip = std::cmp::max(skip, cell.width()).saturating_sub(1);
}
view.push('"');
if !overwritten.is_empty() {
@@ -80,6 +78,28 @@ impl TestBackend {
}
}
/// Creates a new `TestBackend` with the specified lines as the initial screen state.
///
/// The backend's screen size is determined from the initial lines.
#[must_use]
pub fn with_lines<'line, Lines>(lines: Lines) -> Self
where
Lines: IntoIterator,
Lines::Item: Into<crate::text::Line<'line>>,
{
let buffer = Buffer::with_lines(lines);
let scrollback = Buffer::empty(Rect {
width: buffer.area.width,
..Rect::ZERO
});
Self {
buffer,
scrollback,
cursor: false,
pos: (0, 0),
}
}
/// Returns a reference to the internal buffer of the `TestBackend`.
pub const fn buffer(&self) -> &Buffer {
&self.buffer
@@ -247,7 +267,7 @@ impl Backend for TestBackend {
Ok(())
}
fn clear_region(&mut self, clear_type: super::ClearType) -> io::Result<()> {
fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
let region = match clear_type {
ClearType::All => return self.clear(),
ClearType::AfterCursor => {
@@ -343,6 +363,77 @@ impl Backend for TestBackend {
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
#[cfg(feature = "scrolling-regions")]
fn scroll_region_up(&mut self, region: std::ops::Range<u16>, scroll_by: u16) -> io::Result<()> {
let width: usize = self.buffer.area.width.into();
let cell_region_start = width * region.start.min(self.buffer.area.height) as usize;
let cell_region_end = width * region.end.min(self.buffer.area.height) as usize;
let cell_region_len = cell_region_end - cell_region_start;
let cells_to_scroll_by = width * scroll_by as usize;
// Deal with the simple case where nothing needs to be copied into scrollback.
if cell_region_start > 0 {
if cells_to_scroll_by >= cell_region_len {
// The scroll amount is large enough to clear the whole region.
self.buffer.content[cell_region_start..cell_region_end].fill_with(Default::default);
} else {
// Scroll up by rotating, then filling in the bottom with empty cells.
self.buffer.content[cell_region_start..cell_region_end]
.rotate_left(cells_to_scroll_by);
self.buffer.content[cell_region_end - cells_to_scroll_by..cell_region_end]
.fill_with(Default::default);
}
return Ok(());
}
// The rows inserted into the scrollback will first come from the buffer, and if that is
// insufficient, will then be blank rows.
let cells_from_region = cell_region_len.min(cells_to_scroll_by);
append_to_scrollback(
&mut self.scrollback,
self.buffer.content.splice(
0..cells_from_region,
iter::repeat_with(Default::default).take(cells_from_region),
),
);
if cells_to_scroll_by < cell_region_len {
// Rotate the remaining cells to the front of the region.
self.buffer.content[cell_region_start..cell_region_end].rotate_left(cells_from_region);
} else {
// Splice cleared out the region. Insert empty rows in scrollback.
append_to_scrollback(
&mut self.scrollback,
iter::repeat_with(Default::default).take(cells_to_scroll_by - cell_region_len),
);
}
Ok(())
}
#[cfg(feature = "scrolling-regions")]
fn scroll_region_down(
&mut self,
region: std::ops::Range<u16>,
scroll_by: u16,
) -> io::Result<()> {
let width: usize = self.buffer.area.width.into();
let cell_region_start = width * region.start.min(self.buffer.area.height) as usize;
let cell_region_end = width * region.end.min(self.buffer.area.height) as usize;
let cell_region_len = cell_region_end - cell_region_start;
let cells_to_scroll_by = width * scroll_by as usize;
if cells_to_scroll_by >= cell_region_len {
// The scroll amount is large enough to clear the whole region.
self.buffer.content[cell_region_start..cell_region_end].fill_with(Default::default);
} else {
// Scroll up by rotating, then filling in the top with empty cells.
self.buffer.content[cell_region_start..cell_region_end]
.rotate_right(cells_to_scroll_by);
self.buffer.content[cell_region_start..cell_region_start + cells_to_scroll_by]
.fill_with(Default::default);
}
Ok(())
}
}
/// Append the provided cells to the bottom of a scrollback buffer. The number of cells must be a
@@ -492,8 +583,7 @@ mod tests {
#[test]
fn clear_region_all() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines([
let mut backend = TestBackend::with_lines([
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
@@ -513,8 +603,7 @@ mod tests {
#[test]
fn clear_region_after_cursor() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines([
let mut backend = TestBackend::with_lines([
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
@@ -537,8 +626,7 @@ mod tests {
#[test]
fn clear_region_before_cursor() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines([
let mut backend = TestBackend::with_lines([
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
@@ -561,8 +649,7 @@ mod tests {
#[test]
fn clear_region_current_line() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines([
let mut backend = TestBackend::with_lines([
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
@@ -585,8 +672,7 @@ mod tests {
#[test]
fn clear_region_until_new_line() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines([
let mut backend = TestBackend::with_lines([
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
@@ -609,8 +695,7 @@ mod tests {
#[test]
fn append_lines_not_at_last_line() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines([
let mut backend = TestBackend::with_lines([
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
@@ -648,8 +733,7 @@ mod tests {
#[test]
fn append_lines_at_last_line() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines([
let mut backend = TestBackend::with_lines([
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
@@ -681,8 +765,7 @@ mod tests {
#[test]
fn append_multiple_lines_not_at_last_line() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines([
let mut backend = TestBackend::with_lines([
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
@@ -711,8 +794,7 @@ mod tests {
#[test]
fn append_multiple_lines_past_last_line() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines([
let mut backend = TestBackend::with_lines([
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
@@ -739,8 +821,7 @@ mod tests {
#[test]
fn append_multiple_lines_where_cursor_at_end_appends_height_lines() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines([
let mut backend = TestBackend::with_lines([
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
@@ -773,8 +854,7 @@ mod tests {
#[test]
fn append_multiple_lines_where_cursor_appends_height_lines() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines([
let mut backend = TestBackend::with_lines([
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
@@ -799,8 +879,7 @@ mod tests {
#[test]
fn append_multiple_lines_where_cursor_at_end_appends_more_than_height_lines() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines([
let mut backend = TestBackend::with_lines([
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
@@ -916,4 +995,81 @@ mod tests {
let mut backend = TestBackend::new(10, 2);
backend.flush().unwrap();
}
#[cfg(feature = "scrolling-regions")]
mod scrolling_regions {
use rstest::rstest;
use super::*;
const A: &str = "aaaa";
const B: &str = "bbbb";
const C: &str = "cccc";
const D: &str = "dddd";
const E: &str = "eeee";
const S: &str = " ";
#[rstest]
#[case([A, B, C, D, E], 0..5, 0, [], [A, B, C, D, E])]
#[case([A, B, C, D, E], 0..5, 2, [A, B], [C, D, E, S, S])]
#[case([A, B, C, D, E], 0..5, 5, [A, B, C, D, E], [S, S, S, S, S])]
#[case([A, B, C, D, E], 0..5, 7, [A, B, C, D, E, S, S], [S, S, S, S, S])]
#[case([A, B, C, D, E], 0..3, 0, [], [A, B, C, D, E])]
#[case([A, B, C, D, E], 0..3, 2, [A, B], [C, S, S, D, E])]
#[case([A, B, C, D, E], 0..3, 3, [A, B, C], [S, S, S, D, E])]
#[case([A, B, C, D, E], 0..3, 4, [A, B, C, S], [S, S, S, D, E])]
#[case([A, B, C, D, E], 1..4, 0, [], [A, B, C, D, E])]
#[case([A, B, C, D, E], 1..4, 2, [], [A, D, S, S, E])]
#[case([A, B, C, D, E], 1..4, 3, [], [A, S, S, S, E])]
#[case([A, B, C, D, E], 1..4, 4, [], [A, S, S, S, E])]
#[case([A, B, C, D, E], 0..0, 0, [], [A, B, C, D, E])]
#[case([A, B, C, D, E], 0..0, 2, [S, S], [A, B, C, D, E])]
#[case([A, B, C, D, E], 2..2, 0, [], [A, B, C, D, E])]
#[case([A, B, C, D, E], 2..2, 2, [], [A, B, C, D, E])]
fn scroll_region_up<const L: usize, const M: usize, const N: usize>(
#[case] initial_screen: [&'static str; L],
#[case] range: std::ops::Range<u16>,
#[case] scroll_by: u16,
#[case] expected_scrollback: [&'static str; M],
#[case] expected_buffer: [&'static str; N],
) {
let mut backend = TestBackend::with_lines(initial_screen);
backend.scroll_region_up(range, scroll_by).unwrap();
if expected_scrollback.is_empty() {
backend.assert_scrollback_empty();
} else {
backend.assert_scrollback_lines(expected_scrollback);
}
backend.assert_buffer_lines(expected_buffer);
}
#[rstest]
#[case([A, B, C, D, E], 0..5, 0, [A, B, C, D, E])]
#[case([A, B, C, D, E], 0..5, 2, [S, S, A, B, C])]
#[case([A, B, C, D, E], 0..5, 5, [S, S, S, S, S])]
#[case([A, B, C, D, E], 0..5, 7, [S, S, S, S, S])]
#[case([A, B, C, D, E], 0..3, 0, [A, B, C, D, E])]
#[case([A, B, C, D, E], 0..3, 2, [S, S, A, D, E])]
#[case([A, B, C, D, E], 0..3, 3, [S, S, S, D, E])]
#[case([A, B, C, D, E], 0..3, 4, [S, S, S, D, E])]
#[case([A, B, C, D, E], 1..4, 0, [A, B, C, D, E])]
#[case([A, B, C, D, E], 1..4, 2, [A, S, S, B, E])]
#[case([A, B, C, D, E], 1..4, 3, [A, S, S, S, E])]
#[case([A, B, C, D, E], 1..4, 4, [A, S, S, S, E])]
#[case([A, B, C, D, E], 0..0, 0, [A, B, C, D, E])]
#[case([A, B, C, D, E], 0..0, 2, [A, B, C, D, E])]
#[case([A, B, C, D, E], 2..2, 0, [A, B, C, D, E])]
#[case([A, B, C, D, E], 2..2, 2, [A, B, C, D, E])]
fn scroll_region_down<const M: usize, const N: usize>(
#[case] initial_screen: [&'static str; M],
#[case] range: std::ops::Range<u16>,
#[case] scroll_by: u16,
#[case] expected_buffer: [&'static str; N],
) {
let mut backend = TestBackend::with_lines(initial_screen);
backend.scroll_region_down(range, scroll_by).unwrap();
backend.assert_scrollback_empty();
backend.assert_buffer_lines(expected_buffer);
}
}
}

View File

@@ -41,7 +41,11 @@ macro_rules! assert_buffer_eq {
#[allow(deprecated)]
#[cfg(test)]
mod tests {
use crate::prelude::*;
use crate::{
buffer::Buffer,
layout::Rect,
style::{Color, Style},
};
#[test]
fn assert_buffer_eq_does_not_panic_on_equal_buffers() {

View File

@@ -6,7 +6,12 @@ use std::{
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use crate::{buffer::Cell, layout::Position, prelude::*};
use crate::{
buffer::Cell,
layout::{Position, Rect},
style::Style,
text::{Line, Span},
};
/// A buffer that maps to the desired content of the terminal after the draw call
///
@@ -74,7 +79,7 @@ impl Buffer {
/// Returns a Buffer with all cells set to the default one
#[must_use]
pub fn empty(area: Rect) -> Self {
Self::filled(area, Cell::EMPTY)
Self::filled(area, Cell::empty())
}
/// Returns a Buffer with all cells initialized with the attributes of the given Cell
@@ -163,7 +168,11 @@ impl Buffer {
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, buffer::Cell, layout::Position};
/// use ratatui::{
/// buffer::{Buffer, Cell},
/// layout::{Position, Rect},
/// };
///
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 10));
///
/// assert_eq!(buffer.cell(Position::new(0, 0)), Some(&Cell::default()));
@@ -190,7 +199,11 @@ impl Buffer {
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, buffer::Cell, layout::Position};
/// use ratatui::{
/// buffer::{Buffer, Cell},
/// layout::{Position, Rect},
/// style::{Color, Style},
/// };
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 10));
///
/// if let Some(cell) = buffer.cell_mut(Position::new(0, 0)) {
@@ -214,7 +227,8 @@ impl Buffer {
/// # Examples
///
/// ```
/// # use ratatui::prelude::*;
/// use ratatui::{buffer::Buffer, layout::Rect};
///
/// let buffer = Buffer::empty(Rect::new(200, 100, 10, 10));
/// // Global coordinates to the top corner of this buffer's area
/// assert_eq!(buffer.index_of(200, 100), 0);
@@ -225,7 +239,8 @@ impl Buffer {
/// Panics when given an coordinate that is outside of this Buffer's area.
///
/// ```should_panic
/// # use ratatui::prelude::*;
/// use ratatui::{buffer::Buffer, layout::Rect};
///
/// let buffer = Buffer::empty(Rect::new(200, 100, 10, 10));
/// // Top coordinate is outside of the buffer in global coordinate space, as the Buffer's area
/// // starts at (200, 100).
@@ -254,9 +269,10 @@ impl Buffer {
return None;
}
// remove offset
let y = position.y - self.area.y;
let x = position.x - self.area.x;
Some((y * self.area.width + x) as usize)
let y = (position.y - self.area.y) as usize;
let x = (position.x - self.area.x) as usize;
let width = self.area.width as usize;
Some(y * width + x)
}
/// Returns the (global) coordinates of a cell given its index
@@ -266,7 +282,8 @@ impl Buffer {
/// # Examples
///
/// ```
/// # use ratatui::prelude::*;
/// use ratatui::{buffer::Buffer, layout::Rect};
///
/// let rect = Rect::new(200, 100, 10, 10);
/// let buffer = Buffer::empty(rect);
/// assert_eq!(buffer.pos_of(0), (200, 100));
@@ -278,7 +295,8 @@ impl Buffer {
/// Panics when given an index that is outside the Buffer's content.
///
/// ```should_panic
/// # use ratatui::prelude::*;
/// use ratatui::{buffer::Buffer, layout::Rect};
///
/// 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.
@@ -377,6 +395,8 @@ impl Buffer {
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// [`Color`]: crate::style::Color
pub fn set_style<S: Into<Style>>(&mut self, area: Rect, style: S) {
let style = style.into();
let area = self.area.intersection(area);
@@ -394,7 +414,7 @@ impl Buffer {
if self.content.len() > length {
self.content.truncate(length);
} else {
self.content.resize(length, Cell::EMPTY);
self.content.resize(length, Cell::empty());
}
self.area = area;
}
@@ -409,7 +429,7 @@ impl Buffer {
/// Merge an other buffer into this one
pub fn merge(&mut self, other: &Self) {
let area = self.area.union(other.area);
self.content.resize(area.area() as usize, Cell::EMPTY);
self.content.resize(area.area() as usize, Cell::empty());
// Move original content to the appropriate space
let size = self.area.area() as usize;
@@ -479,9 +499,8 @@ impl Buffer {
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());
to_skip = current.width().saturating_sub(1);
let affected_width = std::cmp::max(current.width(), previous.width());
invalidated = std::cmp::max(affected_width, invalidated).saturating_sub(1);
}
updates
@@ -504,7 +523,11 @@ impl<P: Into<Position>> Index<P> for Buffer {
/// # Examples
///
/// ```
/// # use ratatui::{prelude::*, buffer::Cell, layout::Position};
/// use ratatui::{
/// buffer::{Buffer, Cell},
/// layout::{Position, Rect},
/// };
///
/// let buf = Buffer::empty(Rect::new(0, 0, 10, 10));
/// let cell = &buf[(0, 0)];
/// let cell = &buf[Position::new(0, 0)];
@@ -530,7 +553,11 @@ impl<P: Into<Position>> IndexMut<P> for Buffer {
/// # Examples
///
/// ```
/// # use ratatui::{prelude::*, buffer::Cell, layout::Position};
/// use ratatui::{
/// buffer::{Buffer, Cell},
/// layout::{Position, Rect},
/// };
///
/// let mut buf = Buffer::empty(Rect::new(0, 0, 10, 10));
/// buf[(0, 0)].set_symbol("A");
/// buf[Position::new(0, 0)].set_symbol("B");
@@ -570,7 +597,7 @@ impl fmt::Debug for Buffer {
} else {
overwritten.push((x, c.symbol()));
}
skip = std::cmp::max(skip, c.symbol().width()).saturating_sub(1);
skip = std::cmp::max(skip, c.width()).saturating_sub(1);
#[cfg(feature = "underline-color")]
{
let style = (c.fg, c.bg, c.underline_color, c.modifier);
@@ -622,6 +649,7 @@ mod tests {
use rstest::{fixture, rstest};
use super::*;
use crate::style::{Color, Modifier, Stylize};
#[test]
fn debug_empty_buffer() {

View File

@@ -1,9 +1,12 @@
use compact_str::CompactString;
use std::hash::{Hash, Hasher};
use crate::prelude::*;
use compact_str::CompactString;
use unicode_width::UnicodeWidthStr;
use crate::style::{Color, Modifier, Style};
/// A buffer cell
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
#[derive(Debug, Clone, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Cell {
/// The string to be drawn in the cell.
@@ -31,11 +34,57 @@ pub struct Cell {
/// Whether the cell should be skipped when copying (diffing) the buffer to the screen.
pub skip: bool,
/// Cache the width of the cell.
width: std::cell::Cell<Option<usize>>,
}
impl PartialEq for Cell {
fn eq(&self, other: &Self) -> bool {
let eq = self.symbol == other.symbol
&& self.fg == other.fg
&& self.bg == other.bg
&& self.modifier == other.modifier
&& self.skip == other.skip;
// explicitly not comparing width, as it is a cache and may be not set
// && self.width == other.width
#[cfg(feature = "underline-color")]
return eq && self.underline_color == other.underline_color;
#[cfg(not(feature = "underline-color"))]
return eq;
}
}
impl Hash for Cell {
fn hash<H: Hasher>(&self, state: &mut H) {
self.symbol.hash(state);
self.fg.hash(state);
self.bg.hash(state);
#[cfg(feature = "underline-color")]
self.underline_color.hash(state);
self.modifier.hash(state);
self.skip.hash(state);
// explicitly not hashing width, as it is a cache and not part of the cell's identity
// self.width.hash(state);
}
}
impl Cell {
/// An empty `Cell`
pub const EMPTY: Self = Self::new(" ");
pub const fn empty() -> Self {
Self {
symbol: CompactString::const_new(" "),
fg: Color::Reset,
bg: Color::Reset,
#[cfg(feature = "underline-color")]
underline_color: Color::Reset,
modifier: Modifier::empty(),
skip: false,
width: std::cell::Cell::new(Some(1)),
}
}
/// Creates a new `Cell` with the given symbol.
///
@@ -52,6 +101,7 @@ impl Cell {
underline_color: Color::Reset,
modifier: Modifier::empty(),
skip: false,
width: std::cell::Cell::new(None),
}
}
@@ -64,6 +114,7 @@ impl Cell {
/// Sets the symbol of the cell.
pub fn set_symbol(&mut self, symbol: &str) -> &mut Self {
self.symbol = CompactString::new(symbol);
self.width.set(None);
self
}
@@ -72,6 +123,7 @@ impl Cell {
/// This is particularly useful for adding zero-width characters to the cell.
pub(crate) fn append_symbol(&mut self, symbol: &str) -> &mut Self {
self.symbol.push_str(symbol);
self.width.set(None);
self
}
@@ -79,6 +131,7 @@ impl Cell {
pub fn set_char(&mut self, ch: char) -> &mut Self {
let mut buf = [0; 4];
self.symbol = CompactString::new(ch.encode_utf8(&mut buf));
self.width.set(None);
self
}
@@ -148,18 +201,33 @@ impl Cell {
}
self.modifier = Modifier::empty();
self.skip = false;
self.width.set(Some(1));
}
/// Returns the width of the cell.
///
/// This value is cached and will only be recomputed when the cell is modified.
#[must_use]
pub fn width(&self) -> usize {
if let Some(width) = self.width.get() {
width
} else {
let width = self.symbol().width();
self.width.set(Some(width));
width
}
}
}
impl Default for Cell {
fn default() -> Self {
Self::EMPTY
Self::empty()
}
}
impl From<char> for Cell {
fn from(ch: char) -> Self {
let mut cell = Self::EMPTY;
let mut cell = Self::empty();
cell.set_char(ch);
cell
}
@@ -182,19 +250,20 @@ mod tests {
underline_color: Color::Reset,
modifier: Modifier::empty(),
skip: false,
width: std::cell::Cell::new(None),
}
);
}
#[test]
fn empty() {
let cell = Cell::EMPTY;
let cell = Cell::empty();
assert_eq!(cell.symbol(), " ");
}
#[test]
fn set_symbol() {
let mut cell = Cell::EMPTY;
let mut cell = Cell::empty();
cell.set_symbol(""); // Multi-byte character
assert_eq!(cell.symbol(), "");
cell.set_symbol("👨‍👩‍👧‍👦"); // Multiple code units combined with ZWJ
@@ -203,7 +272,7 @@ mod tests {
#[test]
fn append_symbol() {
let mut cell = Cell::EMPTY;
let mut cell = Cell::empty();
cell.set_symbol(""); // Multi-byte character
cell.append_symbol("\u{200B}"); // zero-width space
assert_eq!(cell.symbol(), "\u{200B}");
@@ -211,28 +280,28 @@ mod tests {
#[test]
fn set_char() {
let mut cell = Cell::EMPTY;
let mut cell = Cell::empty();
cell.set_char('あ'); // Multi-byte character
assert_eq!(cell.symbol(), "");
}
#[test]
fn set_fg() {
let mut cell = Cell::EMPTY;
let mut cell = Cell::empty();
cell.set_fg(Color::Red);
assert_eq!(cell.fg, Color::Red);
}
#[test]
fn set_bg() {
let mut cell = Cell::EMPTY;
let mut cell = Cell::empty();
cell.set_bg(Color::Red);
assert_eq!(cell.bg, Color::Red);
}
#[test]
fn set_style() {
let mut cell = Cell::EMPTY;
let mut cell = Cell::empty();
cell.set_style(Style::new().fg(Color::Red).bg(Color::Blue));
assert_eq!(cell.fg, Color::Red);
assert_eq!(cell.bg, Color::Blue);
@@ -240,14 +309,14 @@ mod tests {
#[test]
fn set_skip() {
let mut cell = Cell::EMPTY;
let mut cell = Cell::empty();
cell.set_skip(true);
assert!(cell.skip);
}
#[test]
fn reset() {
let mut cell = Cell::EMPTY;
let mut cell = Cell::empty();
cell.set_symbol("");
cell.set_fg(Color::Red);
cell.set_bg(Color::Blue);
@@ -261,7 +330,7 @@ mod tests {
#[test]
fn style() {
let cell = Cell::EMPTY;
let cell = Cell::empty();
assert_eq!(
cell.style(),
Style {
@@ -294,4 +363,12 @@ mod tests {
let cell2 = Cell::new("");
assert_ne!(cell1, cell2);
}
#[test]
fn width() {
let cell = Cell::new("");
assert_eq!(cell.width, std::cell::Cell::new(None)); // not yet cached
assert_eq!(cell.width(), 2);
assert_eq!(cell.width, std::cell::Cell::new(Some(2))); // cached
}
}

View File

@@ -17,5 +17,5 @@ pub use flex::Flex;
pub use layout::Layout;
pub use margin::Margin;
pub use position::Position;
pub use rect::*;
pub use rect::{Columns, Offset, Positions, Rect, Rows};
pub use size::Size;

View File

@@ -26,7 +26,8 @@ use strum::EnumIs;
/// `Constraint` provides helper methods to create lists of constraints from various input formats.
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::layout::Constraint;
///
/// // Create a layout with specified lengths for each element
/// let constraints = Constraint::from_lengths([10, 20, 10]);
///
@@ -223,7 +224,8 @@ impl Constraint {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::layout::{Constraint, Layout, Rect};
///
/// # let area = Rect::default();
/// let constraints = Constraint::from_lengths([1, 2, 3]);
/// let layout = Layout::default().constraints(constraints).split(area);
@@ -240,7 +242,8 @@ impl Constraint {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::layout::{Constraint, Layout, Rect};
///
/// # let area = Rect::default();
/// let constraints = Constraint::from_ratios([(1, 4), (1, 2), (1, 4)]);
/// let layout = Layout::default().constraints(constraints).split(area);
@@ -257,7 +260,8 @@ impl Constraint {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::layout::{Constraint, Layout, Rect};
///
/// # let area = Rect::default();
/// let constraints = Constraint::from_percentages([25, 50, 25]);
/// let layout = Layout::default().constraints(constraints).split(area);
@@ -274,7 +278,8 @@ impl Constraint {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::layout::{Constraint, Layout, Rect};
///
/// # let area = Rect::default();
/// let constraints = Constraint::from_maxes([1, 2, 3]);
/// let layout = Layout::default().constraints(constraints).split(area);
@@ -291,7 +296,8 @@ impl Constraint {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::layout::{Constraint, Layout, Rect};
///
/// # let area = Rect::default();
/// let constraints = Constraint::from_mins([1, 2, 3]);
/// let layout = Layout::default().constraints(constraints).split(area);
@@ -308,7 +314,8 @@ impl Constraint {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::layout::{Constraint, Layout, Rect};
///
/// # let area = Rect::default();
/// let constraints = Constraint::from_fills([1, 2, 3]);
/// let layout = Layout::default().constraints(constraints).split(area);
@@ -330,7 +337,8 @@ impl From<u16> for Constraint {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::layout::{Constraint, Direction, Layout, Rect};
///
/// # let area = Rect::default();
/// let layout = Layout::new(Direction::Vertical, [1, 2, 3]).split(area);
/// let layout = Layout::horizontal([1, 2, 3]).split(area);

View File

@@ -1,7 +1,7 @@
use strum::{Display, EnumIs, EnumString};
#[allow(unused_imports)]
use super::constraint::Constraint;
use crate::layout::Constraint;
/// Defines the options for layout flex justify content in a container.
///

View File

@@ -12,8 +12,7 @@ use self::strengths::{
ALL_SEGMENT_GROW, FILL_GROW, GROW, LENGTH_SIZE_EQ, MAX_SIZE_EQ, MAX_SIZE_LE, MIN_SIZE_EQ,
MIN_SIZE_GE, PERCENTAGE_SIZE_EQ, RATIO_SIZE_EQ, SPACER_SIZE_EQ, SPACE_GROW,
};
use super::Flex;
use crate::prelude::*;
use crate::layout::{Constraint, Direction, Flex, Margin, Rect};
type Rects = Rc<[Rect]>;
type Segments = Rects;
@@ -87,7 +86,11 @@ thread_local! {
/// # Example
///
/// ```rust
/// use ratatui::{prelude::*, widgets::*};
/// use ratatui::{
/// layout::{Constraint, Direction, Layout, Rect},
/// widgets::Paragraph,
/// Frame,
/// };
///
/// fn render(frame: &mut Frame, area: Rect) {
/// let layout = Layout::new(
@@ -141,7 +144,8 @@ impl Layout {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::layout::{Constraint, Direction, Layout};
///
/// Layout::new(
/// Direction::Horizontal,
/// [Constraint::Length(5), Constraint::Min(0)],
@@ -174,7 +178,8 @@ impl Layout {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::layout::{Constraint, Layout};
///
/// let layout = Layout::vertical([Constraint::Length(5), Constraint::Min(0)]);
/// ```
pub fn vertical<I>(constraints: I) -> Self
@@ -193,7 +198,8 @@ impl Layout {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::layout::{Constraint, Layout};
///
/// let layout = Layout::horizontal([Constraint::Length(5), Constraint::Min(0)]);
/// ```
pub fn horizontal<I>(constraints: I) -> Self
@@ -221,7 +227,8 @@ impl Layout {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::layout::{Constraint, Direction, Layout, Rect};
///
/// let layout = Layout::default()
/// .direction(Direction::Horizontal)
/// .constraints([Constraint::Length(5), Constraint::Min(0)])
@@ -255,7 +262,8 @@ impl Layout {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::layout::{Constraint, Layout, Rect};
///
/// let layout = Layout::default()
/// .constraints([
/// Constraint::Percentage(20),
@@ -299,7 +307,8 @@ impl Layout {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::layout::{Constraint, Layout, Rect};
///
/// let layout = Layout::default()
/// .constraints([Constraint::Min(0)])
/// .margin(2)
@@ -320,7 +329,8 @@ impl Layout {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::layout::{Constraint, Layout, Rect};
///
/// let layout = Layout::default()
/// .constraints([Constraint::Min(0)])
/// .horizontal_margin(2)
@@ -338,7 +348,8 @@ impl Layout {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::layout::{Constraint, Layout, Rect};
///
/// let layout = Layout::default()
/// .constraints([Constraint::Min(0)])
/// .vertical_margin(2)
@@ -369,7 +380,8 @@ impl Layout {
/// In this example, the items in the layout will be aligned to the start.
///
/// ```rust
/// # use ratatui::layout::{Flex, Layout, Constraint::*};
/// use ratatui::layout::{Constraint::*, Flex, Layout};
///
/// let layout = Layout::horizontal([Length(20), Length(20), Length(20)]).flex(Flex::Start);
/// ```
///
@@ -377,7 +389,8 @@ impl Layout {
/// space.
///
/// ```rust
/// # use ratatui::layout::{Flex, Layout, Constraint::*};
/// use ratatui::layout::{Constraint::*, Flex, Layout};
///
/// let layout = Layout::horizontal([Length(20), Length(20), Length(20)]).flex(Flex::Legacy);
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
@@ -397,7 +410,8 @@ impl Layout {
/// In this example, the spacing between each item in the layout is set to 2 cells.
///
/// ```rust
/// # use ratatui::layout::{Layout, Constraint::*};
/// use ratatui::layout::{Constraint::*, Layout};
///
/// let layout = Layout::horizontal([Length(20), Length(20), Length(20)]).spacing(2);
/// ```
///
@@ -426,7 +440,8 @@ impl Layout {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::{layout::{Layout, Constraint}, Frame};
///
/// # fn render(frame: &mut Frame) {
/// let area = frame.area();
/// let layout = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
@@ -458,7 +473,8 @@ impl Layout {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::{layout::{Layout, Constraint}, Frame};
///
/// # fn render(frame: &mut Frame) {
/// let area = frame.area();
/// let layout = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
@@ -497,7 +513,7 @@ impl Layout {
/// # Examples
///
/// ```
/// # use ratatui::prelude::*;
/// use ratatui::layout::{Constraint, Direction, Layout, Rect};
/// let layout = Layout::default()
/// .direction(Direction::Vertical)
/// .constraints([Constraint::Length(5), Constraint::Min(0)])
@@ -529,7 +545,8 @@ impl Layout {
/// # Examples
///
/// ```
/// # use ratatui::prelude::*;
/// use ratatui::layout::{Constraint, Direction, Layout, Rect};
///
/// let (areas, spacers) = Layout::default()
/// .direction(Direction::Vertical)
/// .constraints([Constraint::Length(5), Constraint::Min(0)])
@@ -702,35 +719,35 @@ fn configure_constraints(
constraints: &[Constraint],
flex: Flex,
) -> Result<(), AddConstraintError> {
for (&constraint, &element) in constraints.iter().zip(segments.iter()) {
for (&constraint, &segment) in constraints.iter().zip(segments.iter()) {
match constraint {
Constraint::Max(max) => {
solver.add_constraint(element.has_max_size(max, MAX_SIZE_LE))?;
solver.add_constraint(element.has_int_size(max, MAX_SIZE_EQ))?;
solver.add_constraint(segment.has_max_size(max, MAX_SIZE_LE))?;
solver.add_constraint(segment.has_int_size(max, MAX_SIZE_EQ))?;
}
Constraint::Min(min) => {
solver.add_constraint(element.has_min_size(min, MIN_SIZE_GE))?;
solver.add_constraint(segment.has_min_size(min, MIN_SIZE_GE))?;
if flex.is_legacy() {
solver.add_constraint(element.has_int_size(min, MIN_SIZE_EQ))?;
solver.add_constraint(segment.has_int_size(min, MIN_SIZE_EQ))?;
} else {
solver.add_constraint(element.has_size(area, FILL_GROW))?;
solver.add_constraint(segment.has_size(area, FILL_GROW))?;
}
}
Constraint::Length(length) => {
solver.add_constraint(element.has_int_size(length, LENGTH_SIZE_EQ))?;
solver.add_constraint(segment.has_int_size(length, LENGTH_SIZE_EQ))?;
}
Constraint::Percentage(p) => {
let size = area.size() * f64::from(p) / 100.00;
solver.add_constraint(element.has_size(size, PERCENTAGE_SIZE_EQ))?;
solver.add_constraint(segment.has_size(size, PERCENTAGE_SIZE_EQ))?;
}
Constraint::Ratio(num, den) => {
// avoid division by zero by using 1 when denominator is 0
let size = area.size() * f64::from(num) / f64::from(den.max(1));
solver.add_constraint(element.has_size(size, RATIO_SIZE_EQ))?;
solver.add_constraint(segment.has_size(size, RATIO_SIZE_EQ))?;
}
Constraint::Fill(_) => {
// given no other constraints, this segment will grow as much as possible.
solver.add_constraint(element.has_size(area, FILL_GROW))?;
solver.add_constraint(segment.has_size(area, FILL_GROW))?;
}
}
}
@@ -837,7 +854,7 @@ fn configure_fill_constraints(
constraints: &[Constraint],
flex: Flex,
) -> Result<(), AddConstraintError> {
for ((&left_constraint, &left_element), (&right_constraint, &right_element)) in constraints
for ((&left_constraint, &left_segment), (&right_constraint, &right_segment)) in constraints
.iter()
.zip(segments.iter())
.filter(|(c, _)| c.is_fill() || (!flex.is_legacy() && c.is_min()))
@@ -854,9 +871,9 @@ fn configure_fill_constraints(
_ => unreachable!(),
};
solver.add_constraint(
(right_scaling_factor * left_element.size())
(right_scaling_factor * left_segment.size())
| EQ(GROW)
| (left_scaling_factor * right_element.size()),
| (left_scaling_factor * right_segment.size()),
)?;
}
Ok(())
@@ -1293,9 +1310,9 @@ mod tests {
use rstest::rstest;
use crate::{
layout::flex::Flex,
prelude::{Constraint::*, *},
widgets::Paragraph,
buffer::Buffer,
layout::{Constraint, Constraint::*, Direction, Flex, Layout, Rect},
widgets::{Paragraph, Widget},
};
/// Test that the given constraints applied to the given area result in the expected layout.
@@ -1847,7 +1864,7 @@ mod tests {
])
.split(target);
assert_eq!(target.height, chunks.iter().map(|r| r.height).sum::<u16>());
assert_eq!(chunks.iter().map(|r| r.height).sum::<u16>(), target.height);
chunks.windows(2).for_each(|w| assert!(w[0].y <= w[1].y));
}
@@ -1940,7 +1957,7 @@ mod tests {
.iter()
.map(|r| r.width)
.collect::<Vec<u16>>();
assert_eq!(expected, r);
assert_eq!(r, expected);
}
#[rstest]
@@ -1959,7 +1976,7 @@ mod tests {
.iter()
.map(|r| (r.x, r.width))
.collect::<Vec<(u16, u16)>>();
assert_eq!(expected, r);
assert_eq!(r, expected);
}
#[rstest]
@@ -1990,7 +2007,7 @@ mod tests {
.iter()
.map(|r| r.width)
.collect::<Vec<u16>>();
assert_eq!(expected, r);
assert_eq!(r, expected);
}
#[rstest]
@@ -2020,7 +2037,7 @@ mod tests {
.iter()
.map(|r| r.width)
.collect::<Vec<u16>>();
assert_eq!(expected, r);
assert_eq!(r, expected);
let rect = Rect::new(0, 0, 100, 1);
let r = Layout::horizontal(&constraints)
@@ -2029,7 +2046,7 @@ mod tests {
.iter()
.map(|r| r.width)
.collect::<Vec<u16>>();
assert_eq!(expected, r);
assert_eq!(r, expected);
let rect = Rect::new(0, 0, 100, 1);
let r = Layout::horizontal(&constraints)
@@ -2038,7 +2055,7 @@ mod tests {
.iter()
.map(|r| r.width)
.collect::<Vec<u16>>();
assert_eq!(expected, r);
assert_eq!(r, expected);
let rect = Rect::new(0, 0, 100, 1);
let r = Layout::horizontal(&constraints)
@@ -2047,7 +2064,7 @@ mod tests {
.iter()
.map(|r| r.width)
.collect::<Vec<u16>>();
assert_eq!(expected, r);
assert_eq!(r, expected);
let rect = Rect::new(0, 0, 100, 1);
let r = Layout::horizontal(&constraints)
@@ -2056,7 +2073,7 @@ mod tests {
.iter()
.map(|r| r.width)
.collect::<Vec<u16>>();
assert_eq!(expected, r);
assert_eq!(r, expected);
}
#[rstest]
@@ -2070,7 +2087,7 @@ mod tests {
.iter()
.map(|r| r.width)
.collect::<Vec<u16>>();
assert_eq!(expected, r);
assert_eq!(r, expected);
}
#[rstest]
@@ -2117,7 +2134,7 @@ mod tests {
.iter()
.map(|r| r.width)
.collect::<Vec<u16>>();
assert_eq!(expected, r);
assert_eq!(r, expected);
}
#[rstest]
@@ -2134,7 +2151,7 @@ mod tests {
.iter()
.map(|r| r.width)
.collect::<Vec<u16>>();
assert_eq!(expected, r);
assert_eq!(r, expected);
}
#[rstest]
@@ -2152,7 +2169,7 @@ mod tests {
.iter()
.map(|r| r.width)
.collect::<Vec<u16>>();
assert_eq!(expected, r);
assert_eq!(r, expected);
}
#[rstest]
@@ -2216,7 +2233,7 @@ mod tests {
.iter()
.map(|r| (r.x, r.width))
.collect::<Vec<(u16, u16)>>();
assert_eq!(expected, r);
assert_eq!(r, expected);
}
#[rstest]
@@ -2242,7 +2259,7 @@ mod tests {
.iter()
.map(|r| (r.x, r.width))
.collect::<Vec<(u16, u16)>>();
assert_eq!(expected, result);
assert_eq!(result, expected);
}
#[rstest]
@@ -2274,7 +2291,7 @@ mod tests {
.iter()
.map(|r| (r.x, r.width))
.collect::<Vec<(u16, u16)>>();
assert_eq!(expected, r);
assert_eq!(r, expected);
}
#[rstest]
@@ -2298,7 +2315,7 @@ mod tests {
.iter()
.map(|r| (r.x, r.width))
.collect::<Vec<(u16, u16)>>();
assert_eq!(expected, r);
assert_eq!(r, expected);
}
#[rstest]
@@ -2323,7 +2340,7 @@ mod tests {
.iter()
.map(|r| (r.x, r.width))
.collect::<Vec<(u16, u16)>>();
assert_eq!(expected, result);
assert_eq!(result, expected);
}
#[rstest]
@@ -2366,7 +2383,7 @@ mod tests {
.iter()
.map(|r| (r.x, r.width))
.collect::<Vec<(u16, u16)>>();
assert_eq!(expected, result);
assert_eq!(result, expected);
}
#[rstest]
@@ -2386,7 +2403,7 @@ mod tests {
.iter()
.map(|r| (r.x, r.width))
.collect::<Vec<(u16, u16)>>();
assert_eq!(expected, result);
assert_eq!(result, expected);
}
#[rstest]
@@ -2410,7 +2427,7 @@ mod tests {
.iter()
.map(|r| (r.x, r.width))
.collect::<Vec<(u16, u16)>>();
assert_eq!(expected, result);
assert_eq!(result, expected);
}
#[rstest]
@@ -2436,7 +2453,7 @@ mod tests {
.iter()
.map(|r| (r.x, r.width))
.collect::<Vec<(u16, u16)>>();
assert_eq!(expected, result);
assert_eq!(result, expected);
}
#[rstest]
@@ -2462,7 +2479,7 @@ mod tests {
.iter()
.map(|r| (r.x, r.width))
.collect::<Vec<(u16, u16)>>();
assert_eq!(expected, result);
assert_eq!(result, expected);
}
#[rstest]
@@ -2483,7 +2500,7 @@ mod tests {
.iter()
.map(|r| (r.x, r.width))
.collect::<Vec<(u16, u16)>>();
assert_eq!(expected, result);
assert_eq!(result, expected);
}
}

View File

@@ -4,8 +4,7 @@ use std::{
fmt,
};
use super::{Position, Size};
use crate::prelude::*;
use crate::layout::{Margin, Position, Size};
mod iter;
pub use iter::*;
@@ -27,7 +26,7 @@ pub struct Rect {
pub height: u16,
}
/// Amounts by which to move a [`Rect`](super::Rect).
/// Amounts by which to move a [`Rect`](crate::layout::Rect).
///
/// Positive numbers move to the right/bottom and negative to the left/top.
///
@@ -56,32 +55,41 @@ impl Rect {
height: 0,
};
/// Creates a new `Rect`, with width and height limited to keep the area under max `u16`. If
/// clipped, aspect ratio will be preserved.
pub fn new(x: u16, y: u16, width: u16, height: u16) -> Self {
let max_area = u16::MAX;
let (clipped_width, clipped_height) =
if u32::from(width) * u32::from(height) > u32::from(max_area) {
let aspect_ratio = f64::from(width) / f64::from(height);
let max_area_f = f64::from(max_area);
let height_f = (max_area_f / aspect_ratio).sqrt();
let width_f = height_f * aspect_ratio;
(width_f as u16, height_f as u16)
} else {
(width, height)
};
/// Creates a new `Rect`, with width and height limited to keep both bounds within `u16`.
///
/// If the width or height would cause the right or bottom coordinate to be larger than the
/// maximum value of `u16`, the width or height will be clamped to keep the right or bottom
/// coordinate within `u16`.
///
/// # Examples
///
/// ```
/// use ratatui::layout::Rect;
///
/// let rect = Rect::new(1, 2, 3, 4);
/// ```
pub const fn new(x: u16, y: u16, width: u16, height: u16) -> Self {
// these calculations avoid using min so that this function can be const
let max_width = u16::MAX - x;
let max_height = u16::MAX - y;
let width = if width > max_width { max_width } else { width };
let height = if height > max_height {
max_height
} else {
height
};
Self {
x,
y,
width: clipped_width,
height: clipped_height,
width,
height,
}
}
/// The area of the `Rect`. If the area is larger than the maximum value of `u16`, it will be
/// clamped to `u16::MAX`.
pub const fn area(self) -> u16 {
self.width.saturating_mul(self.height)
pub const fn area(self) -> u32 {
(self.width as u32) * (self.height as u32)
}
/// Returns true if the `Rect` has no area.
@@ -205,7 +213,8 @@ impl Rect {
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, layout::Position};
/// use ratatui::layout::{Position, Rect};
///
/// let rect = Rect::new(1, 2, 3, 4);
/// assert!(rect.contains(Position { x: 1, y: 2 }));
/// ````
@@ -234,7 +243,8 @@ impl Rect {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::{layout::Rect, Frame};
///
/// # fn render(frame: &mut Frame) {
/// let area = frame.area();
/// let rect = Rect::new(0, 0, 100, 100).clamp(area);
@@ -254,7 +264,8 @@ impl Rect {
/// # Example
///
/// ```
/// # use ratatui::prelude::*;
/// use ratatui::{buffer::Buffer, layout::Rect, text::Line, widgets::Widget};
///
/// fn render(area: Rect, buf: &mut Buffer) {
/// for row in area.rows() {
/// Line::raw("Hello, world!").render(row, buf);
@@ -270,7 +281,11 @@ impl Rect {
/// # Example
///
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::{
/// buffer::Buffer,
/// layout::Rect,
/// widgets::{Block, Borders, Widget},
/// };
/// fn render(area: Rect, buf: &mut Buffer) {
/// if let Some(left) = area.columns().next() {
/// Block::new().borders(Borders::LEFT).render(left, buf);
@@ -288,7 +303,8 @@ impl Rect {
/// # Example
///
/// ```
/// # use ratatui::prelude::*;
/// use ratatui::{buffer::Buffer, layout::Rect};
///
/// fn render(area: Rect, buf: &mut Buffer) {
/// for position in area.positions() {
/// buf[(position.x, position.y)].set_symbol("x");
@@ -304,7 +320,8 @@ impl Rect {
/// # Examples
///
/// ```
/// # use ratatui::prelude::*;
/// use ratatui::layout::Rect;
///
/// let rect = Rect::new(1, 2, 3, 4);
/// let position = rect.as_position();
/// ````
@@ -352,6 +369,7 @@ mod tests {
use rstest::rstest;
use super::*;
use crate::layout::{Constraint, Layout};
#[test]
fn to_string() {
@@ -496,46 +514,28 @@ mod tests {
#[test]
fn size_truncation() {
for width in 256u16..300u16 {
for height in 256u16..300u16 {
let rect = Rect::new(0, 0, width, height);
rect.area(); // Should not panic.
assert!(rect.width < width || rect.height < height);
// The target dimensions are rounded down so the math will not be too precise
// but let's make sure the ratios don't diverge crazily.
assert!(
(f64::from(rect.width) / f64::from(rect.height)
- f64::from(width) / f64::from(height))
.abs()
< 1.0
);
assert_eq!(
Rect::new(u16::MAX - 100, u16::MAX - 1000, 200, 2000),
Rect {
x: u16::MAX - 100,
y: u16::MAX - 1000,
width: 100,
height: 1000
}
}
// One dimension below 255, one above. Area above max u16.
let width = 900;
let height = 100;
let rect = Rect::new(0, 0, width, height);
assert_ne!(rect.width, 900);
assert_ne!(rect.height, 100);
assert!(rect.width < width || rect.height < height);
);
}
#[test]
fn size_preservation() {
for width in 0..256u16 {
for height in 0..256u16 {
let rect = Rect::new(0, 0, width, height);
rect.area(); // Should not panic.
assert_eq!(rect.width, width);
assert_eq!(rect.height, height);
assert_eq!(
Rect::new(u16::MAX - 100, u16::MAX - 1000, 100, 1000),
Rect {
x: u16::MAX - 100,
y: u16::MAX - 1000,
width: 100,
height: 1000
}
}
// One dimension below 255, one above. Area below max u16.
let rect = Rect::new(0, 0, 300, 100);
assert_eq!(rect.width, 300);
assert_eq!(rect.height, 100);
);
}
#[test]
@@ -546,7 +546,7 @@ mod tests {
width: 10,
height: 10,
};
const _AREA: u16 = RECT.area();
const _AREA: u32 = RECT.area();
const _LEFT: u16 = RECT.left();
const _RIGHT: u16 = RECT.right();
const _TOP: u16 = RECT.top();

View File

@@ -1,11 +1,13 @@
use crate::prelude::*;
use crate::layout::{Position, Rect};
/// An iterator over rows within a `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,
rect: Rect,
/// The y coordinate of the row within the `Rect` when iterating forwards.
current_row_fwd: u16,
/// The y coordinate of the row within the `Rect` when iterating backwards.
current_row_back: u16,
}
impl Rows {
@@ -13,7 +15,8 @@ impl Rows {
pub const fn new(rect: Rect) -> Self {
Self {
rect,
current_row: rect.y,
current_row_fwd: rect.y,
current_row_back: rect.bottom(),
}
}
}
@@ -25,11 +28,25 @@ impl Iterator for Rows {
///
/// 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() {
if self.current_row_fwd >= self.current_row_back {
return None;
}
let row = Rect::new(self.rect.x, self.current_row, self.rect.width, 1);
self.current_row += 1;
let row = Rect::new(self.rect.x, self.current_row_fwd, self.rect.width, 1);
self.current_row_fwd += 1;
Some(row)
}
}
impl DoubleEndedIterator for Rows {
/// Retrieves the previous row within the `Rect`.
///
/// Returns `None` when there are no more rows to iterate through.
fn next_back(&mut self) -> Option<Self::Item> {
if self.current_row_back <= self.current_row_fwd {
return None;
}
self.current_row_back -= 1;
let row = Rect::new(self.rect.x, self.current_row_back, self.rect.width, 1);
Some(row)
}
}
@@ -37,9 +54,11 @@ impl Iterator for Rows {
/// An iterator over columns within a `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,
rect: Rect,
/// The x coordinate of the column within the `Rect` when iterating forwards.
current_column_fwd: u16,
/// The x coordinate of the column within the `Rect` when iterating backwards.
current_column_back: u16,
}
impl Columns {
@@ -47,7 +66,8 @@ impl Columns {
pub const fn new(rect: Rect) -> Self {
Self {
rect,
current_column: rect.x,
current_column_fwd: rect.x,
current_column_back: rect.right(),
}
}
}
@@ -59,11 +79,25 @@ impl Iterator for Columns {
///
/// 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() {
if self.current_column_fwd >= self.current_column_back {
return None;
}
let column = Rect::new(self.current_column, self.rect.y, 1, self.rect.height);
self.current_column += 1;
let column = Rect::new(self.current_column_fwd, self.rect.y, 1, self.rect.height);
self.current_column_fwd += 1;
Some(column)
}
}
impl DoubleEndedIterator for Columns {
/// Retrieves the previous column within the `Rect`.
///
/// Returns `None` when there are no more columns to iterate through.
fn next_back(&mut self) -> Option<Self::Item> {
if self.current_column_back <= self.current_column_fwd {
return None;
}
self.current_column_back -= 1;
let column = Rect::new(self.current_column_back, self.rect.y, 1, self.rect.height);
Some(column)
}
}
@@ -114,19 +148,92 @@ mod tests {
#[test]
fn rows() {
let rect = Rect::new(0, 0, 2, 2);
let rect = Rect::new(0, 0, 2, 3);
let mut rows = Rows::new(rect);
assert_eq!(rows.next(), Some(Rect::new(0, 0, 2, 1)));
assert_eq!(rows.next(), Some(Rect::new(0, 1, 2, 1)));
assert_eq!(rows.next(), Some(Rect::new(0, 2, 2, 1)));
assert_eq!(rows.next(), None);
assert_eq!(rows.next_back(), None);
}
#[test]
fn rows_back() {
let rect = Rect::new(0, 0, 2, 3);
let mut rows = Rows::new(rect);
assert_eq!(rows.next_back(), Some(Rect::new(0, 2, 2, 1)));
assert_eq!(rows.next_back(), Some(Rect::new(0, 1, 2, 1)));
assert_eq!(rows.next_back(), Some(Rect::new(0, 0, 2, 1)));
assert_eq!(rows.next_back(), None);
assert_eq!(rows.next(), None);
}
#[test]
fn rows_meet_in_the_middle() {
let rect = Rect::new(0, 0, 2, 4);
let mut rows = Rows::new(rect);
assert_eq!(rows.next(), Some(Rect::new(0, 0, 2, 1)));
assert_eq!(rows.next_back(), Some(Rect::new(0, 3, 2, 1)));
assert_eq!(rows.next(), Some(Rect::new(0, 1, 2, 1)));
assert_eq!(rows.next_back(), Some(Rect::new(0, 2, 2, 1)));
assert_eq!(rows.next(), None);
assert_eq!(rows.next_back(), None);
}
#[test]
fn columns() {
let rect = Rect::new(0, 0, 2, 2);
let rect = Rect::new(0, 0, 3, 2);
let mut columns = Columns::new(rect);
assert_eq!(columns.next(), Some(Rect::new(0, 0, 1, 2)));
assert_eq!(columns.next(), Some(Rect::new(1, 0, 1, 2)));
assert_eq!(columns.next(), Some(Rect::new(2, 0, 1, 2)));
assert_eq!(columns.next(), None);
assert_eq!(columns.next_back(), None);
}
#[test]
fn columns_back() {
let rect = Rect::new(0, 0, 3, 2);
let mut columns = Columns::new(rect);
assert_eq!(columns.next_back(), Some(Rect::new(2, 0, 1, 2)));
assert_eq!(columns.next_back(), Some(Rect::new(1, 0, 1, 2)));
assert_eq!(columns.next_back(), Some(Rect::new(0, 0, 1, 2)));
assert_eq!(columns.next_back(), None);
assert_eq!(columns.next(), None);
}
#[test]
fn columns_meet_in_the_middle() {
let rect = Rect::new(0, 0, 4, 2);
let mut columns = Columns::new(rect);
assert_eq!(columns.next(), Some(Rect::new(0, 0, 1, 2)));
assert_eq!(columns.next_back(), Some(Rect::new(3, 0, 1, 2)));
assert_eq!(columns.next(), Some(Rect::new(1, 0, 1, 2)));
assert_eq!(columns.next_back(), Some(Rect::new(2, 0, 1, 2)));
assert_eq!(columns.next(), None);
assert_eq!(columns.next_back(), None);
}
/// We allow a total of `65536` columns in the range `(0..=65535)`. In this test we iterate
/// forward and skip the first `65534` columns, and expect the next column to be `65535` and
/// the subsequent columns to be `None`.
#[test]
fn columns_max() {
let rect = Rect::new(0, 0, u16::MAX, 1);
let mut columns = Columns::new(rect).skip(usize::from(u16::MAX - 1));
assert_eq!(columns.next(), Some(Rect::new(u16::MAX - 1, 0, 1, 1)));
assert_eq!(columns.next(), None);
}
/// We allow a total of `65536` columns in the range `(0..=65535)`. In this test we iterate
/// backward and skip the last `65534` columns, and expect the next column to be `0` and the
/// subsequent columns to be `None`.
#[test]
fn columns_min() {
let rect = Rect::new(0, 0, u16::MAX, 1);
let mut columns = Columns::new(rect).rev().skip(usize::from(u16::MAX - 1));
assert_eq!(columns.next(), Some(Rect::new(0, 0, 1, 1)));
assert_eq!(columns.next(), None);
assert_eq!(columns.next(), None);
}

View File

@@ -1,7 +1,7 @@
#![warn(missing_docs)]
use std::fmt;
use crate::prelude::*;
use crate::layout::Rect;
/// A simple size struct
///

View File

@@ -23,7 +23,6 @@ pub use crate::backend::CrosstermBackend;
pub use crate::backend::TermionBackend;
#[cfg(feature = "termwiz")]
pub use crate::backend::TermwizBackend;
pub(crate) use crate::widgets::{StatefulWidgetRef, WidgetRef};
pub use crate::{
backend::{self, Backend},
buffer::{self, Buffer},

View File

@@ -13,7 +13,10 @@
//! ## Example
//!
//! ```
//! use ratatui::prelude::*;
//! use ratatui::{
//! style::{Color, Modifier, Style},
//! text::Span,
//! };
//!
//! let heading_style = Style::new()
//! .fg(Color::Black)
@@ -41,7 +44,11 @@
//! ## Example
//!
//! ```
//! use ratatui::{prelude::*, widgets::*};
//! use ratatui::{
//! style::{Color, Modifier, Style, Stylize},
//! text::Span,
//! widgets::Paragraph,
//! };
//!
//! assert_eq!(
//! "hello".red().on_blue().bold(),
@@ -92,7 +99,7 @@ bitflags! {
/// ## Examples
///
/// ```rust
/// use ratatui::{prelude::*};
/// use ratatui::style::Modifier;
///
/// let m = Modifier::BOLD | Modifier::ITALIC;
/// ```
@@ -128,7 +135,7 @@ impl fmt::Debug for Modifier {
/// Style lets you control the main characteristics of the displayed elements.
///
/// ```rust
/// use ratatui::prelude::*;
/// use ratatui::style::{Color, Modifier, Style};
///
/// Style::default()
/// .fg(Color::Black)
@@ -139,7 +146,8 @@ impl fmt::Debug for Modifier {
/// Styles can also be created with a [shorthand notation](crate::style#using-style-shorthands).
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::style::{Style, Stylize};
///
/// Style::new().black().on_green().italic().bold();
/// ```
///
@@ -149,7 +157,11 @@ impl fmt::Debug for Modifier {
/// anywhere that accepts `Into<Style>`.
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::{
/// style::{Color, Modifier, Style},
/// text::Line,
/// };
///
/// Line::styled("hello", Style::new().fg(Color::Red));
/// // simplifies to
/// Line::styled("hello", Color::Red);
@@ -164,7 +176,11 @@ impl fmt::Debug for Modifier {
/// just S3.
///
/// ```rust
/// use ratatui::prelude::*;
/// use ratatui::{
/// buffer::Buffer,
/// layout::Rect,
/// style::{Color, Modifier, Style},
/// };
///
/// let styles = [
/// Style::default()
@@ -200,7 +216,11 @@ impl fmt::Debug for Modifier {
/// reset all properties until that point use [`Style::reset`].
///
/// ```
/// use ratatui::prelude::*;
/// use ratatui::{
/// buffer::Buffer,
/// layout::Rect,
/// style::{Color, Modifier, Style},
/// };
///
/// let styles = [
/// Style::default()
@@ -240,46 +260,7 @@ pub struct Style {
impl fmt::Debug for Style {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("Style::new()")?;
if let Some(fg) = self.fg {
fg.stylize_debug(ColorDebugKind::Foreground).fmt(f)?;
}
if let Some(bg) = self.bg {
bg.stylize_debug(ColorDebugKind::Background).fmt(f)?;
}
#[cfg(feature = "underline-color")]
if let Some(underline_color) = self.underline_color {
underline_color
.stylize_debug(ColorDebugKind::Underline)
.fmt(f)?;
}
for modifier in self.add_modifier.iter() {
match modifier {
Modifier::BOLD => f.write_str(".bold()")?,
Modifier::DIM => f.write_str(".dim()")?,
Modifier::ITALIC => f.write_str(".italic()")?,
Modifier::UNDERLINED => f.write_str(".underlined()")?,
Modifier::SLOW_BLINK => f.write_str(".slow_blink()")?,
Modifier::RAPID_BLINK => f.write_str(".rapid_blink()")?,
Modifier::REVERSED => f.write_str(".reversed()")?,
Modifier::HIDDEN => f.write_str(".hidden()")?,
Modifier::CROSSED_OUT => f.write_str(".crossed_out()")?,
_ => f.write_fmt(format_args!(".add_modifier(Modifier::{modifier:?})"))?,
}
}
for modifier in self.sub_modifier.iter() {
match modifier {
Modifier::BOLD => f.write_str(".not_bold()")?,
Modifier::DIM => f.write_str(".not_dim()")?,
Modifier::ITALIC => f.write_str(".not_italic()")?,
Modifier::UNDERLINED => f.write_str(".not_underlined()")?,
Modifier::SLOW_BLINK => f.write_str(".not_slow_blink()")?,
Modifier::RAPID_BLINK => f.write_str(".not_rapid_blink()")?,
Modifier::REVERSED => f.write_str(".not_reversed()")?,
Modifier::HIDDEN => f.write_str(".not_hidden()")?,
Modifier::CROSSED_OUT => f.write_str(".not_crossed_out()")?,
_ => f.write_fmt(format_args!(".remove_modifier(Modifier::{modifier:?})"))?,
}
}
self.fmt_stylize(f)?;
Ok(())
}
}
@@ -325,7 +306,8 @@ impl Style {
/// ## Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::style::{Color, Style};
///
/// let style = Style::default().fg(Color::Blue);
/// let diff = Style::default().fg(Color::Red);
/// assert_eq!(style.patch(diff), Style::default().fg(Color::Red));
@@ -341,7 +323,8 @@ impl Style {
/// ## Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::style::{Color, Style};
///
/// let style = Style::default().bg(Color::Blue);
/// let diff = Style::default().bg(Color::Red);
/// assert_eq!(style.patch(diff), Style::default().bg(Color::Red));
@@ -365,7 +348,8 @@ impl Style {
/// ## Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::style::{Color, Modifier, Style};
///
/// let style = Style::default()
/// .underline_color(Color::Blue)
/// .add_modifier(Modifier::UNDERLINED);
@@ -393,7 +377,8 @@ impl Style {
/// ## Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::style::{Modifier, Style};
///
/// let style = Style::default().add_modifier(Modifier::BOLD);
/// let diff = Style::default().add_modifier(Modifier::ITALIC);
/// let patched = style.patch(diff);
@@ -414,7 +399,8 @@ impl Style {
/// ## Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::style::{Modifier, Style};
///
/// let style = Style::default().add_modifier(Modifier::BOLD | Modifier::ITALIC);
/// let diff = Style::default().remove_modifier(Modifier::ITALIC);
/// let patched = style.patch(diff);
@@ -436,7 +422,8 @@ impl Style {
///
/// ## Examples
/// ```
/// # use ratatui::prelude::*;
/// use ratatui::style::{Color, Modifier, Style};
///
/// let style_1 = Style::default().fg(Color::Yellow);
/// let style_2 = Style::default().bg(Color::Red);
/// let combined = style_1.patch(style_2);
@@ -463,6 +450,54 @@ impl Style {
self
}
/// Formats the style in a way that can be copy-pasted into code using the style shorthands.
///
/// This is useful for debugging and for generating code snippets.
pub(crate) fn fmt_stylize(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use fmt::Debug;
if let Some(fg) = self.fg {
fg.stylize_debug(ColorDebugKind::Foreground).fmt(f)?;
}
if let Some(bg) = self.bg {
bg.stylize_debug(ColorDebugKind::Background).fmt(f)?;
}
#[cfg(feature = "underline-color")]
if let Some(underline_color) = self.underline_color {
underline_color
.stylize_debug(ColorDebugKind::Underline)
.fmt(f)?;
}
for modifier in self.add_modifier.iter() {
match modifier {
Modifier::BOLD => f.write_str(".bold()")?,
Modifier::DIM => f.write_str(".dim()")?,
Modifier::ITALIC => f.write_str(".italic()")?,
Modifier::UNDERLINED => f.write_str(".underlined()")?,
Modifier::SLOW_BLINK => f.write_str(".slow_blink()")?,
Modifier::RAPID_BLINK => f.write_str(".rapid_blink()")?,
Modifier::REVERSED => f.write_str(".reversed()")?,
Modifier::HIDDEN => f.write_str(".hidden()")?,
Modifier::CROSSED_OUT => f.write_str(".crossed_out()")?,
_ => f.write_fmt(format_args!(".add_modifier(Modifier::{modifier:?})"))?,
}
}
for modifier in self.sub_modifier.iter() {
match modifier {
Modifier::BOLD => f.write_str(".not_bold()")?,
Modifier::DIM => f.write_str(".not_dim()")?,
Modifier::ITALIC => f.write_str(".not_italic()")?,
Modifier::UNDERLINED => f.write_str(".not_underlined()")?,
Modifier::SLOW_BLINK => f.write_str(".not_slow_blink()")?,
Modifier::RAPID_BLINK => f.write_str(".not_rapid_blink()")?,
Modifier::REVERSED => f.write_str(".not_reversed()")?,
Modifier::HIDDEN => f.write_str(".not_hidden()")?,
Modifier::CROSSED_OUT => f.write_str(".not_crossed_out()")?,
_ => f.write_fmt(format_args!(".remove_modifier(Modifier::{modifier:?})"))?,
}
}
Ok(())
}
}
impl From<Color> for Style {
@@ -473,7 +508,8 @@ impl From<Color> for Style {
/// # Example
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::style::{Color, Style};
///
/// let style = Style::from(Color::Red);
/// ```
fn from(color: Color) -> Self {
@@ -487,7 +523,8 @@ impl From<(Color, Color)> for Style {
/// # Example
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::style::{Color, Style};
///
/// // red foreground, blue background
/// let style = Style::from((Color::Red, Color::Blue));
/// // default foreground, blue background
@@ -509,7 +546,8 @@ impl From<Modifier> for Style {
/// # Example
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::style::{Style, Modifier};
///
/// // add bold and italic
/// let style = Style::from(Modifier::BOLD|Modifier::ITALIC);
fn from(modifier: Modifier) -> Self {
@@ -523,7 +561,8 @@ impl From<(Modifier, Modifier)> for Style {
/// # Example
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::style::{Modifier, Style};
///
/// // add bold and italic, remove dim
/// let style = Style::from((Modifier::BOLD | Modifier::ITALIC, Modifier::DIM));
/// ```
@@ -542,7 +581,8 @@ impl From<(Color, Modifier)> for Style {
/// # Example
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::style::{Color, Modifier, Style};
///
/// // red foreground, add bold and italic
/// let style = Style::from((Color::Red, Modifier::BOLD | Modifier::ITALIC));
/// ```
@@ -559,7 +599,8 @@ impl From<(Color, Color, Modifier)> for Style {
/// # Example
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::style::{Color, Modifier, Style};
///
/// // red foreground, blue background, add bold and italic
/// let style = Style::from((Color::Red, Color::Blue, Modifier::BOLD | Modifier::ITALIC));
/// ```
@@ -575,7 +616,8 @@ impl From<(Color, Color, Modifier, Modifier)> for Style {
/// # Example
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::style::{Color, Modifier, Style};
///
/// // red foreground, blue background, add bold and italic, remove dim
/// let style = Style::from((
/// Color::Red,
@@ -605,6 +647,10 @@ mod tests {
#[case(Style::new().on_blue(), "Style::new().on_blue()")]
#[case(Style::new().bold(), "Style::new().bold()")]
#[case(Style::new().not_italic(), "Style::new().not_italic()")]
#[case(
Style::new().red().on_blue().bold().italic().not_dim().not_hidden(),
"Style::new().red().on_blue().bold().italic().not_dim().not_hidden()"
)]
fn debug(#[case] style: Style, #[case] expected: &'static str) {
assert_eq!(format!("{style:?}"), expected);
}

View File

@@ -44,7 +44,7 @@ use crate::style::stylize::{ColorDebug, ColorDebugKind};
/// ```
/// use std::str::FromStr;
///
/// use ratatui::prelude::*;
/// use ratatui::style::Color;
///
/// assert_eq!(Color::from_str("red"), Ok(Color::Red));
/// assert_eq!("red".parse(), Ok(Color::Red));
@@ -168,7 +168,9 @@ impl<'de> serde::Deserialize<'de> for Color {
/// # Examples
///
/// ```
/// use ratatui::prelude::*;
/// use std::str::FromStr;
///
/// use ratatui::style::Color;
///
/// #[derive(Debug, serde::Deserialize)]
/// struct Theme {
@@ -263,7 +265,7 @@ impl std::error::Error for ParseColorError {}
/// ```
/// use std::str::FromStr;
///
/// use ratatui::prelude::*;
/// use ratatui::style::Color;
///
/// let color: Color = Color::from_str("blue").unwrap();
/// assert_eq!(color, Color::Blue);
@@ -379,7 +381,7 @@ impl Color {
/// # Examples
///
/// ```
/// use ratatui::prelude::*;
/// use ratatui::style::Color;
///
/// let color: Color = Color::from_hsl(360.0, 100.0, 100.0);
/// assert_eq!(color, Color::Rgb(255, 255, 255));
@@ -396,6 +398,41 @@ impl Color {
// Delegate to the function for normalized HSL to RGB conversion
normalized_hsl_to_rgb(h / 360.0, s / 100.0, l / 100.0)
}
/// Converts a `HSLuv` representation to a `Color::Rgb` instance.
///
/// The `from_hsluv` function converts the Hue, Saturation and Lightness values to a
/// corresponding `Color` RGB equivalent.
///
/// Hue values should be in the range [0, 360].
/// Saturation and L values should be in the range [0, 100].
/// Values that are not in the range are clamped to be within the range.
///
/// # Examples
///
/// ```
/// use ratatui::prelude::*;
///
/// let color = Color::from_hsluv(360.0, 50.0, 75.0);
/// assert_eq!(color, Color::Rgb(223, 171, 181));
///
/// let color: Color = Color::from_hsluv(0.0, 0.0, 0.0);
/// assert_eq!(color, Color::Rgb(0, 0, 0));
/// ```
#[cfg(feature = "palette")]
pub fn from_hsluv(h: f64, s: f64, l: f64) -> Self {
use palette::{Clamp, FromColor, Hsluv, Srgb};
let hsluv = Hsluv::new(h, s, l).clamp();
let Srgb {
red,
green,
blue,
standard: _,
}: Srgb<u8> = Srgb::from_color(hsluv).into();
Self::Rgb(red, green, blue)
}
}
/// Converts normalized HSL (Hue, Saturation, Lightness) values to RGB (Red, Green, Blue) color
@@ -511,6 +548,34 @@ mod tests {
assert_eq!(color, Color::Rgb(0, 0, 0));
}
#[cfg(feature = "palette")]
#[test]
fn test_hsluv_to_rgb() {
// Test with valid HSLuv values
let color = Color::from_hsluv(120.0, 50.0, 75.0);
assert_eq!(color, Color::Rgb(147, 198, 129));
// Test with H value at upper bound
let color = Color::from_hsluv(360.0, 50.0, 75.0);
assert_eq!(color, Color::Rgb(223, 171, 181));
// Test with H value exceeding the upper bound
let color = Color::from_hsluv(400.0, 50.0, 75.0);
assert_eq!(color, Color::Rgb(226, 174, 140));
// Test with S and L values exceeding the upper bound
let color = Color::from_hsluv(240.0, 120.0, 150.0);
assert_eq!(color, Color::Rgb(255, 255, 255));
// Test with H, S, and L values below the lower bound
let color = Color::from_hsluv(0.0, 0.0, 0.0);
assert_eq!(color, Color::Rgb(0, 0, 0));
// Test with S and L values below the lower bound
let color = Color::from_hsluv(60.0, 0.0, 0.0);
assert_eq!(color, Color::Rgb(0, 0, 0));
}
#[test]
fn from_u32() {
assert_eq!(Color::from_u32(0x000000), Color::Rgb(0, 0, 0));

View File

@@ -403,8 +403,10 @@
//! # Example
//!
//! ```rust
//! # use ratatui::prelude::*;
//! use ratatui::style::palette::material::{BLUE, RED};
//! use ratatui::style::{
//! palette::material::{BLUE, RED},
//! Color,
//! };
//!
//! assert_eq!(RED.c500, Color::Rgb(244, 67, 54));
//! assert_eq!(BLUE.c500, Color::Rgb(33, 150, 243));
@@ -412,7 +414,7 @@
//!
//! [`matdesign-color` crate]: https://crates.io/crates/matdesign-color
use crate::prelude::*;
use crate::style::Color;
/// A palette of colors for use in Material design with accent colors
///

View File

@@ -268,14 +268,16 @@
//! # Example
//!
//! ```rust
//! # use ratatui::prelude::*;
//! use ratatui::style::palette::tailwind::{BLUE, RED};
//! use ratatui::style::{
//! palette::tailwind::{BLUE, RED},
//! Color,
//! };
//!
//! assert_eq!(RED.c500, Color::Rgb(239, 68, 68));
//! assert_eq!(BLUE.c500, Color::Rgb(59, 130, 246));
//! ```
use crate::prelude::*;
use crate::style::Color;
pub struct Palette {
pub c50: Color,

View File

@@ -7,7 +7,7 @@ use ::palette::{
};
use palette::{stimulus::IntoStimulus, Srgb};
use super::Color;
use crate::style::Color;
/// Convert an [`palette::Srgb`] color to a [`Color`].
///

View File

@@ -196,7 +196,11 @@ macro_rules! modifier {
///
/// # Examples
/// ```
/// use ratatui::{prelude::*, widgets::*};
/// use ratatui::{
/// style::{Color, Modifier, Style, Stylize},
/// text::Line,
/// widgets::{Block, Paragraph},
/// };
///
/// let span = "hello".red().on_blue().bold();
/// let line = Line::from(vec![

View File

@@ -155,7 +155,7 @@ pub enum Marker {
}
pub mod scrollbar {
use super::{block, line};
use crate::symbols::{block, line};
/// Scrollbar Set
/// ```text

View File

@@ -1,4 +1,4 @@
use super::{block, line};
use crate::symbols::{block, line};
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub struct Set {

View File

@@ -13,7 +13,7 @@
//! ```rust,no_run
//! use std::io::stdout;
//!
//! use ratatui::{prelude::*, widgets::Paragraph};
//! use ratatui::{backend::CrosstermBackend, widgets::Paragraph, Terminal};
//!
//! let backend = CrosstermBackend::new(stdout());
//! let mut terminal = Terminal::new(backend)?;

View File

@@ -1,4 +1,8 @@
use crate::prelude::*;
use crate::{
buffer::Buffer,
layout::{Position, Rect},
widgets::{StatefulWidget, StatefulWidgetRef, Widget, WidgetRef},
};
/// A consistent view into the terminal state for rendering a single frame.
///
@@ -10,6 +14,7 @@ use crate::prelude::*;
/// to the terminal. This avoids drawing redundant cells.
///
/// [`Buffer`]: crate::buffer::Buffer
/// [`Terminal::draw`]: crate::Terminal::draw
#[derive(Debug, Hash)]
pub struct Frame<'a> {
/// Where should the cursor be after drawing this frame?
@@ -31,6 +36,8 @@ pub struct Frame<'a> {
/// `CompletedFrame` represents the state of the terminal after all changes performed in the last
/// [`Terminal::draw`] call have been applied. Therefore, it is only valid until the next call to
/// [`Terminal::draw`].
///
/// [`Terminal::draw`]: crate::Terminal::draw
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct CompletedFrame<'a> {
/// The buffer that was used to draw the last frame.
@@ -73,10 +80,12 @@ impl Frame<'_> {
/// # Example
///
/// ```rust
/// # use ratatui::{backend::TestBackend, prelude::*, widgets::Block};
/// # use ratatui::{backend::TestBackend, Terminal};
/// # let backend = TestBackend::new(5, 5);
/// # let mut terminal = Terminal::new(backend).unwrap();
/// # let mut frame = terminal.get_frame();
/// use ratatui::{layout::Rect, widgets::Block};
///
/// let block = Block::new();
/// let area = Rect::new(0, 0, 5, 5);
/// frame.render_widget(block, area);
@@ -96,10 +105,12 @@ impl Frame<'_> {
///
/// ```rust
/// # #[cfg(feature = "unstable-widget-ref")] {
/// # use ratatui::{backend::TestBackend, prelude::*, widgets::Block};
/// # use ratatui::{backend::TestBackend, Terminal};
/// # let backend = TestBackend::new(5, 5);
/// # let mut terminal = Terminal::new(backend).unwrap();
/// # let mut frame = terminal.get_frame();
/// use ratatui::{layout::Rect, widgets::Block};
///
/// let block = Block::new();
/// let area = Rect::new(0, 0, 5, 5);
/// frame.render_widget_ref(block, area);
@@ -122,10 +133,15 @@ impl Frame<'_> {
/// # Example
///
/// ```rust
/// # use ratatui::{backend::TestBackend, prelude::*, widgets::*};
/// # use ratatui::{backend::TestBackend, Terminal};
/// # let backend = TestBackend::new(5, 5);
/// # let mut terminal = Terminal::new(backend).unwrap();
/// # let mut frame = terminal.get_frame();
/// use ratatui::{
/// layout::Rect,
/// widgets::{List, ListItem, ListState},
/// };
///
/// let mut state = ListState::default().with_selected(Some(1));
/// let list = List::new(vec![ListItem::new("Item 1"), ListItem::new("Item 2")]);
/// let area = Rect::new(0, 0, 5, 5);
@@ -153,10 +169,15 @@ impl Frame<'_> {
///
/// ```rust
/// # #[cfg(feature = "unstable-widget-ref")] {
/// # use ratatui::{backend::TestBackend, prelude::*, widgets::*};
/// # use ratatui::{backend::TestBackend, Terminal};
/// # let backend = TestBackend::new(5, 5);
/// # let mut terminal = Terminal::new(backend).unwrap();
/// # let mut frame = terminal.get_frame();
/// use ratatui::{
/// layout::Rect,
/// widgets::{List, ListItem, ListState},
/// };
///
/// let mut state = ListState::default().with_selected(Some(1));
/// let list = List::new(vec![ListItem::new("Item 1"), ListItem::new("Item 2")]);
/// let area = Rect::new(0, 0, 5, 5);
@@ -178,6 +199,10 @@ impl Frame<'_> {
/// Note that this will interfere with calls to [`Terminal::hide_cursor`],
/// [`Terminal::show_cursor`], and [`Terminal::set_cursor_position`]. Pick one of the APIs and
/// stick with it.
///
/// [`Terminal::hide_cursor`]: crate::Terminal::hide_cursor
/// [`Terminal::show_cursor`]: crate::Terminal::show_cursor
/// [`Terminal::set_cursor_position`]: crate::Terminal::set_cursor_position
pub fn set_cursor_position<P: Into<Position>>(&mut self, position: P) {
self.cursor_position = Some(position.into());
}
@@ -188,6 +213,10 @@ impl Frame<'_> {
/// Note that this will interfere with calls to [`Terminal::hide_cursor`],
/// [`Terminal::show_cursor`], and [`Terminal::set_cursor_position`]. Pick one of the APIs and
/// stick with it.
///
/// [`Terminal::hide_cursor`]: crate::Terminal::hide_cursor
/// [`Terminal::show_cursor`]: crate::Terminal::show_cursor
/// [`Terminal::set_cursor_position`]: crate::Terminal::set_cursor_position
#[deprecated = "the method set_cursor_position indicates more clearly what about the cursor to set"]
pub fn set_cursor(&mut self, x: u16, y: u16) {
self.set_cursor_position(Position { x, y });
@@ -215,7 +244,7 @@ impl Frame<'_> {
/// # Examples
///
/// ```rust
/// # use ratatui::{backend::TestBackend, prelude::*, widgets::*};
/// # use ratatui::{backend::TestBackend, Terminal};
/// # let backend = TestBackend::new(5, 5);
/// # let mut terminal = Terminal::new(backend).unwrap();
/// # let mut frame = terminal.get_frame();

View File

@@ -5,8 +5,7 @@ use crossterm::{
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use super::TerminalOptions;
use crate::{backend::CrosstermBackend, Terminal};
use crate::{backend::CrosstermBackend, terminal::TerminalOptions, Terminal};
/// A type alias for the default terminal type.
///

View File

@@ -1,7 +1,10 @@
use std::io;
use crate::{
backend::ClearType, buffer::Cell, prelude::*, CompletedFrame, TerminalOptions, Viewport,
backend::{Backend, ClearType},
buffer::{Buffer, Cell},
layout::{Position, Rect, Size},
CompletedFrame, Frame, TerminalOptions, Viewport,
};
/// An interface to interact and draw [`Frame`]s on the user's terminal.
@@ -30,10 +33,9 @@ use crate::{
/// # Examples
///
/// ```rust,no_run
/// # use ratatui::prelude::*;
/// use std::io::stdout;
///
/// use ratatui::widgets::Paragraph;
/// use ratatui::{backend::CrosstermBackend, widgets::Paragraph, Terminal};
///
/// let backend = CrosstermBackend::new(stdout());
/// let mut terminal = Terminal::new(backend)?;
@@ -107,8 +109,10 @@ where
/// # Example
///
/// ```rust,no_run
/// # use std::io::stdout;
/// # use ratatui::prelude::*;
/// use std::io::stdout;
///
/// use ratatui::{backend::CrosstermBackend, Terminal};
///
/// let backend = CrosstermBackend::new(stdout());
/// let terminal = Terminal::new(backend)?;
/// # std::io::Result::Ok(())
@@ -127,8 +131,10 @@ where
/// # Example
///
/// ```rust
/// # use std::io::stdout;
/// # use ratatui::{prelude::*, backend::TestBackend, Viewport, TerminalOptions};
/// use std::io::stdout;
///
/// use ratatui::{backend::CrosstermBackend, layout::Rect, Terminal, TerminalOptions, Viewport};
///
/// let backend = CrosstermBackend::new(stdout());
/// let viewport = Viewport::Fixed(Rect::new(0, 0, 10, 10));
/// let terminal = Terminal::with_options(backend, TerminalOptions { viewport })?;
@@ -276,10 +282,9 @@ where
/// # Examples
///
/// ```
/// # use ratatui::layout::Position;
/// # let backend = ratatui::backend::TestBackend::new(10, 10);
/// # let mut terminal = ratatui::Terminal::new(backend)?;
/// use ratatui::widgets::Paragraph;
/// use ratatui::{layout::Position, widgets::Paragraph};
///
/// // with a closure
/// terminal.draw(|frame| {
@@ -552,7 +557,13 @@ where
/// ## Insert a single line before the current viewport
///
/// ```rust
/// # use ratatui::{backend::TestBackend, prelude::*, widgets::*};
/// use ratatui::{
/// backend::TestBackend,
/// style::{Color, Style},
/// text::{Line, Span},
/// widgets::{Paragraph, Widget},
/// Terminal,
/// };
/// # let backend = TestBackend::new(10, 10);
/// # let mut terminal = Terminal::new(backend).unwrap();
/// terminal.insert_before(1, |buf| {
@@ -568,10 +579,22 @@ where
where
F: FnOnce(&mut Buffer),
{
if !matches!(self.viewport, Viewport::Inline(_)) {
return Ok(());
match self.viewport {
#[cfg(feature = "scrolling-regions")]
Viewport::Inline(_) => self.insert_before_scrolling_regions(height, draw_fn),
#[cfg(not(feature = "scrolling-regions"))]
Viewport::Inline(_) => self.insert_before_no_scrolling_regions(height, draw_fn),
_ => Ok(()),
}
}
/// Implement `Self::insert_before` using standard backend capabilities.
#[cfg(not(feature = "scrolling-regions"))]
fn insert_before_no_scrolling_regions(
&mut self,
height: u16,
draw_fn: impl FnOnce(&mut Buffer),
) -> io::Result<()> {
// The approach of this function is to first render all of the lines to insert into a
// temporary buffer, and then to loop drawing chunks from the buffer to the screen. drawing
// this buffer onto the screen.
@@ -657,6 +680,86 @@ where
Ok(())
}
/// Implement `Self::insert_before` using scrolling regions.
///
/// If a terminal supports scrolling regions, it means that we can define a subset of rows of
/// the screen, and then tell the terminal to scroll up or down just within that region. The
/// rows outside of the region are not affected.
///
/// This function utilizes this feature to avoid having to redraw the viewport. This is done
/// either by splitting the screen at the top of the viewport, and then creating a gap by
/// either scrolling the viewport down, or scrolling the area above it up. The lines to insert
/// are then drawn into the gap created.
#[cfg(feature = "scrolling-regions")]
fn insert_before_scrolling_regions(
&mut self,
mut height: u16,
draw_fn: impl FnOnce(&mut Buffer),
) -> io::Result<()> {
// The approach of this function is to first render all of the lines to insert into a
// temporary buffer, and then to loop drawing chunks from the buffer to the screen. drawing
// this buffer onto the screen.
let area = Rect {
x: 0,
y: 0,
width: self.viewport_area.width,
height,
};
let mut buffer = Buffer::empty(area);
draw_fn(&mut buffer);
let mut buffer = buffer.content.as_slice();
// Handle the special case where the viewport takes up the whole screen.
if self.viewport_area.height == self.last_known_area.height {
// "Borrow" the top line of the viewport. Draw over it, then immediately scroll it into
// scrollback. Do this repeatedly until the whole buffer has been put into scrollback.
let mut first = true;
while !buffer.is_empty() {
buffer = if first {
self.draw_lines(0, 1, buffer)?
} else {
self.draw_lines_over_cleared(0, 1, buffer)?
};
first = false;
self.backend.scroll_region_up(0..1, 1)?;
}
// Redraw the top line of the viewport.
let width = self.viewport_area.width as usize;
let top_line = self.buffers[1 - self.current].content[0..width].to_vec();
self.draw_lines_over_cleared(0, 1, &top_line)?;
return Ok(());
}
// Handle the case where the viewport isn't yet at the bottom of the screen.
{
let viewport_top = self.viewport_area.top();
let viewport_bottom = self.viewport_area.bottom();
let screen_bottom = self.last_known_area.bottom();
if viewport_bottom < screen_bottom {
let to_draw = height.min(screen_bottom - viewport_bottom);
self.backend
.scroll_region_down(viewport_top..viewport_bottom + to_draw, to_draw)?;
buffer = self.draw_lines_over_cleared(viewport_top, to_draw, buffer)?;
self.set_viewport_area(Rect {
y: viewport_top + to_draw,
..self.viewport_area
});
height -= to_draw;
}
}
let viewport_top = self.viewport_area.top();
while height > 0 {
let to_draw = height.min(viewport_top);
self.backend.scroll_region_up(0..viewport_top, to_draw)?;
buffer = self.draw_lines_over_cleared(viewport_top - to_draw, to_draw, buffer)?;
height -= to_draw;
}
Ok(())
}
/// Draw lines at the given vertical offset. The slice of cells must contain enough cells
/// for the requested lines. A slice of the unused cells are returned.
fn draw_lines<'a>(
@@ -678,7 +781,33 @@ where
Ok(remainder)
}
/// Draw lines at the given vertical offset, assuming that the lines they are replacing on the
/// screen are cleared. The slice of cells must contain enough cells for the requested lines. A
/// slice of the unused cells are returned.
#[cfg(feature = "scrolling-regions")]
fn draw_lines_over_cleared<'a>(
&mut self,
y_offset: u16,
lines_to_draw: u16,
cells: &'a [Cell],
) -> io::Result<&'a [Cell]> {
let width: usize = self.last_known_area.width.into();
let (to_draw, remainder) = cells.split_at(width * lines_to_draw as usize);
if lines_to_draw > 0 {
let area = Rect::new(0, y_offset, width as u16, y_offset + lines_to_draw);
let old = Buffer::empty(area);
let new = Buffer {
area,
content: to_draw.to_vec(),
};
self.backend.draw(old.diff(&new).into_iter())?;
self.backend.flush()?;
}
Ok(remainder)
}
/// Scroll the whole screen up by the given number of lines.
#[cfg(not(feature = "scrolling-regions"))]
fn scroll_up(&mut self, lines_to_scroll: u16) -> io::Result<()> {
if lines_to_scroll > 0 {
self.set_cursor_position(Position::new(

View File

@@ -1,6 +1,6 @@
use std::fmt;
use crate::prelude::*;
use crate::layout::Rect;
/// Represents the viewport of the terminal. The viewport is the area of the terminal that is
/// currently visible to the user. It can be either fullscreen, inline or fixed.
@@ -14,6 +14,8 @@ use crate::prelude::*;
/// by a [`Rect`].
///
/// See [`Terminal::with_options`] for more information.
///
/// [`Terminal::with_options`]: crate::Terminal::with_options
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub enum Viewport {
/// The viewport is fullscreen

View File

@@ -19,7 +19,11 @@
//! its `title` property (which is a [`Line`] under the hood):
//!
//! ```rust
//! use ratatui::{prelude::*, widgets::*};
//! use ratatui::{
//! style::{Color, Style},
//! text::{Line, Span},
//! widgets::Block,
//! };
//!
//! // A simple string with no styling.
//! // Converted to Line(vec![

View File

@@ -1,4 +1,4 @@
use crate::{prelude::*, style::Styled};
use crate::style::{Style, Styled};
const NBSP: &str = "\u{00a0}";
const ZWSP: &str = "\u{200b}";
@@ -19,6 +19,8 @@ impl<'a> StyledGrapheme<'a> {
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// [`Color`]: crate::style::Color
pub fn new<S: Into<Style>>(symbol: &'a str, style: S) -> Self {
Self {
symbol,
@@ -48,6 +50,7 @@ impl<'a> Styled for StyledGrapheme<'a> {
#[cfg(test)]
mod tests {
use super::*;
use crate::style::Stylize;
#[test]
fn new() {

View File

@@ -4,7 +4,13 @@ use std::{borrow::Cow, fmt};
use unicode_truncate::UnicodeTruncateStr;
use crate::{prelude::*, style::Styled, text::StyledGrapheme};
use crate::{
buffer::Buffer,
layout::{Alignment, Rect},
style::{Style, Styled},
text::{Span, StyledGrapheme, Text},
widgets::{Widget, WidgetRef},
};
/// A line of text, consisting of one or more [`Span`]s.
///
@@ -69,7 +75,10 @@ use crate::{prelude::*, style::Styled, text::StyledGrapheme};
/// [`Style`].
///
/// ```rust
/// use ratatui::prelude::*;
/// use ratatui::{
/// style::{Color, Modifier, Style, Stylize},
/// text::{Line, Span},
/// };
///
/// let style = Style::new().yellow();
/// let line = Line::raw("Hello, world!").style(style);
@@ -93,7 +102,11 @@ use crate::{prelude::*, style::Styled, text::StyledGrapheme};
/// methods of the [`Stylize`] trait.
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::{
/// style::{Color, Modifier, Style, Stylize},
/// text::Line,
/// };
///
/// let line = Line::from("Hello world!").style(Style::new().yellow().italic());
/// let line = Line::from("Hello world!").style(Color::Yellow);
/// let line = Line::from("Hello world!").style((Color::Yellow, Color::Black));
@@ -108,7 +121,8 @@ use crate::{prelude::*, style::Styled, text::StyledGrapheme};
/// ignored and the line is truncated.
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::{layout::Alignment, text::Line};
///
/// let line = Line::from("Hello world!").alignment(Alignment::Right);
/// let line = Line::from("Hello world!").centered();
/// let line = Line::from("Hello world!").left_aligned();
@@ -120,7 +134,15 @@ use crate::{prelude::*, style::Styled, text::StyledGrapheme};
/// `Line` implements the [`Widget`] trait, which means it can be rendered to a [`Buffer`].
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::{
/// buffer::Buffer,
/// layout::Rect,
/// style::{Style, Stylize},
/// text::Line,
/// widgets::Widget,
/// Frame,
/// };
///
/// # fn render(area: Rect, buf: &mut Buffer) {
/// // in another widget's render method
/// let line = Line::from("Hello world!").style(Style::new().yellow().italic());
@@ -139,7 +161,14 @@ use crate::{prelude::*, style::Styled, text::StyledGrapheme};
/// provides more functionality.
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::{
/// buffer::Buffer,
/// layout::Rect,
/// style::Stylize,
/// text::Line,
/// widgets::{Paragraph, Widget, Wrap},
/// };
///
/// # fn render(area: Rect, buf: &mut Buffer) {
/// let line = Line::from("Hello world!").yellow().italic();
/// Paragraph::new(line)
@@ -149,6 +178,7 @@ use crate::{prelude::*, style::Styled, text::StyledGrapheme};
/// ```
///
/// [`Paragraph`]: crate::widgets::Paragraph
/// [`Stylize`]: crate::style::Stylize
#[derive(Default, Clone, Eq, PartialEq, Hash)]
pub struct Line<'a> {
/// The style of this line of text.
@@ -163,18 +193,28 @@ pub struct Line<'a> {
impl fmt::Debug for Line<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.style == Style::default() && self.alignment.is_none() {
f.write_str("Line ")?;
return f.debug_list().entries(&self.spans).finish();
if self.spans.is_empty() {
f.write_str("Line::default()")?;
} else if self.spans.len() == 1 && self.spans[0].style == Style::default() {
f.write_str(r#"Line::from(""#)?;
f.write_str(&self.spans[0].content)?;
f.write_str(r#"")"#)?;
} else if self.spans.len() == 1 {
f.write_str("Line::from(")?;
self.spans[0].fmt(f)?;
f.write_str(")")?;
} else {
f.write_str("Line::from_iter(")?;
f.debug_list().entries(&self.spans).finish()?;
f.write_str(")")?;
}
let mut debug = f.debug_struct("Line");
if self.style != Style::default() {
debug.field("style", &self.style);
self.style.fmt_stylize(f)?;
match self.alignment {
Some(Alignment::Left) => write!(f, ".left_aligned()"),
Some(Alignment::Center) => write!(f, ".centered()"),
Some(Alignment::Right) => write!(f, ".right_aligned()"),
None => Ok(()),
}
if let Some(alignment) = self.alignment {
debug.field("alignment", &format!("Alignment::{alignment}"));
}
debug.field("spans", &self.spans).finish()
}
}
@@ -199,8 +239,10 @@ impl<'a> Line<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// # use std::borrow::Cow;
/// use std::borrow::Cow;
///
/// use ratatui::text::Line;
///
/// Line::raw("test content");
/// Line::raw(String::from("test content"));
/// Line::raw(Cow::from("test content"));
@@ -228,13 +270,20 @@ impl<'a> Line<'a> {
/// Any newlines in the content are removed.
///
/// ```rust
/// # use ratatui::prelude::*;
/// # use std::borrow::Cow;
/// use std::borrow::Cow;
///
/// use ratatui::{
/// style::{Style, Stylize},
/// text::Line,
/// };
///
/// let style = Style::new().yellow().italic();
/// Line::styled("My text", style);
/// Line::styled(String::from("My text"), style);
/// Line::styled(Cow::from("test content"), style);
/// ```
///
/// [`Color`]: crate::style::Color
pub fn styled<T, S>(content: T, style: S) -> Self
where
T: Into<Cow<'a, str>>,
@@ -255,7 +304,8 @@ impl<'a> Line<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::{style::Stylize, text::Line};
///
/// let line = Line::default().spans(vec!["Hello".blue(), " world!".green()]);
/// let line = Line::default().spans([1, 2, 3].iter().map(|i| format!("Item {}", i)));
/// ```
@@ -282,9 +332,15 @@ impl<'a> Line<'a> {
///
/// # Examples
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::{
/// style::{Style, Stylize},
/// text::Line,
/// };
///
/// let mut line = Line::from("foo").style(Style::new().red());
/// ```
///
/// [`Color`]: crate::style::Color
#[must_use = "method moves the value of self and returns the modified value"]
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = style.into();
@@ -300,7 +356,8 @@ impl<'a> Line<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::{layout::Alignment, text::Line};
///
/// let mut line = Line::from("Hi, what's up?");
/// assert_eq!(None, line.alignment);
/// assert_eq!(
@@ -325,7 +382,8 @@ impl<'a> Line<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::text::Line;
///
/// let line = Line::from("Hi, what's up?").left_aligned();
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
@@ -342,7 +400,8 @@ impl<'a> Line<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::text::Line;
///
/// let line = Line::from("Hi, what's up?").centered();
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
@@ -359,7 +418,8 @@ impl<'a> Line<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::text::Line;
///
/// let line = Line::from("Hi, what's up?").right_aligned();
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
@@ -372,7 +432,8 @@ impl<'a> Line<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::{style::Stylize, text::Line};
///
/// let line = Line::from(vec!["Hello".blue(), " world!".green()]);
/// assert_eq!(12, line.width());
/// ```
@@ -393,7 +454,10 @@ impl<'a> Line<'a> {
/// ```rust
/// use std::iter::Iterator;
///
/// use ratatui::{prelude::*, text::StyledGrapheme};
/// use ratatui::{
/// style::{Color, Style},
/// text::{Line, StyledGrapheme},
/// };
///
/// let line = Line::styled("Text", Style::default().fg(Color::Yellow));
/// let style = Style::default().fg(Color::Green).bg(Color::Black);
@@ -408,6 +472,8 @@ impl<'a> Line<'a> {
/// ]
/// );
/// ```
///
/// [`Color`]: crate::style::Color
pub fn styled_graphemes<S: Into<Style>>(
&'a self,
base_style: S,
@@ -432,13 +498,19 @@ impl<'a> Line<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::{
/// style::{Color, Modifier},
/// text::Line,
/// };
///
/// let line = Line::styled("My text", Modifier::ITALIC);
///
/// let styled_line = Line::styled("My text", (Color::Yellow, Modifier::ITALIC));
///
/// assert_eq!(styled_line, line.patch_style(Color::Yellow));
/// ```
///
/// [`Color`]: crate::style::Color
#[must_use = "method moves the value of self and returns the modified value"]
pub fn patch_style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = self.style.patch(style);
@@ -454,8 +526,12 @@ impl<'a> Line<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// # let style = Style::default().yellow();
/// use ratatui::{
/// style::{Style, Stylize},
/// text::Line,
/// };
///
/// let line = Line::styled("My text", style);
///
/// assert_eq!(Style::reset(), line.reset_style().style);
@@ -483,7 +559,8 @@ impl<'a> Line<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::text::{Line, Span};
///
/// let mut line = Line::from("Hello, ");
/// line.push_span(Span::raw("world!"));
/// line.push_span(" How are you?");
@@ -532,6 +609,12 @@ impl<'a> From<&'a str> for Line<'a> {
}
}
impl<'a> From<Cow<'a, str>> for Line<'a> {
fn from(s: Cow<'a, str>) -> Self {
Self::raw(s)
}
}
impl<'a> From<Vec<Span<'a>>> for Line<'a> {
fn from(spans: Vec<Span<'a>>) -> Self {
Self {
@@ -749,6 +832,7 @@ mod tests {
use rstest::{fixture, rstest};
use super::*;
use crate::style::{Color, Modifier, Stylize};
#[fixture]
fn small_buf() -> Buffer {
@@ -1522,4 +1606,49 @@ mod tests {
assert_eq!(result, "Hello world!");
}
}
#[rstest]
#[case::empty(Line::default(), "Line::default()")]
#[case::raw(Line::raw("Hello, world!"), r#"Line::from("Hello, world!")"#)]
#[case::styled(
Line::styled("Hello, world!", Color::Yellow),
r#"Line::from("Hello, world!").yellow()"#
)]
#[case::styled_complex(
Line::from(String::from("Hello, world!")).green().on_blue().bold().italic().not_dim(),
r#"Line::from("Hello, world!").green().on_blue().bold().italic().not_dim()"#
)]
#[case::styled_span(
Line::from(Span::styled("Hello, world!", Color::Yellow)),
r#"Line::from(Span::from("Hello, world!").yellow())"#
)]
#[case::styled_line_and_span(
Line::from(vec![
Span::styled("Hello", Color::Yellow),
Span::styled(" world!", Color::Green),
]).italic(),
r#"Line::from_iter([Span::from("Hello").yellow(), Span::from(" world!").green()]).italic()"#
)]
#[case::spans_vec(
Line::from(vec![
Span::styled("Hello", Color::Blue),
Span::styled(" world!", Color::Green),
]),
r#"Line::from_iter([Span::from("Hello").blue(), Span::from(" world!").green()])"#,
)]
#[case::left_aligned(
Line::from("Hello, world!").left_aligned(),
r#"Line::from("Hello, world!").left_aligned()"#
)]
#[case::centered(
Line::from("Hello, world!").centered(),
r#"Line::from("Hello, world!").centered()"#
)]
#[case::right_aligned(
Line::from("Hello, world!").right_aligned(),
r#"Line::from("Hello, world!").right_aligned()"#
)]
fn debug(#[case] line: Line, #[case] expected: &str) {
assert_eq!(format!("{line:?}"), expected);
}
}

View File

@@ -1,6 +1,6 @@
use std::{borrow::Cow, fmt};
use super::Text;
use crate::text::Text;
/// A wrapper around a string that is masked when displayed.
///
@@ -10,7 +10,12 @@ use super::Text;
/// # Examples
///
/// ```rust
/// use ratatui::{prelude::*, widgets::*};
/// use ratatui::{
/// buffer::Buffer,
/// layout::Rect,
/// text::Masked,
/// widgets::{Paragraph, Widget},
/// };
///
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
/// let password = Masked::new("12345", 'x');

View File

@@ -3,7 +3,13 @@ use std::{borrow::Cow, fmt};
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use crate::{prelude::*, style::Styled, text::StyledGrapheme};
use crate::{
buffer::Buffer,
layout::Rect,
style::{Style, Styled},
text::{Line, StyledGrapheme},
widgets::{Widget, WidgetRef},
};
/// Represents a part of a line that is contiguous and where all characters share the same style.
///
@@ -36,7 +42,7 @@ use crate::{prelude::*, style::Styled, text::StyledGrapheme};
/// any type convertible to [`Cow<str>`].
///
/// ```rust
/// use ratatui::prelude::*;
/// use ratatui::text::Span;
///
/// let span = Span::raw("test content");
/// let span = Span::raw(String::from("test content"));
@@ -50,7 +56,10 @@ use crate::{prelude::*, style::Styled, text::StyledGrapheme};
/// the [`Stylize`] trait.
///
/// ```rust
/// use ratatui::prelude::*;
/// use ratatui::{
/// style::{Style, Stylize},
/// text::Span,
/// };
///
/// let span = Span::styled("test content", Style::new().green());
/// let span = Span::styled(String::from("test content"), Style::new().green());
@@ -64,7 +73,7 @@ use crate::{prelude::*, style::Styled, text::StyledGrapheme};
/// defined in the [`Stylize`] trait.
///
/// ```rust
/// use ratatui::prelude::*;
/// use ratatui::{style::Stylize, text::Span};
///
/// let span = Span::raw("test content").green().on_yellow().italic();
/// let span = Span::raw(String::from("test content"))
@@ -78,7 +87,7 @@ use crate::{prelude::*, style::Styled, text::StyledGrapheme};
/// wrapping and alignment for you.
///
/// ```rust
/// use ratatui::prelude::*;
/// use ratatui::{style::Stylize, Frame};
///
/// # fn render_frame(frame: &mut Frame) {
/// frame.render_widget("test content".green().on_yellow().italic(), frame.area());
@@ -98,13 +107,15 @@ pub struct Span<'a> {
impl fmt::Debug for Span<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.style == Style::default() {
return write!(f, "Span({:?})", self.content);
if self.content.is_empty() {
write!(f, "Span::default()")?;
} else {
write!(f, "Span::from({:?})", self.content)?;
}
f.debug_struct("Span")
.field("style", &self.style)
.field("content", &self.content)
.finish()
if self.style != Style::default() {
self.style.fmt_stylize(f)?;
}
Ok(())
}
}
@@ -114,7 +125,8 @@ impl<'a> Span<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::text::Span;
///
/// Span::raw("test content");
/// Span::raw(String::from("test content"));
/// ```
@@ -139,11 +151,17 @@ impl<'a> Span<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::{
/// style::{Style, Stylize},
/// text::Span,
/// };
///
/// let style = Style::new().yellow().on_green().italic();
/// Span::styled("test content", style);
/// Span::styled(String::from("test content"), style);
/// ```
///
/// [`Color`]: crate::style::Color
pub fn styled<T, S>(content: T, style: S) -> Self
where
T: Into<Cow<'a, str>>,
@@ -165,7 +183,8 @@ impl<'a> Span<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::text::Span;
///
/// let mut span = Span::default().content("content");
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
@@ -190,9 +209,15 @@ impl<'a> Span<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::{
/// style::{Style, Stylize},
/// text::Span,
/// };
///
/// let mut span = Span::default().style(Style::new().green());
/// ```
///
/// [`Color`]: crate::style::Color
#[must_use = "method moves the value of self and returns the modified value"]
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = style.into();
@@ -209,11 +234,17 @@ impl<'a> Span<'a> {
/// # Example
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::{
/// style::{Style, Stylize},
/// text::Span,
/// };
///
/// let span = Span::styled("test content", Style::new().green().italic())
/// .patch_style(Style::new().red().on_yellow().bold());
/// assert_eq!(span.style, Style::new().red().on_yellow().italic().bold());
/// ```
///
/// [`Color`]: crate::style::Color
#[must_use = "method moves the value of self and returns the modified value"]
pub fn patch_style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = self.style.patch(style);
@@ -229,7 +260,11 @@ impl<'a> Span<'a> {
/// # Example
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::{
/// style::{Style, Stylize},
/// text::Span,
/// };
///
/// let span = Span::styled(
/// "Test Content",
/// Style::new().dark_gray().on_yellow().italic(),
@@ -260,7 +295,10 @@ impl<'a> Span<'a> {
/// ```rust
/// use std::iter::Iterator;
///
/// use ratatui::{prelude::*, text::StyledGrapheme};
/// use ratatui::{
/// style::{Style, Stylize},
/// text::{Span, StyledGrapheme},
/// };
///
/// let span = Span::styled("Test", Style::new().green().italic());
/// let style = Style::new().red().on_yellow();
@@ -275,6 +313,8 @@ impl<'a> Span<'a> {
/// ],
/// );
/// ```
///
/// [`Color`]: crate::style::Color
pub fn styled_graphemes<S: Into<Style>>(
&'a self,
base_style: S,
@@ -292,7 +332,8 @@ impl<'a> Span<'a> {
/// # Example
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::style::Stylize;
///
/// let line = "Test Content".green().italic().into_left_aligned_line();
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
@@ -311,7 +352,8 @@ impl<'a> Span<'a> {
/// # Example
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::style::Stylize;
///
/// let line = "Test Content".green().italic().into_centered_line();
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
@@ -330,7 +372,8 @@ impl<'a> Span<'a> {
/// # Example
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::style::Stylize;
///
/// let line = "Test Content".green().italic().into_right_aligned_line();
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
@@ -464,10 +507,10 @@ impl fmt::Display for Span<'_> {
#[cfg(test)]
mod tests {
use buffer::Cell;
use rstest::fixture;
use rstest::{fixture, rstest};
use super::*;
use crate::{buffer::Cell, layout::Alignment, style::Stylize};
#[fixture]
fn small_buf() -> Buffer {
@@ -843,4 +886,16 @@ mod tests {
Line::from(vec![Span::raw("test"), Span::raw("content")])
);
}
#[rstest]
#[case::default(Span::default(), "Span::default()")]
#[case::raw(Span::raw("test"), r#"Span::from("test")"#)]
#[case::styled(Span::styled("test", Style::new().green()), r#"Span::from("test").green()"#)]
#[case::styled_italic(
Span::styled("test", Style::new().green().italic()),
r#"Span::from("test").green().italic()"#
)]
fn debug(#[case] span: Span, #[case] expected: &str) {
assert_eq!(format!("{span:?}"), expected);
}
}

View File

@@ -1,7 +1,13 @@
#![warn(missing_docs)]
use std::{borrow::Cow, fmt};
use crate::{prelude::*, style::Styled};
use crate::{
buffer::Buffer,
layout::{Alignment, Rect},
style::{Style, Styled},
text::{Line, Span},
widgets::{Widget, WidgetRef},
};
/// A string split over one or more lines.
///
@@ -62,7 +68,10 @@ use crate::{prelude::*, style::Styled};
/// ```rust
/// use std::{borrow::Cow, iter};
///
/// use ratatui::prelude::*;
/// use ratatui::{
/// style::{Color, Modifier, Style, Stylize},
/// text::{Line, Span, Text},
/// };
///
/// let style = Style::new().yellow().italic();
/// let text = Text::raw("The first line\nThe second line").style(style);
@@ -99,7 +108,11 @@ use crate::{prelude::*, style::Styled};
/// [`Stylize`] trait.
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::{
/// style::{Color, Modifier, Style, Stylize},
/// text::{Line, Text},
/// };
///
/// let text = Text::from("The first line\nThe second line").style(Style::new().yellow().italic());
/// let text = Text::from("The first line\nThe second line")
/// .yellow()
@@ -116,7 +129,11 @@ use crate::{prelude::*, style::Styled};
/// Lines composing the text can also be individually aligned with [`Line::alignment`].
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::{
/// layout::Alignment,
/// text::{Line, Text},
/// };
///
/// let text = Text::from("The first line\nThe second line").alignment(Alignment::Right);
/// let text = Text::from("The first line\nThe second line").right_aligned();
/// let text = Text::from(vec![
@@ -132,7 +149,9 @@ use crate::{prelude::*, style::Styled};
/// [`Frame`].
///
/// ```rust
/// # use ratatui::prelude::*;
/// # use ratatui::{buffer::Buffer, layout::Rect};
/// use ratatui::{text::Text, widgets::Widget, Frame};
///
/// // within another widget's `render` method:
/// # fn render(area: Rect, buf: &mut Buffer) {
/// let text = Text::from("The first line\nThe second line");
@@ -152,7 +171,13 @@ use crate::{prelude::*, style::Styled};
/// provides more functionality.
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::{
/// buffer::Buffer,
/// layout::Rect,
/// text::Text,
/// widgets::{Paragraph, Widget, Wrap},
/// };
///
/// # fn render(area: Rect, buf: &mut Buffer) {
/// let text = Text::from("The first line\nThe second line");
/// let paragraph = Paragraph::new(text)
@@ -163,6 +188,8 @@ use crate::{prelude::*, style::Styled};
/// ```
///
/// [`Paragraph`]: crate::widgets::Paragraph
/// [`Stylize`]: crate::style::Stylize
/// [`Frame`]: crate::Frame
#[derive(Default, Clone, Eq, PartialEq, Hash)]
pub struct Text<'a> {
/// The alignment of this text.
@@ -175,19 +202,23 @@ pub struct Text<'a> {
impl fmt::Debug for Text<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.style == Style::default() && self.alignment.is_none() {
f.write_str("Text ")?;
f.debug_list().entries(&self.lines).finish()
if self.lines.is_empty() {
f.write_str("Text::default()")?;
} else if self.lines.len() == 1 {
write!(f, "Text::from({:?})", self.lines[0])?;
} else {
let mut debug = f.debug_struct("Text");
if self.style != Style::default() {
debug.field("style", &self.style);
}
if let Some(alignment) = self.alignment {
debug.field("alignment", &format!("Alignment::{alignment}"));
}
debug.field("lines", &self.lines).finish()
f.write_str("Text::from_iter(")?;
f.debug_list().entries(self.lines.iter()).finish()?;
f.write_str(")")?;
}
self.style.fmt_stylize(f)?;
match self.alignment {
Some(Alignment::Left) => f.write_str(".left_aligned()")?,
Some(Alignment::Center) => f.write_str(".centered()")?,
Some(Alignment::Right) => f.write_str(".right_aligned()")?,
_ => (),
}
Ok(())
}
}
@@ -197,7 +228,8 @@ impl<'a> Text<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::text::Text;
///
/// Text::raw("The first line\nThe second line");
/// Text::raw(String::from("The first line\nThe second line"));
/// ```
@@ -222,13 +254,19 @@ impl<'a> Text<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::{
/// style::{Color, Modifier, Style},
/// text::Text,
/// };
///
/// let style = Style::default()
/// .fg(Color::Yellow)
/// .add_modifier(Modifier::ITALIC);
/// Text::styled("The first line\nThe second line", style);
/// Text::styled(String::from("The first line\nThe second line"), style);
/// ```
///
/// [`Color`]: crate::style::Color
pub fn styled<T, S>(content: T, style: S) -> Self
where
T: Into<Cow<'a, str>>,
@@ -242,7 +280,8 @@ impl<'a> Text<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::text::Text;
///
/// let text = Text::from("The first line\nThe second line");
/// assert_eq!(15, text.width());
/// ```
@@ -255,7 +294,8 @@ impl<'a> Text<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::text::Text;
///
/// let text = Text::from("The first line\nThe second line");
/// assert_eq!(2, text.height());
/// ```
@@ -276,9 +316,15 @@ impl<'a> Text<'a> {
///
/// # Examples
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::{
/// style::{Style, Stylize},
/// text::Text,
/// };
///
/// let mut line = Text::from("foo").style(Style::new().red());
/// ```
///
/// [`Color`]: crate::style::Color
#[must_use = "method moves the value of self and returns the modified value"]
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = style.into();
@@ -302,7 +348,11 @@ impl<'a> Text<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::{
/// style::{Color, Modifier},
/// text::Text,
/// };
///
/// let raw_text = Text::styled("The first line\nThe second line", Modifier::ITALIC);
/// let styled_text = Text::styled(
/// String::from("The first line\nThe second line"),
@@ -313,6 +363,9 @@ impl<'a> Text<'a> {
/// let raw_text = raw_text.patch_style(Color::Yellow);
/// assert_eq!(raw_text, styled_text);
/// ```
///
/// [`Color`]: crate::style::Color
/// [`Stylize`]: crate::style::Stylize
#[must_use = "method moves the value of self and returns the modified value"]
pub fn patch_style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = self.style.patch(style);
@@ -328,7 +381,11 @@ impl<'a> Text<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::{
/// style::{Color, Modifier, Style},
/// text::Text,
/// };
///
/// let text = Text::styled(
/// "The first line\nThe second line",
/// (Color::Yellow, Modifier::ITALIC),
@@ -355,7 +412,8 @@ impl<'a> Text<'a> {
/// Set alignment to the whole text.
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::{layout::Alignment, text::Text};
///
/// let mut text = Text::from("Hi, what's up?");
/// assert_eq!(None, text.alignment);
/// assert_eq!(
@@ -367,7 +425,11 @@ impl<'a> Text<'a> {
/// Set a default alignment and override it on a per line basis.
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::{
/// layout::Alignment,
/// text::{Line, Text},
/// };
///
/// let text = Text::from(vec![
/// Line::from("left").alignment(Alignment::Left),
/// Line::from("default"),
@@ -404,7 +466,8 @@ impl<'a> Text<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::text::Text;
///
/// let text = Text::from("Hi, what's up?").left_aligned();
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
@@ -423,7 +486,8 @@ impl<'a> Text<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::text::Text;
///
/// let text = Text::from("Hi, what's up?").centered();
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
@@ -442,7 +506,8 @@ impl<'a> Text<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::text::Text;
///
/// let text = Text::from("Hi, what's up?").right_aligned();
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
@@ -468,7 +533,8 @@ impl<'a> Text<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::text::{Line, Span, Text};
///
/// let mut text = Text::from("Hello, world!");
/// text.push_line(Line::from("How are you?"));
/// text.push_line(Span::from("How are you?"));
@@ -486,7 +552,8 @@ impl<'a> Text<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui::text::{Span, Text};
///
/// let mut text = Text::from("Hello, world!");
/// text.push_span(Span::from("How are you?"));
/// text.push_span("How are you?");
@@ -708,6 +775,7 @@ mod tests {
use rstest::{fixture, rstest};
use super::*;
use crate::style::{Color, Modifier, Stylize};
#[fixture]
fn small_buf() -> Buffer {
@@ -1251,45 +1319,68 @@ mod tests {
}
}
mod debug {
use super::*;
#[rstest]
#[case::default(Text::default(), "Text::default()")]
// TODO jm: these could be improved to inspect the line / span if there's only one. e.g.
// Text::from("Hello, world!") and Text::from("Hello, world!".blue()) but the current
// implementation is good enough for now.
#[case::raw(
Text::raw("Hello, world!"),
r#"Text::from(Line::from("Hello, world!"))"#
)]
#[case::styled(
Text::styled("Hello, world!", Color::Yellow),
r#"Text::from(Line::from("Hello, world!")).yellow()"#
)]
#[case::complex_styled(
Text::from("Hello, world!").yellow().on_blue().bold().italic().not_dim().not_hidden(),
r#"Text::from(Line::from("Hello, world!")).yellow().on_blue().bold().italic().not_dim().not_hidden()"#
)]
#[case::alignment(
Text::from("Hello, world!").centered(),
r#"Text::from(Line::from("Hello, world!")).centered()"#
)]
#[case::styled_alignment(
Text::styled("Hello, world!", Color::Yellow).centered(),
r#"Text::from(Line::from("Hello, world!")).yellow().centered()"#
)]
#[case::multiple_lines(
Text::from(vec![
Line::from("Hello, world!"),
Line::from("How are you?")
]),
r#"Text::from_iter([Line::from("Hello, world!"), Line::from("How are you?")])"#
)]
fn debug(#[case] text: Text, #[case] expected: &str) {
assert_eq!(format!("{text:?}"), expected);
}
#[test]
#[ignore = "This is just showing the debug output of the assertions"]
fn no_style() {
let text = Text::from("single unstyled line");
assert_eq!(text, Text::default());
}
#[test]
#[ignore = "This is just showing the debug output of the assertions"]
fn text_style() {
let text = Text::from("single styled line")
.red()
.on_black()
.bold()
.not_italic();
assert_eq!(text, Text::default());
}
#[test]
#[ignore = "This is just showing the debug output of the assertions"]
fn line_style() {
let text = Text::from(vec![
Line::from("first line").red().alignment(Alignment::Right),
Line::from("second line").on_black(),
]);
assert_eq!(text, Text::default());
}
#[test]
#[ignore = "This is just showing the debug output of the assertions"]
fn span_style() {
let text = Text::from(Line::from(vec![
Span::from("first span").red(),
Span::from("second span").on_black(),
]));
assert_eq!(text, Text::default());
}
#[test]
fn debug_alternate() {
let text = Text::from_iter([
Line::from("Hello, world!"),
Line::from("How are you?").bold().left_aligned(),
Line::from_iter([
Span::from("I'm "),
Span::from("doing ").italic(),
Span::from("great!").bold(),
]),
])
.on_blue()
.italic()
.centered();
assert_eq!(
format!("{text:#?}"),
indoc::indoc! {r#"
Text::from_iter([
Line::from("Hello, world!"),
Line::from("How are you?").bold().left_aligned(),
Line::from_iter([
Span::from("I'm "),
Span::from("doing ").italic(),
Span::from("great!").bold(),
]),
]).on_blue().italic().centered()"#}
);
}
}

View File

@@ -31,6 +31,7 @@ mod chart;
mod clear;
mod gauge;
mod list;
mod logo;
mod paragraph;
mod reflow;
mod scrollbar;
@@ -46,6 +47,7 @@ pub use self::{
clear::Clear,
gauge::{Gauge, LineGauge},
list::{List, ListDirection, ListItem, ListState},
logo::{RatatuiLogo, Size as RatatuiLogoSize},
paragraph::{Paragraph, Wrap},
scrollbar::{ScrollDirection, Scrollbar, ScrollbarOrientation, ScrollbarState},
sparkline::{RenderDirection, Sparkline},
@@ -83,7 +85,11 @@ use crate::{buffer::Buffer, layout::Rect, style::Style};
/// # Examples
///
/// ```rust,no_run
/// use ratatui::{backend::TestBackend, prelude::*, widgets::*};
/// use ratatui::{
/// backend::TestBackend,
/// widgets::{Clear, Widget},
/// Terminal,
/// };
/// # let backend = TestBackend::new(5, 5);
/// # let mut terminal = Terminal::new(backend).unwrap();
///
@@ -95,7 +101,7 @@ use crate::{buffer::Buffer, layout::Rect, style::Style};
/// It's common to render widgets inside other widgets:
///
/// ```rust
/// use ratatui::{prelude::*, widgets::*};
/// use ratatui::{buffer::Buffer, layout::Rect, text::Line, widgets::Widget};
///
/// struct MyWidget;
///
@@ -133,7 +139,11 @@ pub trait Widget {
/// ```rust,no_run
/// use std::io;
///
/// use ratatui::{backend::TestBackend, prelude::*, widgets::*};
/// use ratatui::{
/// backend::TestBackend,
/// widgets::{List, ListItem, ListState, StatefulWidget, Widget},
/// Terminal,
/// };
///
/// // Let's say we have some events to display.
/// struct Events {
@@ -257,7 +267,12 @@ pub trait StatefulWidget {
///
/// ```rust
/// # #[cfg(feature = "unstable-widget-ref")] {
/// use ratatui::{prelude::*, widgets::*};
/// use ratatui::{
/// buffer::Buffer,
/// layout::Rect,
/// text::Line,
/// widgets::{Widget, WidgetRef},
/// };
///
/// struct Greeting;
///
@@ -332,7 +347,12 @@ impl<W: WidgetRef> Widget for &W {
///
/// ```rust
/// # #[cfg(feature = "unstable-widget-ref")] {
/// use ratatui::{prelude::*, widgets::*};
/// use ratatui::{
/// buffer::Buffer,
/// layout::Rect,
/// text::Line,
/// widgets::{Widget, WidgetRef},
/// };
///
/// struct Parent {
/// child: Option<Child>,
@@ -381,7 +401,13 @@ impl<W: WidgetRef> WidgetRef for Option<W> {
///
/// ```rust
/// # #[cfg(feature = "unstable-widget-ref")] {
/// use ratatui::{prelude::*, widgets::*};
/// use ratatui::{
/// buffer::Buffer,
/// layout::Rect,
/// style::Stylize,
/// text::Line,
/// widgets::{StatefulWidget, StatefulWidgetRef, Widget},
/// };
///
/// struct PersonalGreeting;
///

View File

@@ -1,4 +1,11 @@
use crate::{prelude::*, style::Styled, widgets::Block};
use crate::{
buffer::Buffer,
layout::{Direction, Rect},
style::{Style, Styled},
symbols::{self},
text::Line,
widgets::{block::BlockExt, Block, Widget, WidgetRef},
};
mod bar;
mod bar_group;
@@ -42,7 +49,10 @@ pub use bar_group::BarGroup;
/// The first group is added by an array slice (`&[(&str, u64)]`).
/// The second group is added by a [`BarGroup`] instance.
/// ```
/// use ratatui::{prelude::*, widgets::*};
/// use ratatui::{
/// style::{Style, Stylize},
/// widgets::{Bar, BarChart, BarGroup, Block},
/// };
///
/// BarChart::default()
/// .block(Block::bordered().title("BarChart"))
@@ -113,7 +123,8 @@ impl<'a> BarChart<'a> {
/// The first group is added by an array slice (`&[(&str, u64)]`).
/// The second group is added by a [`BarGroup`] instance.
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::{Bar, BarChart, BarGroup};
///
/// BarChart::default()
/// .data(&[("B0", 0), ("B1", 2), ("B2", 4), ("B3", 3)])
/// .data(BarGroup::default().bars(&[Bar::default().value(10), Bar::default().value(20)]));
@@ -143,7 +154,7 @@ impl<'a> BarChart<'a> {
/// This example shows the default behavior when `max` is not set.
/// The maximum value in the dataset is taken (here, `100`).
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::BarChart;
/// BarChart::default().data(&[("foo", 1), ("bar", 2), ("baz", 100)]);
/// // Renders
/// // █
@@ -154,7 +165,8 @@ impl<'a> BarChart<'a> {
/// This example shows a custom max value.
/// The maximum height being `2`, `bar` & `baz` render as the max.
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::BarChart;
///
/// BarChart::default()
/// .data(&[("foo", 1), ("bar", 2), ("baz", 100)])
/// .max(2);
@@ -176,6 +188,8 @@ impl<'a> BarChart<'a> {
///
/// It is also possible to set individually the style of each [`Bar`].
/// In this case the default style will be patched by the individual style
///
/// [`Color`]: crate::style::Color
#[must_use = "method moves the value of self and returns the modified value"]
pub fn bar_style<S: Into<Style>>(mut self, style: S) -> Self {
self.bar_style = style.into();
@@ -204,7 +218,8 @@ impl<'a> BarChart<'a> {
///
/// This shows two bars with a gap of `3`. Notice the labels will always stay under the bar.
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::BarChart;
///
/// BarChart::default()
/// .data(&[("foo", 1), ("bar", 2)])
/// .bar_gap(3);
@@ -239,6 +254,8 @@ impl<'a> BarChart<'a> {
/// # See also
///
/// [`Bar::value_style`] to set the value style individually.
///
/// [`Color`]: crate::style::Color
#[must_use = "method moves the value of self and returns the modified value"]
pub fn value_style<S: Into<Style>>(mut self, style: S) -> Self {
self.value_style = style.into();
@@ -256,6 +273,8 @@ impl<'a> BarChart<'a> {
/// # See also
///
/// [`Bar::label`] to set the label style individually.
///
/// [`Color`]: crate::style::Color
#[must_use = "method moves the value of self and returns the modified value"]
pub fn label_style<S: Into<Style>>(mut self, style: S) -> Self {
self.label_style = style.into();
@@ -275,6 +294,8 @@ impl<'a> BarChart<'a> {
/// your own type that implements [`Into<Style>`]).
///
/// The style will be applied to everything that isn't styled (borders, bars, labels, ...).
///
/// [`Color`]: crate::style::Color
#[must_use = "method moves the value of self and returns the modified value"]
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = style.into();
@@ -615,7 +636,12 @@ mod tests {
use itertools::iproduct;
use super::*;
use crate::widgets::BorderType;
use crate::{
layout::Alignment,
style::{Color, Modifier, Stylize},
text::Span,
widgets::BorderType,
};
#[test]
fn default() {

View File

@@ -1,7 +1,6 @@
use unicode_width::UnicodeWidthStr;
use crate::prelude::*;
use crate::{buffer::Buffer, layout::Rect, style::Style, text::Line, widgets::Widget};
/// A bar to be shown by the [`BarChart`](crate::widgets::BarChart) widget.
///
/// Here is an explanation of a `Bar`'s components.
@@ -17,13 +16,16 @@ use crate::prelude::*;
/// The following example creates a bar with the label "Bar 1", a value "10",
/// red background and a white value foreground.
/// ```
/// use ratatui::{prelude::*, widgets::*};
/// use ratatui::{
/// style::{Style, Stylize},
/// widgets::Bar,
/// };
///
/// Bar::default()
/// .label("Bar 1".into())
/// .value(10)
/// .style(Style::default().fg(Color::Red))
/// .value_style(Style::default().bg(Color::Red).fg(Color::White))
/// .style(Style::new().red())
/// .value_style(Style::new().red().on_white())
/// .text_value("10°C".to_string());
/// ```
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
@@ -74,6 +76,8 @@ impl<'a> Bar<'a> {
/// your own type that implements [`Into<Style>`]).
///
/// This will apply to every non-styled element. It can be seen and used as a default value.
///
/// [`Color`]: crate::style::Color
#[must_use = "method moves the value of self and returns the modified value"]
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = style.into();
@@ -88,6 +92,8 @@ impl<'a> Bar<'a> {
/// # See also
///
/// [`Bar::value`] to set the value.
///
/// [`Color`]: crate::style::Color
#[must_use = "method moves the value of self and returns the modified value"]
pub fn value_style<S: Into<Style>>(mut self, style: S) -> Self {
self.value_style = style.into();

View File

@@ -1,12 +1,17 @@
use super::Bar;
use crate::prelude::*;
use crate::{
buffer::Buffer,
layout::{Alignment, Rect},
style::Style,
text::Line,
widgets::{barchart::Bar, Widget},
};
/// A group of bars to be shown by the Barchart.
///
/// # Examples
///
/// ```
/// use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::{Bar, BarGroup};
///
/// BarGroup::default()
/// .label("Group 1".into())

View File

@@ -8,7 +8,14 @@
use itertools::Itertools;
use strum::{Display, EnumString};
use crate::{prelude::*, style::Styled, symbols::border, widgets::Borders};
use crate::{
buffer::Buffer,
layout::{Alignment, Rect},
style::{Style, Styled},
symbols::border,
text::Line,
widgets::{Borders, Widget, WidgetRef},
};
mod padding;
pub mod title;
@@ -67,7 +74,10 @@ pub use title::{Position, Title};
/// # Examples
///
/// ```
/// use ratatui::{prelude::*, widgets::*};
/// use ratatui::{
/// style::{Color, Style},
/// widgets::{Block, BorderType, Borders},
/// };
///
/// Block::new()
/// .border_type(BorderType::Rounded)
@@ -79,12 +89,9 @@ pub use title::{Position, Title};
///
/// You may also use multiple titles like in the following:
/// ```
/// use ratatui::{
/// prelude::*,
/// widgets::{
/// block::{Position, Title},
/// Block,
/// },
/// use ratatui::widgets::{
/// block::{Position, Title},
/// Block,
/// };
///
/// Block::new()
@@ -94,10 +101,7 @@ pub use title::{Position, Title};
///
/// You can also pass it as parameters of another widget so that the block surrounds them:
/// ```
/// use ratatui::{
/// prelude::*,
/// widgets::{Block, Borders, List},
/// };
/// use ratatui::widgets::{Block, Borders, List};
///
/// let surrounding_block = Block::default()
/// .borders(Borders::ALL)
@@ -220,7 +224,8 @@ impl<'a> Block<'a> {
/// Create a new block with [all borders](Borders::ALL) shown
///
/// ```
/// # use ratatui::widgets::{Block, Borders};
/// use ratatui::widgets::{Block, Borders};
///
/// assert_eq!(Block::bordered(), Block::new().borders(Borders::ALL));
/// ```
pub const fn bordered() -> Self {
@@ -268,8 +273,8 @@ impl<'a> Block<'a> {
/// - Two titles with the same alignment (notice the left titles are separated)
/// ```
/// use ratatui::{
/// prelude::*,
/// widgets::{block::*, *},
/// text::Line,
/// widgets::{Block, Borders},
/// };
///
/// Block::new()
@@ -320,7 +325,8 @@ impl<'a> Block<'a> {
/// # Example
///
/// ```
/// # use ratatui::{ prelude::*, widgets::* };
/// use ratatui::{ widgets::Block, text::Line };
///
/// Block::bordered()
/// .title_top("Left1") // By default in the top left corner
/// .title_top(Line::from("Left2").left_aligned())
@@ -348,7 +354,8 @@ impl<'a> Block<'a> {
/// # Example
///
/// ```
/// # use ratatui::{ prelude::*, widgets::* };
/// use ratatui::{ widgets::Block, text::Line };
///
/// Block::bordered()
/// .title_bottom("Left1") // By default in the top left corner
/// .title_bottom(Line::from("Left2").left_aligned())
@@ -377,6 +384,8 @@ impl<'a> Block<'a> {
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// [`Color`]: crate::style::Color
#[must_use = "method moves the value of self and returns the modified value"]
pub fn title_style<S: Into<Style>>(mut self, style: S) -> Self {
self.titles_style = style.into();
@@ -392,10 +401,7 @@ impl<'a> Block<'a> {
/// This example aligns all titles in the center except the "right" title which explicitly sets
/// [`Alignment::Right`].
/// ```
/// use ratatui::{
/// prelude::*,
/// widgets::{block::*, *},
/// };
/// use ratatui::{layout::Alignment, text::Line, widgets::Block};
///
/// Block::new()
/// .title_alignment(Alignment::Center)
@@ -419,13 +425,7 @@ impl<'a> Block<'a> {
/// This example positions all titles on the bottom except the "top" title which explicitly sets
/// [`Position::Top`].
/// ```
/// use ratatui::{
/// prelude::*,
/// widgets::{
/// block::{Position, Title},
/// Block,
/// },
/// };
/// use ratatui::widgets::{block::Position, Block};
///
/// Block::new()
/// .title_position(Position::Bottom)
@@ -454,9 +454,14 @@ impl<'a> Block<'a> {
///
/// This example shows a `Block` with blue borders.
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::{
/// style::{Style, Stylize},
/// widgets::Block,
/// };
/// Block::bordered().border_style(Style::new().blue());
/// ```
///
/// [`Color`]: crate::style::Color
#[must_use = "method moves the value of self and returns the modified value"]
pub fn border_style<S: Into<Style>>(mut self, style: S) -> Self {
self.border_style = style.into();
@@ -479,7 +484,11 @@ impl<'a> Block<'a> {
/// # Example
///
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::{
/// style::{Color, Style, Stylize},
/// widgets::{Block, Paragraph},
/// };
///
/// let block = Block::new().style(Style::new().red().on_black());
///
/// // For border and title you can additionally apply styles on top of the block level style.
@@ -496,6 +505,7 @@ impl<'a> Block<'a> {
/// ```
///
/// [`Paragraph`]: crate::widgets::Paragraph
/// [`Color`]: crate::style::Color
#[must_use = "method moves the value of self and returns the modified value"]
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = style.into();
@@ -510,7 +520,7 @@ impl<'a> Block<'a> {
///
/// Display left and right borders.
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::{Block, Borders};
/// Block::new().borders(Borders::LEFT | Borders::RIGHT);
/// ```
///
@@ -531,7 +541,7 @@ impl<'a> Block<'a> {
/// # Examples
///
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::{Block, BorderType};
/// Block::bordered()
/// .border_type(BorderType::Rounded)
/// .title("Block");
@@ -553,7 +563,8 @@ impl<'a> Block<'a> {
/// # Examples
///
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::{widgets::Block, symbols};
///
/// Block::bordered().border_set(symbols::border::DOUBLE).title("Block");
/// // Renders
/// // ╔Block╗
@@ -573,7 +584,8 @@ impl<'a> Block<'a> {
///
/// This renders a `Block` with no padding (the default).
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::{Block, Padding};
///
/// Block::bordered().padding(Padding::ZERO);
/// // Renders
/// // ┌───────┐
@@ -584,7 +596,8 @@ impl<'a> Block<'a> {
/// This example shows a `Block` with padding left and right ([`Padding::horizontal`]).
/// Notice the two spaces before and after the content.
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::{Block, Padding};
///
/// Block::bordered().padding(Padding::horizontal(2));
/// // Renders
/// // ┌───────────┐
@@ -603,7 +616,8 @@ impl<'a> Block<'a> {
///
/// Draw a block nested within another block
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::{widgets::Block, Frame};
///
/// # fn render_nested_block(frame: &mut Frame) {
/// let outer_block = Block::bordered().title("Outer");
/// let inner_block = Block::bordered().title("Inner");
@@ -990,6 +1004,7 @@ mod tests {
use strum::ParseError;
use super::*;
use crate::style::{Color, Modifier, Stylize};
#[test]
fn create_with_all_borders() {

View File

@@ -10,7 +10,7 @@
/// # Example
///
/// ```
/// use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::Padding;
///
/// Padding::uniform(1);
/// Padding::horizontal(2);

View File

@@ -31,14 +31,14 @@ use crate::{layout::Alignment, text::Line};
///
/// Blue title on a white background (via [`Stylize`](crate::style::Stylize) trait).
/// ```
/// use ratatui::{prelude::*, widgets::block::*};
/// use ratatui::{style::Stylize, widgets::block::Title};
///
/// Title::from("Title".blue().on_white());
/// ```
///
/// Title with multiple styles (see [`Line`] and [`Stylize`](crate::style::Stylize)).
/// ```
/// use ratatui::{prelude::*, widgets::block::*};
/// use ratatui::{style::Stylize, text::Line, widgets::block::Title};
///
/// Title::from(Line::from(vec!["Q".white().underlined(), "uit".gray()]));
/// ```
@@ -46,7 +46,7 @@ use crate::{layout::Alignment, text::Line};
/// Complete example
/// ```
/// use ratatui::{
/// prelude::*,
/// layout::Alignment,
/// widgets::{
/// block::{Position, Title},
/// Block,
@@ -84,7 +84,10 @@ pub struct Title<'a> {
/// # Example
///
/// ```
/// use ratatui::widgets::{block::*, *};
/// use ratatui::widgets::{
/// block::{Position, Title},
/// Block,
/// };
///
/// Block::new().title(Title::from("title").position(Position::Bottom));
/// ```

View File

@@ -60,7 +60,11 @@ impl fmt::Debug for Borders {
/// ## Examples
///
/// ```
/// # use ratatui::{border, prelude::*, widgets::*};
/// use ratatui::{
/// border,
/// widgets::{Block, Borders},
/// };
///
/// Block::new()
/// .title("Construct Borders and use them in place")
/// .borders(border!(TOP, BOTTOM));
@@ -69,7 +73,7 @@ impl fmt::Debug for Borders {
/// `border!` can be called with any number of individual sides:
///
/// ```
/// # use ratatui::{border, prelude::*, widgets::*};
/// use ratatui::{border, widgets::Borders};
/// let right_open = border!(TOP, LEFT, BOTTOM);
/// assert_eq!(right_open, Borders::TOP | Borders::LEFT | Borders::BOTTOM);
/// ```
@@ -77,7 +81,8 @@ impl fmt::Debug for Borders {
/// Single borders work but using `Borders::` directly would be simpler.
///
/// ```
/// # use ratatui::{border, prelude::*, widgets::*};
/// use ratatui::{border, widgets::Borders};
///
/// assert_eq!(border!(TOP), Borders::TOP);
/// assert_eq!(border!(ALL), Borders::ALL);
/// assert_eq!(border!(), Borders::NONE);

View File

@@ -12,7 +12,13 @@ use std::collections::HashMap;
use time::{Date, Duration, OffsetDateTime};
use crate::{prelude::*, widgets::Block};
use crate::{
buffer::Buffer,
layout::{Alignment, Constraint, Layout, Rect},
style::Style,
text::{Line, Span},
widgets::{block::BlockExt, Block, Widget, WidgetRef},
};
/// Display a month calendar for the month containing `display_date`
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
@@ -46,6 +52,8 @@ impl<'a, DS: DateStyler> Monthly<'a, DS> {
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// [`Color`]: crate::style::Color
#[must_use = "method moves the value of self and returns the modified value"]
pub fn show_surrounding<S: Into<Style>>(mut self, style: S) -> Self {
self.show_surrounding = Some(style.into());
@@ -56,6 +64,8 @@ impl<'a, DS: DateStyler> Monthly<'a, DS> {
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// [`Color`]: crate::style::Color
#[must_use = "method moves the value of self and returns the modified value"]
pub fn show_weekdays_header<S: Into<Style>>(mut self, style: S) -> Self {
self.show_weekday = Some(style.into());
@@ -66,6 +76,8 @@ impl<'a, DS: DateStyler> Monthly<'a, DS> {
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// [`Color`]: crate::style::Color
#[must_use = "method moves the value of self and returns the modified value"]
pub fn show_month_header<S: Into<Style>>(mut self, style: S) -> Self {
self.show_month = Some(style.into());
@@ -76,6 +88,8 @@ impl<'a, DS: DateStyler> Monthly<'a, DS> {
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// [`Color`]: crate::style::Color
#[must_use = "method moves the value of self and returns the modified value"]
pub fn default_style<S: Into<Style>>(mut self, style: S) -> Self {
self.default_style = style.into();
@@ -201,6 +215,8 @@ impl CalendarEventStore {
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// [`Color`]: crate::style::Color
pub fn today<S: Into<Style>>(style: S) -> Self {
let mut res = Self::default();
res.add(
@@ -216,6 +232,8 @@ impl CalendarEventStore {
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// [`Color`]: crate::style::Color
pub fn add<S: Into<Style>>(&mut self, date: Date, style: S) {
// to simplify style nonsense, last write wins
let _ = self.0.insert(date, style.into());
@@ -250,6 +268,7 @@ mod tests {
use time::Month;
use super::*;
use crate::style::Color;
#[test]
fn event_store() {

View File

@@ -30,7 +30,14 @@ pub use self::{
points::Points,
rectangle::Rectangle,
};
use crate::{prelude::*, symbols::Marker, text::Line as TextLine, widgets::Block};
use crate::{
buffer::Buffer,
layout::Rect,
style::{Color, Style},
symbols::{self, Marker},
text::Line as TextLine,
widgets::{block::BlockExt, Block, Widget, WidgetRef},
};
/// Something that can be drawn on a [`Canvas`].
///
@@ -356,7 +363,10 @@ impl<'a, 'b> Painter<'a, 'b> {
/// # Examples
///
/// ```
/// use ratatui::{prelude::*, widgets::canvas::*};
/// use ratatui::{
/// symbols,
/// widgets::canvas::{Context, Painter},
/// };
///
/// let mut ctx = Context::new(2, 2, [1.0, 2.0], [0.0, 2.0], symbols::Marker::Braille);
/// let mut painter = Painter::from(&mut ctx);
@@ -399,7 +409,11 @@ impl<'a, 'b> Painter<'a, 'b> {
/// # Example
///
/// ```
/// use ratatui::{prelude::*, widgets::canvas::*};
/// use ratatui::{
/// style::Color,
/// symbols,
/// widgets::canvas::{Context, Painter},
/// };
///
/// let mut ctx = Context::new(1, 1, [0.0, 2.0], [0.0, 2.0], symbols::Marker::Braille);
/// let mut painter = Painter::from(&mut ctx);
@@ -425,7 +439,7 @@ impl<'a, 'b> From<&'a mut Context<'b>> for Painter<'a, 'b> {
/// This is used by the [`Canvas`] widget to draw shapes on the grid. It can be useful to think of
/// this as similar to the [`Frame`] struct that is used to draw widgets on the terminal.
///
/// [`Frame`]: crate::prelude::Frame
/// [`Frame`]: crate::Frame
#[derive(Debug)]
pub struct Context<'a> {
x_bounds: [f64; 2],
@@ -449,7 +463,7 @@ impl<'a> Context<'a> {
/// example, if you want to draw a map of the world, you might want to use the following bounds:
///
/// ```
/// use ratatui::{prelude::*, widgets::canvas::*};
/// use ratatui::{symbols, widgets::canvas::Context};
///
/// let ctx = Context::new(
/// 100,
@@ -513,6 +527,8 @@ impl<'a> Context<'a> {
///
/// Note that the text is always printed on top of the canvas and is **not** affected by the
/// layers.
///
/// [`Text`]: crate::text::Text
pub fn print<T>(&mut self, x: f64, y: f64, line: T)
where
T: Into<TextLine<'a>>,
@@ -563,7 +579,10 @@ impl<'a> Context<'a> {
/// ```
/// use ratatui::{
/// style::Color,
/// widgets::{canvas::*, *},
/// widgets::{
/// canvas::{Canvas, Line, Map, MapResolution, Rectangle},
/// Block,
/// },
/// };
///
/// Canvas::default()
@@ -698,7 +717,7 @@ where
/// # Examples
///
/// ```
/// use ratatui::{prelude::*, widgets::canvas::*};
/// use ratatui::{symbols, widgets::canvas::Canvas};
///
/// Canvas::default()
/// .marker(symbols::Marker::Braille)

View File

@@ -65,7 +65,12 @@ mod tests {
use strum::ParseError;
use super::*;
use crate::{prelude::*, symbols::Marker, widgets::canvas::Canvas};
use crate::{
buffer::Buffer,
layout::Rect,
symbols::Marker,
widgets::{canvas::Canvas, Widget},
};
#[test]
fn map_resolution_to_string() {

View File

@@ -3,7 +3,7 @@ use crate::{
widgets::canvas::{Line, Painter, Shape},
};
/// A rectangle to draw on a [`Canvas`](super::Canvas)
/// A rectangle to draw on a [`Canvas`](crate::widgets::canvas::Canvas)
///
/// Sizes used here are **not** in terminal cell. This is much more similar to the
/// mathematic coordinate system.
@@ -66,7 +66,13 @@ impl Shape for Rectangle {
#[cfg(test)]
mod tests {
use super::*;
use crate::{prelude::*, symbols::Marker, widgets::canvas::Canvas};
use crate::{
buffer::Buffer,
layout::{Margin, Rect},
style::{Style, Stylize},
symbols::Marker,
widgets::{canvas::Canvas, Widget},
};
#[test]
fn draw_block_lines() {

View File

@@ -3,12 +3,15 @@ use std::{cmp::max, ops::Not};
use strum::{Display, EnumString};
use crate::{
layout::Flex,
prelude::*,
style::Styled,
buffer::Buffer,
layout::{Alignment, Constraint, Flex, Layout, Position, Rect},
style::{Color, Style, Styled},
symbols::{self},
text::Line,
widgets::{
block::BlockExt,
canvas::{Canvas, Line as CanvasLine, Points},
Block,
Block, Widget, WidgetRef,
},
};
@@ -25,7 +28,11 @@ use crate::{
/// # Example
///
/// ```rust
/// use ratatui::{prelude::*, widgets::*};
/// use ratatui::{
/// style::{Style, Stylize},
/// widgets::Axis,
/// };
///
/// let axis = Axis::default()
/// .title("X Axis")
/// .style(Style::default().gray())
@@ -94,7 +101,8 @@ impl<'a> Axis<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::{style::Stylize, widgets::Axis};
///
/// let axis = Axis::default()
/// .bounds([0.0, 50.0])
/// .labels(["0".bold(), "25".into(), "50".bold()]);
@@ -122,7 +130,8 @@ impl<'a> Axis<'a> {
/// like so
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::{style::Stylize, widgets::Axis};
///
/// let axis = Axis::default().red();
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
@@ -297,7 +306,11 @@ impl LegendPosition {
/// This example draws a red line between two points.
///
/// ```rust
/// use ratatui::{prelude::*, symbols::Marker, widgets::*};
/// use ratatui::{
/// style::Stylize,
/// symbols::Marker,
/// widgets::{Dataset, GraphType},
/// };
///
/// let dataset = Dataset::default()
/// .name("dataset 1")
@@ -401,7 +414,8 @@ impl<'a> Dataset<'a> {
/// like so
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::{style::Stylize, widgets::Dataset};
///
/// let dataset = Dataset::default().red();
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
@@ -452,7 +466,11 @@ struct ChartLayout {
/// # Examples
///
/// ```
/// use ratatui::{prelude::*, widgets::*};
/// use ratatui::{
/// style::{Style, Stylize},
/// symbols,
/// widgets::{Axis, Block, Chart, Dataset, GraphType},
/// };
///
/// // Create the datasets to fill the chart with
/// let datasets = vec![
@@ -521,17 +539,19 @@ impl<'a> Chart<'a> {
/// This creates a simple chart with one [`Dataset`]
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # let data_points = vec![];
/// use ratatui::widgets::{Chart, Dataset};
///
/// let data_points = vec![];
/// let chart = Chart::new(vec![Dataset::default().data(&data_points)]);
/// ```
///
/// This creates a chart with multiple [`Dataset`]s
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # let data_points = vec![];
/// # let data_points2 = vec![];
/// use ratatui::widgets::{Chart, Dataset};
///
/// let data_points = vec![];
/// let data_points2 = vec![];
/// let chart = Chart::new(vec![
/// Dataset::default().data(&data_points),
/// Dataset::default().data(&data_points2),
@@ -581,7 +601,8 @@ impl<'a> Chart<'a> {
/// # Example
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::{Axis, Chart};
///
/// let chart = Chart::new(vec![]).x_axis(
/// Axis::default()
/// .title("X Axis")
@@ -604,7 +625,8 @@ impl<'a> Chart<'a> {
/// # Example
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::{Axis, Chart};
///
/// let chart = Chart::new(vec![]).y_axis(
/// Axis::default()
/// .title("Y Axis")
@@ -635,7 +657,8 @@ impl<'a> Chart<'a> {
/// its height is greater than 25% of the total widget height.
///
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::{layout::Constraint, widgets::Chart};
///
/// let constraints = (Constraint::Ratio(1, 3), Constraint::Ratio(1, 4));
/// let chart = Chart::new(vec![]).hidden_legend_constraints(constraints);
/// ```
@@ -644,7 +667,8 @@ impl<'a> Chart<'a> {
/// first one is always true.
///
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::{layout::Constraint, widgets::Chart};
///
/// let constraints = (Constraint::Min(0), Constraint::Ratio(1, 4));
/// let chart = Chart::new(vec![]).hidden_legend_constraints(constraints);
/// ```
@@ -653,7 +677,8 @@ impl<'a> Chart<'a> {
/// [`Chart::legend_position`].
///
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::{layout::Constraint, widgets::Chart};
///
/// let constraints = (Constraint::Length(0), Constraint::Ratio(1, 4));
/// let chart = Chart::new(vec![]).hidden_legend_constraints(constraints);
/// ```
@@ -685,14 +710,16 @@ impl<'a> Chart<'a> {
/// Show the legend on the top left corner.
///
/// ```
/// # use ratatui::widgets::{Chart, LegendPosition};
/// use ratatui::widgets::{Chart, LegendPosition};
///
/// let chart: Chart = Chart::new(vec![]).legend_position(Some(LegendPosition::TopLeft));
/// ```
///
/// Hide the legend altogether
///
/// ```
/// # use ratatui::widgets::{Chart, LegendPosition};
/// use ratatui::widgets::{Chart, LegendPosition};
///
/// let chart = Chart::new(vec![]).legend_position(None);
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
@@ -1134,6 +1161,7 @@ mod tests {
use strum::ParseError;
use super::*;
use crate::style::{Modifier, Stylize};
struct LegendTestCase {
chart_area: Rect,

View File

@@ -1,4 +1,8 @@
use crate::prelude::*;
use crate::{
buffer::Buffer,
layout::Rect,
widgets::{Widget, WidgetRef},
};
/// A widget to clear/reset a certain area to allow overdrawing (e.g. for popups).
///
@@ -8,7 +12,11 @@ use crate::prelude::*;
/// # Examples
///
/// ```
/// use ratatui::{prelude::*, widgets::*};
/// use ratatui::{
/// layout::Rect,
/// widgets::{Block, Clear},
/// Frame,
/// };
///
/// fn draw_on_clear(f: &mut Frame, area: Rect) {
/// let block = Block::bordered().title("Block");
@@ -43,7 +51,7 @@ impl WidgetRef for Clear {
#[cfg(test)]
mod tests {
use super::*;
use crate::{buffer::Buffer, layout::Rect, widgets::Widget};
#[test]
fn render() {
let mut buffer = Buffer::with_lines(["xxxxxxxxxxxxxxx"; 7]);

View File

@@ -1,4 +1,11 @@
use crate::{prelude::*, style::Styled, widgets::Block};
use crate::{
buffer::Buffer,
layout::Rect,
style::{Color, Style, Styled},
symbols::{self},
text::{Line, Span},
widgets::{block::BlockExt, Block, Widget, WidgetRef},
};
/// A widget to display a progress bar.
///
@@ -16,16 +23,14 @@ use crate::{prelude::*, style::Styled, widgets::Block};
/// # Example
///
/// ```
/// use ratatui::{prelude::*, widgets::*};
/// use ratatui::{
/// style::{Style, Stylize},
/// widgets::{Block, Gauge},
/// };
///
/// Gauge::default()
/// .block(Block::bordered().title("Progress"))
/// .gauge_style(
/// Style::default()
/// .fg(Color::White)
/// .bg(Color::Black)
/// .add_modifier(Modifier::ITALIC),
/// )
/// .gauge_style(Style::new().white().on_black().italic())
/// .percent(20);
/// ```
///
@@ -242,16 +247,15 @@ fn get_unicode_block<'a>(frac: f64) -> &'a str {
/// # Examples:
///
/// ```
/// use ratatui::{prelude::*, widgets::*};
/// use ratatui::{
/// style::{Style, Stylize},
/// symbols,
/// widgets::{Block, LineGauge},
/// };
///
/// LineGauge::default()
/// .block(Block::bordered().title("Progress"))
/// .filled_style(
/// Style::default()
/// .fg(Color::White)
/// .bg(Color::Black)
/// .add_modifier(Modifier::BOLD),
/// )
/// .filled_style(Style::new().white().on_black().bold())
/// .line_set(symbols::line::THICK)
/// .ratio(0.4);
/// ```
@@ -443,7 +447,10 @@ impl<'a> Styled for LineGauge<'a> {
#[cfg(test)]
mod tests {
use super::*;
use crate::{
style::{Color, Modifier, Style, Stylize},
symbols,
};
#[test]
#[should_panic = "Percentage should be between 0 and 100 inclusively"]
fn gauge_invalid_percentage() {

View File

@@ -1,4 +1,4 @@
use crate::prelude::*;
use crate::{style::Style, text::Text};
/// A single item in a [`List`]
///
@@ -19,14 +19,15 @@ use crate::prelude::*;
/// You can create [`ListItem`]s from simple `&str`
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::ListItem;
/// let item = ListItem::new("Item 1");
/// ```
///
/// Anything that can be converted to [`Text`] can be a [`ListItem`].
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::{text::Line, widgets::ListItem};
///
/// let item1: ListItem = "Item 1".into();
/// let item2: ListItem = Line::raw("Item 2").into();
/// ```
@@ -34,7 +35,8 @@ use crate::prelude::*;
/// A [`ListItem`] styled with [`Stylize`]
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::{style::Stylize, widgets::ListItem};
///
/// let item = ListItem::new("Item 1").red().on_white();
/// ```
///
@@ -42,7 +44,12 @@ use crate::prelude::*;
/// [`Text`]
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::{
/// style::Stylize,
/// text::{Span, Text},
/// widgets::ListItem,
/// };
///
/// let mut text = Text::default();
/// text.extend(["Item".blue(), Span::raw(" "), "1".bold().red()]);
/// let item = ListItem::new(text);
@@ -51,12 +58,15 @@ use crate::prelude::*;
/// A right-aligned `ListItem`
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// ListItem::new(Text::from("foo").alignment(Alignment::Right));
/// use ratatui::{text::Text, widgets::ListItem};
///
/// ListItem::new(Text::from("foo").right_aligned());
/// ```
///
/// [`List`]: crate::widgets::List
/// [`Stylize`]: crate::style::Stylize
/// [`Line`]: crate::text::Line
/// [`Line::alignment`]: crate::text::Line::alignment
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct ListItem<'a> {
pub(crate) content: Text<'a>,
@@ -73,22 +83,25 @@ impl<'a> ListItem<'a> {
/// You can create [`ListItem`]s from simple `&str`
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::ListItem;
///
/// let item = ListItem::new("Item 1");
/// ```
///
/// Anything that can be converted to [`Text`] can be a [`ListItem`].
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::{text::Line, widgets::ListItem};
///
/// let item1: ListItem = "Item 1".into();
/// let item2: ListItem = Line::raw("Item 2").into();
/// ```
///
/// You can also create multilines item
/// You can also create multiline items
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::ListItem;
///
/// let item = ListItem::new("Multi-line\nitem");
/// ```
///
@@ -118,7 +131,11 @@ impl<'a> ListItem<'a> {
/// # Example
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::{
/// style::{Style, Stylize},
/// widgets::ListItem,
/// };
///
/// let item = ListItem::new("Item 1").style(Style::new().red().italic());
/// ```
///
@@ -127,12 +144,14 @@ impl<'a> ListItem<'a> {
/// concisely.
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::{style::Stylize, widgets::ListItem};
///
/// let item = ListItem::new("Item 1").red().italic();
/// ```
///
/// [`Styled`]: crate::style::Styled
/// [`ListState`]: crate::widgets::list::ListState
/// [`Color`]: crate::style::Color
#[must_use = "method moves the value of self and returns the modified value"]
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = style.into();
@@ -146,7 +165,8 @@ impl<'a> ListItem<'a> {
/// One line item
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::ListItem;
///
/// let item = ListItem::new("Item 1");
/// assert_eq!(item.height(), 1);
/// ```
@@ -154,7 +174,8 @@ impl<'a> ListItem<'a> {
/// Two lines item (note the `\n`)
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::ListItem;
///
/// let item = ListItem::new("Multi-line\nitem");
/// assert_eq!(item.height(), 2);
/// ```
@@ -167,13 +188,15 @@ impl<'a> ListItem<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::ListItem;
///
/// let item = ListItem::new("12345");
/// assert_eq!(item.width(), 5);
/// ```
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::ListItem;
///
/// let item = ListItem::new("12345\n1234567");
/// assert_eq!(item.width(), 7);
/// ```
@@ -198,6 +221,10 @@ mod tests {
use pretty_assertions::assert_eq;
use super::*;
use crate::{
style::{Color, Modifier, Stylize},
text::{Line, Span},
};
#[test]
fn new_from_str() {

View File

@@ -1,10 +1,8 @@
use strum::{Display, EnumString};
use super::ListItem;
use crate::{
prelude::*,
style::Styled,
widgets::{Block, HighlightSpacing},
style::{Style, Styled},
widgets::{Block, HighlightSpacing, ListItem},
};
/// A widget to display several items among which one can be selected (optional)
@@ -41,14 +39,20 @@ use crate::{
/// # Examples
///
/// ```
/// use ratatui::{prelude::*, widgets::*};
/// use ratatui::{
/// layout::Rect,
/// style::{Style, Stylize},
/// widgets::{Block, List, ListDirection, ListItem},
/// Frame,
/// };
///
/// # fn ui(frame: &mut Frame) {
/// # let area = Rect::default();
/// let items = ["Item 1", "Item 2", "Item 3"];
/// let list = List::new(items)
/// .block(Block::bordered().title("List"))
/// .style(Style::default().fg(Color::White))
/// .highlight_style(Style::default().add_modifier(Modifier::ITALIC))
/// .style(Style::new().white())
/// .highlight_style(Style::new().italic())
/// .highlight_symbol(">>")
/// .repeat_highlight_symbol(true)
/// .direction(ListDirection::BottomToTop);
@@ -60,7 +64,13 @@ use crate::{
/// # Stateful example
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::{
/// layout::Rect,
/// style::{Style, Stylize},
/// widgets::{Block, List, ListState},
/// Frame,
/// };
///
/// # fn ui(frame: &mut Frame) {
/// # let area = Rect::default();
/// // This should be stored outside of the function in your application state.
@@ -68,7 +78,7 @@ use crate::{
/// let items = ["Item 1", "Item 2", "Item 3"];
/// let list = List::new(items)
/// .block(Block::bordered().title("List"))
/// .highlight_style(Style::new().add_modifier(Modifier::REVERSED))
/// .highlight_style(Style::new().reversed())
/// .highlight_symbol(">>")
/// .repeat_highlight_symbol(true);
///
@@ -88,6 +98,9 @@ use crate::{
/// [`ListState`]: crate::widgets::list::ListState
/// [scroll]: crate::widgets::list::ListState::offset
/// [select]: crate::widgets::list::ListState::select
/// [`Text::alignment`]: crate::text::Text::alignment
/// [`StatefulWidget`]: crate::widgets::StatefulWidget
/// [`Widget`]: crate::widgets::Widget
#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)]
pub struct List<'a> {
/// An optional block to wrap the widget in
@@ -135,17 +148,23 @@ impl<'a> List<'a> {
/// From a slice of [`&str`]
///
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::List;
///
/// let list = List::new(["Item 1", "Item 2"]);
/// ```
///
/// From [`Text`]
///
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::{
/// style::{Style, Stylize},
/// text::Text,
/// widgets::List,
/// };
///
/// let list = List::new([
/// Text::styled("Item 1", Style::default().red()),
/// Text::styled("Item 2", Style::default().red()),
/// Text::styled("Item 1", Style::new().red()),
/// Text::styled("Item 2", Style::new().red()),
/// ]);
/// ```
///
@@ -153,10 +172,13 @@ impl<'a> List<'a> {
/// [`List::items`] fluent setter.
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::List;
///
/// let empty_list = List::default();
/// let filled_list = empty_list.items(["Item 1"]);
/// ```
///
/// [`Text`]: crate::text::Text
pub fn new<T>(items: T) -> Self
where
T: IntoIterator,
@@ -181,9 +203,12 @@ impl<'a> List<'a> {
/// # Example
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::List;
///
/// let list = List::default().items(["Item 1", "Item 2"]);
/// ```
///
/// [`Text`]: crate::text::Text
#[must_use = "method moves the value of self and returns the modified value"]
pub fn items<T>(mut self, items: T) -> Self
where
@@ -203,8 +228,9 @@ impl<'a> List<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # let items = ["Item 1"];
/// use ratatui::widgets::{Block, List};
///
/// let items = ["Item 1"];
/// let block = Block::bordered().title("List");
/// let list = List::new(items).block(block);
/// ```
@@ -227,8 +253,12 @@ impl<'a> List<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # let items = ["Item 1"];
/// use ratatui::{
/// style::{Style, Stylize},
/// widgets::List,
/// };
///
/// let items = ["Item 1"];
/// let list = List::new(items).style(Style::new().red().italic());
/// ```
///
@@ -238,10 +268,13 @@ impl<'a> List<'a> {
/// [`Stylize`]: crate::style::Stylize
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # let items = ["Item 1"];
/// use ratatui::{style::Stylize, widgets::List};
///
/// let items = ["Item 1"];
/// let list = List::new(items).red().italic();
/// ```
///
/// [`Color`]: crate::style::Color
#[must_use = "method moves the value of self and returns the modified value"]
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = style.into();
@@ -257,8 +290,9 @@ impl<'a> List<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # let items = ["Item 1", "Item 2"];
/// use ratatui::widgets::List;
///
/// let items = ["Item 1", "Item 2"];
/// let list = List::new(items).highlight_symbol(">>");
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
@@ -281,10 +315,16 @@ impl<'a> List<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # let items = ["Item 1", "Item 2"];
/// use ratatui::{
/// style::{Style, Stylize},
/// widgets::List,
/// };
///
/// let items = ["Item 1", "Item 2"];
/// let list = List::new(items).highlight_style(Style::new().red().italic());
/// ```
///
/// [`Color`]: crate::style::Color
#[must_use = "method moves the value of self and returns the modified value"]
pub fn highlight_style<S: Into<Style>>(mut self, style: S) -> Self {
self.highlight_style = style.into();
@@ -323,8 +363,9 @@ impl<'a> List<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # let items = ["Item 1"];
/// use ratatui::widgets::{HighlightSpacing, List};
///
/// let items = ["Item 1"];
/// let list = List::new(items).highlight_spacing(HighlightSpacing::Always);
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
@@ -345,8 +386,9 @@ impl<'a> List<'a> {
/// Bottom to top
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # let items = ["Item 1"];
/// use ratatui::widgets::{List, ListDirection};
///
/// let items = ["Item 1"];
/// let list = List::new(items).direction(ListDirection::BottomToTop);
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
@@ -364,8 +406,9 @@ impl<'a> List<'a> {
/// A padding value of 1 will keep 1 item above and 1 item bellow visible if possible
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # let items = ["Item 1"];
/// use ratatui::widgets::List;
///
/// let items = ["Item 1"];
/// let list = List::new(items).scroll_padding(1);
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
@@ -423,6 +466,7 @@ mod tests {
use pretty_assertions::assert_eq;
use super::*;
use crate::style::{Color, Modifier, Stylize};
#[test]
fn collect_list_from_iterator() {

View File

@@ -1,8 +1,12 @@
use unicode_width::UnicodeWidthStr;
use crate::{
prelude::{Buffer, Rect, StatefulWidget, StatefulWidgetRef, Widget, WidgetRef},
widgets::{block::BlockExt, List, ListDirection, ListState},
buffer::Buffer,
layout::Rect,
widgets::{
block::BlockExt, List, ListDirection, ListState, StatefulWidget, StatefulWidgetRef, Widget,
WidgetRef,
},
};
impl Widget for List<'_> {
@@ -273,8 +277,12 @@ mod tests {
use super::*;
use crate::{
prelude::*,
widgets::{Block, HighlightSpacing, ListItem},
backend,
layout::{Alignment, Rect},
style::{Color, Modifier, Style, Stylize},
text::Line,
widgets::{Block, HighlightSpacing, ListItem, StatefulWidget, Widget},
Terminal,
};
#[fixture]

View File

@@ -20,10 +20,15 @@
/// # Example
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::{
/// layout::Rect,
/// widgets::{List, ListState},
/// Frame,
/// };
///
/// # fn ui(frame: &mut Frame) {
/// # let area = Rect::default();
/// # let items = ["Item 1"];
/// let items = ["Item 1"];
/// let list = List::new(items);
///
/// // This should be stored outside of the function in your application state.
@@ -52,7 +57,8 @@ impl ListState {
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::ListState;
///
/// let state = ListState::default().with_offset(1);
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
@@ -68,7 +74,8 @@ impl ListState {
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::ListState;
///
/// let state = ListState::default().with_selected(Some(1));
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
@@ -82,7 +89,8 @@ impl ListState {
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::ListState;
///
/// let state = ListState::default();
/// assert_eq!(state.offset(), 0);
/// ```
@@ -95,7 +103,8 @@ impl ListState {
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::ListState;
///
/// let mut state = ListState::default();
/// *state.offset_mut() = 1;
/// ```
@@ -110,8 +119,9 @@ impl ListState {
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let state = TableState::default();
/// use ratatui::widgets::ListState;
///
/// let state = ListState::default();
/// assert_eq!(state.selected(), None);
/// ```
pub const fn selected(&self) -> Option<usize> {
@@ -125,7 +135,8 @@ impl ListState {
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::ListState;
///
/// let mut state = ListState::default();
/// *state.selected_mut() = Some(1);
/// ```
@@ -140,7 +151,8 @@ impl ListState {
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::ListState;
///
/// let mut state = ListState::default();
/// state.select(Some(1));
/// ```
@@ -159,7 +171,8 @@ impl ListState {
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::ListState;
///
/// let mut state = ListState::default();
/// state.select_next();
/// ```
@@ -176,7 +189,8 @@ impl ListState {
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::ListState;
///
/// let mut state = ListState::default();
/// state.select_previous();
/// ```
@@ -193,7 +207,8 @@ impl ListState {
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::ListState;
///
/// let mut state = ListState::default();
/// state.select_first();
/// ```
@@ -209,7 +224,8 @@ impl ListState {
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::ListState;
///
/// let mut state = ListState::default();
/// state.select_last();
/// ```
@@ -226,7 +242,8 @@ impl ListState {
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::ListState;
///
/// let mut state = ListState::default();
/// state.scroll_down_by(4);
/// ```
@@ -244,7 +261,8 @@ impl ListState {
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::ListState;
///
/// let mut state = ListState::default();
/// state.scroll_up_by(4);
/// ```

236
src/widgets/logo.rs Normal file
View File

@@ -0,0 +1,236 @@
use indoc::indoc;
use crate::{buffer::Buffer, layout::Rect, text::Text, widgets::Widget};
/// A widget that renders the Ratatui logo
///
/// The Ratatui logo takes up two lines of text and comes in two sizes: `Tiny` and `Small`. This may
/// be used in an application's help or about screen to show that it is powered by Ratatui.
///
/// # Examples
///
/// The [Ratatui-logo] example demonstrates how to use the `RatatuiLogo` widget. This can be run by
/// cloning the Ratatui repository and then running the following command with an optional size
/// argument:
///
/// ```shell
/// cargo run --example ratatui-logo [size]
/// ```
///
/// [Ratatui-logo]: https://github.com/ratatui/ratatui/blob/main/examples/ratatui-logo.rs
///
/// ## Tiny (default, 2x15 characters)
///
/// ```
/// use ratatui::widgets::RatatuiLogo;
///
/// # fn draw(frame: &mut ratatui::Frame) {
/// frame.render_widget(RatatuiLogo::tiny(), frame.area());
/// # }
/// ```
///
/// Renders:
///
/// ```text
/// ▛▚▗▀▖▜▘▞▚▝▛▐ ▌▌
/// ▛▚▐▀▌▐ ▛▜ ▌▝▄▘▌
/// ```
///
/// ## Small (2x27 characters)
///
/// ```
/// use ratatui::widgets::RatatuiLogo;
///
/// # fn draw(frame: &mut ratatui::Frame) {
/// frame.render_widget(RatatuiLogo::small(), frame.area());
/// # }
/// ```
///
/// Renders:
///
/// ```text
/// █▀▀▄ ▄▀▀▄▝▜▛▘▄▀▀▄▝▜▛▘█ █ █
/// █▀▀▄ █▀▀█ ▐▌ █▀▀█ ▐▌ ▀▄▄▀ █
/// ```
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct RatatuiLogo {
size: Size,
}
/// The size of the logo
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Size {
/// A tiny logo
///
/// The default size of the logo (2x15 characters)
///
/// ```text
/// ▛▚▗▀▖▜▘▞▚▝▛▐ ▌▌
/// ▛▚▐▀▌▐ ▛▜ ▌▝▄▘▌
/// ```
#[default]
Tiny,
/// A small logo
///
/// A slightly larger version of the logo (2x27 characters)
///
/// ```text
/// █▀▀▄ ▄▀▀▄▝▜▛▘▄▀▀▄▝▜▛▘█ █ █
/// █▀▀▄ █▀▀█ ▐▌ █▀▀█ ▐▌ ▀▄▄▀ █
/// ```
Small,
}
impl RatatuiLogo {
/// Create a new Ratatui logo widget
///
/// # Examples
///
/// ```
/// use ratatui::widgets::{RatatuiLogo, RatatuiLogoSize};
///
/// let logo = RatatuiLogo::new(RatatuiLogoSize::Tiny);
/// ```
pub const fn new(size: Size) -> Self {
Self { size }
}
/// Set the size of the logo
///
/// # Examples
///
/// ```
/// use ratatui::widgets::{RatatuiLogo, RatatuiLogoSize};
///
/// let logo = RatatuiLogo::default().size(RatatuiLogoSize::Small);
/// ```
#[must_use]
pub const fn size(self, size: Size) -> Self {
let _ = self;
Self { size }
}
/// Create a new Ratatui logo widget with a tiny size
///
/// # Examples
///
/// ```
/// use ratatui::widgets::RatatuiLogo;
///
/// let logo = RatatuiLogo::tiny();
/// ```
pub const fn tiny() -> Self {
Self::new(Size::Tiny)
}
/// Create a new Ratatui logo widget with a small size
///
/// # Examples
///
/// ```
/// use ratatui::widgets::RatatuiLogo;
///
/// let logo = RatatuiLogo::small();
/// ```
pub const fn small() -> Self {
Self::new(Size::Small)
}
}
impl Widget for RatatuiLogo {
fn render(self, area: Rect, buf: &mut Buffer) {
let logo = self.size.as_str();
Text::raw(logo).render(area, buf);
}
}
impl Size {
const fn as_str(self) -> &'static str {
match self {
Self::Tiny => Self::tiny(),
Self::Small => Self::small(),
}
}
const fn tiny() -> &'static str {
indoc! {"
▛▚▗▀▖▜▘▞▚▝▛▐ ▌▌
▛▚▐▀▌▐ ▛▜ ▌▝▄▘▌
"}
}
const fn small() -> &'static str {
indoc! {"
█▀▀▄ ▄▀▀▄▝▜▛▘▄▀▀▄▝▜▛▘█ █ █
█▀▀▄ █▀▀█ ▐▌ █▀▀█ ▐▌ ▀▄▄▀ █
"}
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[rstest]
#[case::tiny(Size::Tiny)]
#[case::small(Size::Small)]
fn new_size(#[case] size: Size) {
let logo = RatatuiLogo::new(size);
assert_eq!(logo.size, size);
}
#[test]
fn default_logo_is_tiny() {
let logo = RatatuiLogo::default();
assert_eq!(logo.size, Size::Tiny);
}
#[test]
fn set_logo_size_to_small() {
let logo = RatatuiLogo::default().size(Size::Small);
assert_eq!(logo.size, Size::Small);
}
#[test]
fn tiny_logo_constant() {
let logo = RatatuiLogo::tiny();
assert_eq!(logo.size, Size::Tiny);
}
#[test]
fn small_logo_constant() {
let logo = RatatuiLogo::small();
assert_eq!(logo.size, Size::Small);
}
#[test]
#[rustfmt::skip]
fn render_tiny() {
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 2));
RatatuiLogo::tiny().render(buf.area, &mut buf);
assert_eq!(
buf,
Buffer::with_lines([
"▛▚▗▀▖▜▘▞▚▝▛▐ ▌▌",
"▛▚▐▀▌▐ ▛▜ ▌▝▄▘▌",
])
);
}
#[test]
#[rustfmt::skip]
fn render_small() {
let mut buf = Buffer::empty(Rect::new(0, 0, 27, 2));
RatatuiLogo::small().render(buf.area, &mut buf);
assert_eq!(
buf,
Buffer::with_lines([
"█▀▀▄ ▄▀▀▄▝▜▛▘▄▀▀▄▝▜▛▘█ █ █",
"█▀▀▄ █▀▀█ ▐▌ █▀▀█ ▐▌ ▀▄▄▀ █",
])
);
}
}

View File

@@ -1,12 +1,14 @@
use unicode_width::UnicodeWidthStr;
use crate::{
prelude::*,
style::Styled,
text::StyledGrapheme,
buffer::Buffer,
layout::{Alignment, Position, Rect},
style::{Style, Styled},
text::{Line, StyledGrapheme, Text},
widgets::{
block::BlockExt,
reflow::{LineComposer, LineTruncator, WordWrapper, WrappedLine},
Block,
Block, Widget, WidgetRef,
},
};
@@ -59,7 +61,12 @@ const fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Align
/// # Example
///
/// ```
/// use ratatui::{prelude::*, widgets::*};
/// use ratatui::{
/// layout::Alignment,
/// style::{Style, Stylize},
/// text::{Line, Span},
/// widgets::{Block, Paragraph, Wrap},
/// };
///
/// let text = vec![
/// Line::from(vec![
@@ -76,6 +83,8 @@ const fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Align
/// .alignment(Alignment::Center)
/// .wrap(Wrap { trim: true });
/// ```
///
/// [`Span`]: crate::text::Span
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Paragraph<'a> {
/// A block to wrap the widget in
@@ -97,7 +106,10 @@ pub struct Paragraph<'a> {
/// ## Examples
///
/// ```
/// use ratatui::{prelude::*, widgets::*};
/// use ratatui::{
/// text::Text,
/// widgets::{Paragraph, Wrap},
/// };
///
/// let bullet_points = Text::from(
/// r#"Some indented points:
@@ -139,7 +151,12 @@ impl<'a> Paragraph<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::{
/// style::{Style, Stylize},
/// text::{Line, Text},
/// widgets::Paragraph,
/// };
///
/// let paragraph = Paragraph::new("Hello, world!");
/// let paragraph = Paragraph::new(String::from("Hello, world!"));
/// let paragraph = Paragraph::new(Text::raw("Hello, world!"));
@@ -165,7 +182,8 @@ impl<'a> Paragraph<'a> {
/// # Example
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::{Block, Paragraph};
///
/// let paragraph = Paragraph::new("Hello, world!").block(Block::bordered().title("Paragraph"));
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
@@ -185,9 +203,15 @@ impl<'a> Paragraph<'a> {
/// # Example
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::{
/// style::{Style, Stylize},
/// widgets::Paragraph,
/// };
///
/// let paragraph = Paragraph::new("Hello, world!").style(Style::new().red().on_white());
/// ```
///
/// [`Color`]: crate::style::Color
#[must_use = "method moves the value of self and returns the modified value"]
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = style.into();
@@ -201,7 +225,8 @@ impl<'a> Paragraph<'a> {
/// # Example
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::{Paragraph, Wrap};
///
/// let paragraph = Paragraph::new("Hello, world!").wrap(Wrap { trim: true });
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
@@ -238,7 +263,8 @@ impl<'a> Paragraph<'a> {
/// # Example
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::{layout::Alignment, widgets::Paragraph};
///
/// let paragraph = Paragraph::new("Hello World").alignment(Alignment::Center);
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
@@ -254,7 +280,8 @@ impl<'a> Paragraph<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::Paragraph;
///
/// let paragraph = Paragraph::new("Hello World").left_aligned();
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
@@ -269,7 +296,8 @@ impl<'a> Paragraph<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::Paragraph;
///
/// let paragraph = Paragraph::new("Hello World").centered();
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
@@ -284,7 +312,8 @@ impl<'a> Paragraph<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::Paragraph;
///
/// let paragraph = Paragraph::new("Hello World").right_aligned();
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
@@ -305,7 +334,8 @@ impl<'a> Paragraph<'a> {
/// # Example
///
/// ```ignore
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::{widgets::{Paragraph, Wrap}};
///
/// let paragraph = Paragraph::new("Hello World")
/// .wrap(Wrap { trim: false });
/// assert_eq!(paragraph.line_count(20), 1);
@@ -359,7 +389,8 @@ impl<'a> Paragraph<'a> {
/// # Example
///
/// ```ignore
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::{widgets::Paragraph};
///
/// let paragraph = Paragraph::new("Hello World");
/// assert_eq!(paragraph.line_width(), 11);
///
@@ -473,7 +504,12 @@ mod test {
use super::*;
use crate::{
backend::TestBackend,
widgets::{block::Position, Borders},
buffer::Buffer,
layout::{Alignment, Rect},
style::{Color, Modifier, Style, Stylize},
text::{Line, Span, Text},
widgets::{block::Position, Borders, Widget},
Terminal,
};
/// Tests the [`Paragraph`] widget against the expected [`Buffer`] by rendering it onto an equal

View File

@@ -12,8 +12,11 @@ use strum::{Display, EnumString};
use unicode_width::UnicodeWidthStr;
use crate::{
prelude::*,
buffer::Buffer,
layout::Rect,
style::Style,
symbols::scrollbar::{Set, DOUBLE_HORIZONTAL, DOUBLE_VERTICAL},
widgets::StatefulWidget,
};
/// A widget to display a scrollbar
@@ -39,7 +42,15 @@ use crate::{
/// # Examples
///
/// ```rust
/// use ratatui::{prelude::*, widgets::*};
/// use ratatui::{
/// layout::{Margin, Rect},
/// text::Line,
/// widgets::{
/// Block, Borders, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState,
/// StatefulWidget,
/// },
/// Frame,
/// };
///
/// # fn render_paragraph_with_scrollbar(frame: &mut Frame, area: Rect) {
/// let vertical_scroll = 0; // from app state
@@ -254,6 +265,8 @@ impl<'a> Scrollbar<'a> {
/// your own type that implements [`Into<Style>`]).
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// [`Color`]: crate::style::Color
#[must_use = "method moves the value of self and returns the modified value"]
pub fn thumb_style<S: Into<Style>>(mut self, thumb_style: S) -> Self {
self.thumb_style = thumb_style.into();
@@ -279,6 +292,8 @@ impl<'a> Scrollbar<'a> {
/// your own type that implements [`Into<Style>`]).
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// [`Color`]: crate::style::Color
#[must_use = "method moves the value of self and returns the modified value"]
pub fn track_style<S: Into<Style>>(mut self, track_style: S) -> Self {
self.track_style = track_style.into();
@@ -304,6 +319,8 @@ impl<'a> Scrollbar<'a> {
/// your own type that implements [`Into<Style>`]).
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// [`Color`]: crate::style::Color
#[must_use = "method moves the value of self and returns the modified value"]
pub fn begin_style<S: Into<Style>>(mut self, begin_style: S) -> Self {
self.begin_style = begin_style.into();
@@ -329,6 +346,8 @@ impl<'a> Scrollbar<'a> {
/// your own type that implements [`Into<Style>`]).
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// [`Color`]: crate::style::Color
#[must_use = "method moves the value of self and returns the modified value"]
pub fn end_style<S: Into<Style>>(mut self, end_style: S) -> Self {
self.end_style = end_style.into();
@@ -382,6 +401,8 @@ impl<'a> Scrollbar<'a> {
/// ```
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// [`Color`]: crate::style::Color
#[must_use = "method moves the value of self and returns the modified value"]
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
let style = style.into();
@@ -621,6 +642,7 @@ mod tests {
use strum::ParseError;
use super::*;
use crate::{text::Text, widgets::Widget};
#[test]
fn scroll_direction_to_string() {

View File

@@ -2,7 +2,13 @@ use std::cmp::min;
use strum::{Display, EnumString};
use crate::{prelude::*, style::Styled, widgets::Block};
use crate::{
buffer::Buffer,
layout::Rect,
style::{Style, Styled},
symbols::{self},
widgets::{block::BlockExt, Block, Widget, WidgetRef},
};
/// Widget to render a sparkline over one or more lines.
///
@@ -21,7 +27,10 @@ use crate::{prelude::*, style::Styled, widgets::Block};
/// # Examples
///
/// ```
/// use ratatui::{prelude::*, widgets::*};
/// use ratatui::{
/// style::{Style, Stylize},
/// widgets::{Block, RenderDirection, Sparkline},
/// };
///
/// Sparkline::default()
/// .block(Block::bordered().title("Sparkline"))
@@ -73,6 +82,8 @@ impl<'a> Sparkline<'a> {
/// your own type that implements [`Into<Style>`]).
///
/// The foreground corresponds to the bars while the background is everything else.
///
/// [`Color`]: crate::style::Color
#[must_use = "method moves the value of self and returns the modified value"]
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = style.into();
@@ -84,7 +95,8 @@ impl<'a> Sparkline<'a> {
/// # Example
///
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::{layout::Rect, widgets::Sparkline, Frame};
///
/// # fn ui(frame: &mut Frame) {
/// # let area = Rect::default();
/// let sparkline = Sparkline::default().data(&[1, 2, 3]);
@@ -211,7 +223,10 @@ mod tests {
use strum::ParseError;
use super::*;
use crate::buffer::Cell;
use crate::{
buffer::Cell,
style::{Color, Modifier, Stylize},
};
#[test]
fn render_direction_to_string() {

View File

@@ -1,4 +1,10 @@
use crate::{prelude::*, style::Styled};
use crate::{
buffer::Buffer,
layout::Rect,
style::{Style, Styled},
text::Text,
widgets::WidgetRef,
};
/// A [`Cell`] contains the [`Text`] to be displayed in a [`Row`] of a [`Table`].
///
@@ -16,13 +22,17 @@ use crate::{prelude::*, style::Styled};
/// ```rust
/// use std::borrow::Cow;
///
/// use ratatui::{prelude::*, widgets::*};
/// use ratatui::{
/// style::Stylize,
/// text::{Line, Span, Text},
/// widgets::Cell,
/// };
///
/// Cell::from("simple string");
/// Cell::from(Span::from("span"));
/// Cell::from(Line::from(vec![
/// Span::raw("a vec of "),
/// Span::styled("spans", Style::default().add_modifier(Modifier::BOLD)),
/// Span::from("a vec of "),
/// Span::from("spans").bold(),
/// ]));
/// Cell::from(Text::from("a text"));
/// Cell::from(Text::from(Cow::Borrowed("hello")));
@@ -32,12 +42,14 @@ use crate::{prelude::*, style::Styled};
/// to set the style of the cell concisely.
///
/// ```rust
/// use ratatui::{prelude::*, widgets::*};
/// use ratatui::{style::Stylize, widgets::Cell};
///
/// Cell::new("Cell 1").red().italic();
/// ```
///
/// [`Row`]: super::Row
/// [`Table`]: super::Table
/// [`Row`]: crate::widgets::Row
/// [`Table`]: crate::widgets::Table
/// [`Stylize`]: crate::style::Stylize
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Cell<'a> {
content: Text<'a>,
@@ -52,12 +64,17 @@ impl<'a> Cell<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::{
/// style::Stylize,
/// text::{Line, Span, Text},
/// widgets::Cell,
/// };
///
/// Cell::new("simple string");
/// Cell::new(Span::from("span"));
/// Cell::new(Line::from(vec![
/// Span::raw("a vec of "),
/// Span::styled("spans", Style::default().add_modifier(Modifier::BOLD)),
/// Span::from("spans").bold(),
/// ]));
/// Cell::new(Text::from("a text"));
/// ```
@@ -80,12 +97,17 @@ impl<'a> Cell<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::{
/// style::Stylize,
/// text::{Line, Span, Text},
/// widgets::Cell,
/// };
///
/// Cell::default().content("simple string");
/// Cell::default().content(Span::from("span"));
/// Cell::default().content(Line::from(vec![
/// Span::raw("a vec of "),
/// Span::styled("spans", Style::new().bold()),
/// Span::from("spans").bold(),
/// ]));
/// Cell::default().content(Text::from("a text"));
/// ```
@@ -111,7 +133,11 @@ impl<'a> Cell<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::{
/// style::{Style, Stylize},
/// widgets::Cell,
/// };
///
/// Cell::new("Cell 1").style(Style::new().red().italic());
/// ```
///
@@ -119,11 +145,14 @@ impl<'a> Cell<'a> {
/// the [`Stylize`] trait to set the style of the widget more concisely.
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::{style::Stylize, widgets::Cell};
///
/// Cell::new("Cell 1").red().italic();
/// ```
///
/// [`Row`]: super::Row
/// [`Row`]: crate::widgets::Row
/// [`Color`]: crate::style::Color
/// [`Stylize`]: crate::style::Stylize
#[must_use = "method moves the value of self and returns the modified value"]
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = style.into();
@@ -165,6 +194,7 @@ impl<'a> Styled for Cell<'a> {
#[cfg(test)]
mod tests {
use super::*;
use crate::style::{Color, Modifier, Stylize};
#[test]
fn new() {

View File

@@ -1,5 +1,7 @@
use super::Cell;
use crate::{prelude::*, style::Styled};
use crate::{
style::{Style, Styled},
widgets::table::Cell,
};
/// A single row of data to be displayed in a [`Table`] widget.
///
@@ -16,7 +18,7 @@ use crate::{prelude::*, style::Styled};
/// You can create `Row`s from simple strings.
///
/// ```rust
/// use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::Row;
///
/// Row::new(vec!["Cell1", "Cell2", "Cell3"]);
/// ```
@@ -24,11 +26,14 @@ use crate::{prelude::*, style::Styled};
/// If you need a bit more control over individual cells, you can explicitly create [`Cell`]s:
///
/// ```rust
/// use ratatui::{prelude::*, widgets::*};
/// use ratatui::{
/// style::Stylize,
/// widgets::{Cell, Row},
/// };
///
/// Row::new(vec![
/// Cell::from("Cell1"),
/// Cell::from("Cell2").style(Style::default().fg(Color::Yellow)),
/// Cell::from("Cell2").red().italic(),
/// ]);
/// ```
///
@@ -37,7 +42,7 @@ use crate::{prelude::*, style::Styled};
/// ```rust
/// use std::borrow::Cow;
///
/// use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::{Cell, Row};
///
/// Row::new(vec![
/// Cow::Borrowed("hello"),
@@ -57,12 +62,15 @@ use crate::{prelude::*, style::Styled};
/// to set the style of the row concisely.
///
/// ```rust
/// use ratatui::{prelude::*, widgets::*};
/// use ratatui::{style::Stylize, widgets::Row};
///
/// let cells = vec!["Cell1", "Cell2", "Cell3"];
/// Row::new(cells).red().italic();
/// ```
///
/// [`Table`]: super::Table
/// [`Table`]: crate::widgets::Table
/// [`Text`]: crate::text::Text
/// [`Stylize`]: crate::style::Stylize
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Row<'a> {
pub(crate) cells: Vec<Cell<'a>>,
@@ -81,7 +89,8 @@ impl<'a> Row<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::{Cell, Row};
///
/// let row = Row::new(vec!["Cell 1", "Cell 2", "Cell 3"]);
/// let row = Row::new(vec![
/// Cell::new("Cell 1"),
@@ -111,7 +120,8 @@ impl<'a> Row<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::{Cell, Row};
///
/// let row = Row::default().cells(vec!["Cell 1", "Cell 2", "Cell 3"]);
/// let row = Row::default().cells(vec![
/// Cell::new("Cell 1"),
@@ -140,7 +150,8 @@ impl<'a> Row<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::Row;
///
/// let cells = vec!["Cell 1\nline 2", "Cell 2", "Cell 3"];
/// let row = Row::new(cells).height(2);
/// ```
@@ -159,8 +170,9 @@ impl<'a> Row<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # let cells = vec!["Cell 1", "Cell 2", "Cell 3"];
/// use ratatui::widgets::Row;
/// let cells = vec!["Cell 1", "Cell 2", "Cell 3"];
///
/// let row = Row::default().top_margin(1);
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
@@ -178,8 +190,9 @@ impl<'a> Row<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # let cells = vec!["Cell 1", "Cell 2", "Cell 3"];
/// use ratatui::widgets::Row;
///
/// let cells = vec!["Cell 1", "Cell 2", "Cell 3"];
/// let row = Row::default().bottom_margin(1);
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
@@ -201,7 +214,10 @@ impl<'a> Row<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::{
/// style::{Style, Stylize},
/// widgets::Row,
/// };
/// let cells = vec!["Cell 1", "Cell 2", "Cell 3"];
/// let row = Row::new(cells).style(Style::new().red().italic());
/// ```
@@ -210,10 +226,15 @@ impl<'a> Row<'a> {
/// the [`Stylize`] trait to set the style of the widget more concisely.
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::{style::Stylize, widgets::Row};
///
/// let cells = vec!["Cell 1", "Cell 2", "Cell 3"];
/// let row = Row::new(cells).red().italic();
/// ```
///
/// [`Color`]: crate::style::Color
/// [`Stylize`]: crate::style::Stylize
/// [`Text`]: crate::text::Text
#[must_use = "method moves the value of self and returns the modified value"]
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = style.into();
@@ -257,6 +278,7 @@ mod tests {
use std::vec;
use super::*;
use crate::style::{Color, Modifier, Stylize};
#[test]
fn new() {

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,19 @@
/// State of a [`Table`] widget
///
/// This state can be used to scroll through the rows and select one of them. When the table is
/// rendered as a stateful widget, the selected row will be highlighted and the table will be
/// shifted to ensure that the selected row is visible. This will modify the [`TableState`] object
/// passed to the [`Frame::render_stateful_widget`] method.
/// rendered as a stateful widget, the selected row, column and cell will be highlighted and the
/// table will be shifted to ensure that the selected row is visible. This will modify the
/// [`TableState`] object passed to the [`Frame::render_stateful_widget`] method.
///
/// The state consists of two fields:
/// - [`offset`]: the index of the first row to be displayed
/// - [`selected`]: the index of the selected row, which can be `None` if no row is selected
/// - [`selected_column`]: the index of the selected column, which can be `None` if no column is
/// selected
///
/// [`offset`]: TableState::offset()
/// [`selected`]: TableState::selected()
/// [`selected_column`]: TableState::selected_column()
///
/// See the `table` example and the `recipe` and `traceroute` tabs in the demo2 example in the
/// [Examples] directory for a more in depth example of the various configuration options and for
@@ -21,11 +24,16 @@
/// # Example
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::{
/// layout::{Constraint, Rect},
/// widgets::{Row, Table, TableState},
/// Frame,
/// };
///
/// # fn ui(frame: &mut Frame) {
/// # let area = Rect::default();
/// # let rows = [Row::new(vec!["Cell1", "Cell2"])];
/// # let widths = [Constraint::Length(5), Constraint::Length(5)];
/// let rows = [Row::new(vec!["Cell1", "Cell2"])];
/// let widths = [Constraint::Length(5), Constraint::Length(5)];
/// let table = Table::new(rows, widths).widths(widths);
///
/// // Note: TableState should be stored in your application state (not constructed in your render
@@ -33,6 +41,7 @@
/// let mut table_state = TableState::default();
/// *table_state.offset_mut() = 1; // display the second row and onwards
/// table_state.select(Some(3)); // select the forth row (0-indexed)
/// table_state.select_column(Some(2)); // select the third column (0-indexed)
///
/// frame.render_stateful_widget(table, area, &mut table_state);
/// # }
@@ -49,6 +58,7 @@
pub struct TableState {
pub(crate) offset: usize,
pub(crate) selected: Option<usize>,
pub(crate) selected_column: Option<usize>,
}
impl TableState {
@@ -57,13 +67,15 @@ impl TableState {
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::TableState;
///
/// let state = TableState::new();
/// ```
pub const fn new() -> Self {
Self {
offset: 0,
selected: None,
selected_column: None,
}
}
@@ -74,7 +86,8 @@ impl TableState {
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::TableState;
///
/// let state = TableState::new().with_offset(1);
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
@@ -90,7 +103,8 @@ impl TableState {
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::TableState;
///
/// let state = TableState::new().with_selected(Some(1));
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
@@ -102,12 +116,58 @@ impl TableState {
self
}
/// Index of the first row to be displayed
/// Sets the index of the selected column
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let state = TableState::new().with_selected_column(Some(1));
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn with_selected_column<T>(mut self, selected: T) -> Self
where
T: Into<Option<usize>>,
{
self.selected_column = selected.into();
self
}
/// Sets the indexes of the selected cell
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let state = TableState::new().with_selected_cell(Some((1, 5)));
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn with_selected_cell<T>(mut self, selected: T) -> Self
where
T: Into<Option<(usize, usize)>>,
{
if let Some((r, c)) = selected.into() {
self.selected = Some(r);
self.selected_column = Some(c);
} else {
self.selected = None;
self.selected_column = None;
}
self
}
/// Index of the first row to be displayed
///
/// # Examples
///
/// ```rust
/// use ratatui::widgets::TableState;
///
/// let state = TableState::new();
/// assert_eq!(state.offset(), 0);
/// ```
@@ -120,7 +180,8 @@ impl TableState {
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::TableState;
///
/// let mut state = TableState::default();
/// *state.offset_mut() = 1;
/// ```
@@ -135,7 +196,8 @@ impl TableState {
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::TableState;
///
/// let state = TableState::new();
/// assert_eq!(state.selected(), None);
/// ```
@@ -143,6 +205,39 @@ impl TableState {
self.selected
}
/// Index of the selected column
///
/// Returns `None` if no column is selected
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let state = TableState::new();
/// assert_eq!(state.selected_column(), None);
/// ```
pub const fn selected_column(&self) -> Option<usize> {
self.selected_column
}
/// Indexes of the selected cell
///
/// Returns `None` if no cell is selected
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let state = TableState::new();
/// assert_eq!(state.selected_cell(), None);
/// ```
pub const fn selected_cell(&self) -> Option<(usize, usize)> {
if let (Some(r), Some(c)) = (self.selected, self.selected_column) {
return Some((r, c));
}
None
}
/// Mutable reference to the index of the selected row
///
/// Returns `None` if no row is selected
@@ -150,7 +245,8 @@ impl TableState {
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::TableState;
///
/// let mut state = TableState::default();
/// *state.selected_mut() = Some(1);
/// ```
@@ -158,6 +254,21 @@ impl TableState {
&mut self.selected
}
/// Mutable reference to the index of the selected column
///
/// Returns `None` if no column is selected
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let mut state = TableState::default();
/// *state.selected_column_mut() = Some(1);
/// ```
pub fn selected_column_mut(&mut self) -> &mut Option<usize> {
&mut self.selected_column
}
/// Sets the index of the selected row
///
/// Set to `None` if no row is selected. This will also reset the offset to `0`.
@@ -165,7 +276,8 @@ impl TableState {
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::TableState;
///
/// let mut state = TableState::default();
/// state.select(Some(1));
/// ```
@@ -176,16 +288,52 @@ impl TableState {
}
}
/// Selects the next item or the first one if no item is selected
///
/// Note: until the table is rendered, the number of items is not known, so the index is set to
/// `0` and will be corrected when the table is rendered
/// Sets the index of the selected column
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let mut state = TableState::default();
/// state.select_column(Some(1));
/// ```
pub fn select_column(&mut self, index: Option<usize>) {
self.selected_column = index;
}
/// Sets the indexes of the selected cell
///
/// Set to `None` if no cell is selected. This will also reset the row offset to `0`.
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let mut state = TableState::default();
/// state.select_cell(Some((1, 5)));
/// ```
pub fn select_cell(&mut self, indexes: Option<(usize, usize)>) {
if let Some((r, c)) = indexes {
self.selected = Some(r);
self.selected_column = Some(c);
} else {
self.offset = 0;
self.selected = None;
self.selected_column = None;
}
}
/// Selects the next row or the first one if no row is selected
///
/// Note: until the table is rendered, the number of rows is not known, so the index is set to
/// `0` and will be corrected when the table is rendered
///
/// # Examples
///
/// ```rust
/// use ratatui::widgets::TableState;
///
/// let mut state = TableState::default();
/// state.select_next();
/// ```
pub fn select_next(&mut self) {
@@ -193,16 +341,34 @@ impl TableState {
self.select(Some(next));
}
/// Selects the previous item or the last one if no item is selected
/// Selects the next column or the first one if no column is selected
///
/// Note: until the table is rendered, the number of items is not known, so the index is set to
/// `usize::MAX` and will be corrected when the table is rendered
/// Note: until the table is rendered, the number of columns is not known, so the index is set
/// to `0` and will be corrected when the table is rendered
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let mut state = TableState::default();
/// state.select_next_column();
/// ```
pub fn select_next_column(&mut self) {
let next = self.selected_column.map_or(0, |i| i.saturating_add(1));
self.select_column(Some(next));
}
/// Selects the previous row or the last one if no item is selected
///
/// Note: until the table is rendered, the number of rows is not known, so the index is set to
/// `usize::MAX` and will be corrected when the table is rendered
///
/// # Examples
///
/// ```rust
/// use ratatui::widgets::TableState;
///
/// let mut state = TableState::default();
/// state.select_previous();
/// ```
pub fn select_previous(&mut self) {
@@ -210,48 +376,102 @@ impl TableState {
self.select(Some(previous));
}
/// Selects the first item
/// Selects the previous column or the last one if no column is selected
///
/// Note: until the table is rendered, the number of items is not known, so the index is set to
/// `0` and will be corrected when the table is rendered
/// Note: until the table is rendered, the number of columns is not known, so the index is set
/// to `usize::MAX` and will be corrected when the table is rendered
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let mut state = TableState::default();
/// state.select_previous_column();
/// ```
pub fn select_previous_column(&mut self) {
let previous = self
.selected_column
.map_or(usize::MAX, |i| i.saturating_sub(1));
self.select_column(Some(previous));
}
/// Selects the first row
///
/// Note: until the table is rendered, the number of rows is not known, so the index is set to
/// `0` and will be corrected when the table is rendered
///
/// # Examples
///
/// ```rust
/// use ratatui::widgets::TableState;
///
/// let mut state = TableState::default();
/// state.select_first();
/// ```
pub fn select_first(&mut self) {
self.select(Some(0));
}
/// Selects the last item
/// Selects the first column
///
/// Note: until the table is rendered, the number of items is not known, so the index is set to
/// Note: until the table is rendered, the number of columns is not known, so the index is set
/// to `0` and will be corrected when the table is rendered
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let mut state = TableState::default();
/// state.select_first_column();
/// ```
pub fn select_first_column(&mut self) {
self.select_column(Some(0));
}
/// Selects the last row
///
/// Note: until the table is rendered, the number of rows is not known, so the index is set to
/// `usize::MAX` and will be corrected when the table is rendered
///
/// # Examples
///
/// ```rust
/// use ratatui::widgets::TableState;
///
/// let mut state = TableState::default();
/// state.select_last();
/// ```
pub fn select_last(&mut self) {
self.select(Some(usize::MAX));
}
/// Selects the last column
///
/// Note: until the table is rendered, the number of columns is not known, so the index is set
/// to `usize::MAX` and will be corrected when the table is rendered
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let mut state = TableState::default();
/// state.select_last();
/// ```
pub fn select_last(&mut self) {
self.select(Some(usize::MAX));
pub fn select_last_column(&mut self) {
self.select_column(Some(usize::MAX));
}
/// Scrolls down by a specified `amount` in the table.
///
/// This method updates the selected index by moving it down by the given `amount`.
/// If the `amount` causes the index to go out of bounds (i.e., if the index is greater than
/// the length of the table), the last item in the table will be selected.
/// the number of rows in the table), the last row in the table will be selected.
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::TableState;
///
/// let mut state = TableState::default();
/// state.scroll_down_by(4);
/// ```
@@ -264,6 +484,43 @@ impl TableState {
///
/// This method updates the selected index by moving it up by the given `amount`.
/// If the `amount` causes the index to go out of bounds (i.e., less than zero),
/// the first row in the table will be selected.
///
/// # Examples
///
/// ```rust
/// use ratatui::widgets::TableState;
///
/// let mut state = TableState::default();
/// state.scroll_up_by(4);
/// ```
pub fn scroll_up_by(&mut self, amount: u16) {
let selected = self.selected.unwrap_or_default();
self.select(Some(selected.saturating_sub(amount as usize)));
}
/// Scrolls right by a specified `amount` in the table.
///
/// This method updates the selected index by moving it right by the given `amount`.
/// If the `amount` causes the index to go out of bounds (i.e., if the index is greater than
/// the number of columns in the table), the last column in the table will be selected.
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let mut state = TableState::default();
/// state.scroll_right_by(4);
/// ```
pub fn scroll_right_by(&mut self, amount: u16) {
let selected = self.selected_column.unwrap_or_default();
self.select_column(Some(selected.saturating_add(amount as usize)));
}
/// Scrolls left by a specified `amount` in the table.
///
/// This method updates the selected index by moving it left by the given `amount`.
/// If the `amount` causes the index to go out of bounds (i.e., less than zero),
/// the first item in the table will be selected.
///
/// # Examples
@@ -271,11 +528,11 @@ impl TableState {
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let mut state = TableState::default();
/// state.scroll_up_by(4);
/// state.scroll_left_by(4);
/// ```
pub fn scroll_up_by(&mut self, amount: u16) {
let selected = self.selected.unwrap_or_default();
self.select(Some(selected.saturating_sub(amount as usize)));
pub fn scroll_left_by(&mut self, amount: u16) {
let selected = self.selected_column.unwrap_or_default();
self.select_column(Some(selected.saturating_sub(amount as usize)));
}
}
@@ -288,6 +545,7 @@ mod tests {
let state = TableState::new();
assert_eq!(state.offset, 0);
assert_eq!(state.selected, None);
assert_eq!(state.selected_column, None);
}
#[test]
@@ -302,6 +560,19 @@ mod tests {
assert_eq!(state.selected, Some(1));
}
#[test]
fn with_selected_column() {
let state = TableState::new().with_selected_column(Some(1));
assert_eq!(state.selected_column, Some(1));
}
#[test]
fn with_selected_cell_none() {
let state = TableState::new().with_selected_cell(None);
assert_eq!(state.selected, None);
assert_eq!(state.selected_column, None);
}
#[test]
fn offset() {
let state = TableState::new();
@@ -321,6 +592,18 @@ mod tests {
assert_eq!(state.selected(), None);
}
#[test]
fn selected_column() {
let state = TableState::new();
assert_eq!(state.selected_column(), None);
}
#[test]
fn selected_cell() {
let state = TableState::new();
assert_eq!(state.selected_cell(), None);
}
#[test]
fn selected_mut() {
let mut state = TableState::new();
@@ -328,6 +611,13 @@ mod tests {
assert_eq!(state.selected, Some(1));
}
#[test]
fn selected_column_mut() {
let mut state = TableState::new();
*state.selected_column_mut() = Some(1);
assert_eq!(state.selected_column, Some(1));
}
#[test]
fn select() {
let mut state = TableState::new();
@@ -342,6 +632,36 @@ mod tests {
assert_eq!(state.selected, None);
}
#[test]
fn select_column() {
let mut state = TableState::new();
state.select_column(Some(1));
assert_eq!(state.selected_column, Some(1));
}
#[test]
fn select_column_none() {
let mut state = TableState::new().with_selected_column(Some(1));
state.select_column(None);
assert_eq!(state.selected_column, None);
}
#[test]
fn select_cell() {
let mut state = TableState::new();
state.select_cell(Some((1, 5)));
assert_eq!(state.selected_cell(), Some((1, 5)));
}
#[test]
fn select_cell_none() {
let mut state = TableState::new().with_selected_cell(Some((1, 5)));
state.select_cell(None);
assert_eq!(state.selected, None);
assert_eq!(state.selected_column, None);
assert_eq!(state.selected_cell(), None);
}
#[test]
fn test_table_state_navigation() {
let mut state = TableState::default();
@@ -392,5 +712,37 @@ mod tests {
state.scroll_up_by(4);
assert_eq!(state.selected, Some(0));
let mut state = TableState::default();
state.select_first_column();
assert_eq!(state.selected_column, Some(0));
state.select_previous_column();
assert_eq!(state.selected_column, Some(0));
state.select_next_column();
assert_eq!(state.selected_column, Some(1));
state.select_previous_column();
assert_eq!(state.selected_column, Some(0));
state.select_last_column();
assert_eq!(state.selected_column, Some(usize::MAX));
state.select_previous_column();
assert_eq!(state.selected_column, Some(usize::MAX - 1));
let mut state = TableState::default().with_selected_column(Some(12));
state.scroll_right_by(4);
assert_eq!(state.selected_column, Some(16));
state.scroll_left_by(20);
assert_eq!(state.selected_column, Some(0));
state.scroll_right_by(100);
assert_eq!(state.selected_column, Some(100));
state.scroll_left_by(20);
assert_eq!(state.selected_column, Some(80));
}
}

View File

@@ -1,4 +1,13 @@
use crate::{prelude::*, style::Styled, widgets::Block};
use itertools::Itertools;
use crate::{
buffer::Buffer,
layout::Rect,
style::{Modifier, Style, Styled},
symbols::{self},
text::{Line, Span},
widgets::{block::BlockExt, Block, Widget, WidgetRef},
};
const DEFAULT_HIGHLIGHT_STYLE: Style = Style::new().add_modifier(Modifier::REVERSED);
@@ -14,7 +23,11 @@ const DEFAULT_HIGHLIGHT_STYLE: Style = Style::new().add_modifier(Modifier::REVER
/// # Example
///
/// ```
/// use ratatui::{prelude::*, widgets::*};
/// use ratatui::{
/// style::{Style, Stylize},
/// symbols,
/// widgets::{Block, Tabs},
/// };
///
/// Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"])
/// .block(Block::bordered().title("Tabs"))
@@ -33,14 +46,14 @@ const DEFAULT_HIGHLIGHT_STYLE: Style = Style::new().add_modifier(Modifier::REVER
///
/// (0..5).map(|i| format!("Tab{i}")).collect::<Tabs>();
/// ```
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Tabs<'a> {
/// A block to wrap this widget in if necessary
block: Option<Block<'a>>,
/// One title for each tab
titles: Vec<Line<'a>>,
/// The index of the selected tabs
selected: usize,
selected: Option<usize>,
/// The style used to draw the text
style: Style,
/// Style to apply to the selected item
@@ -53,6 +66,30 @@ pub struct Tabs<'a> {
padding_right: Line<'a>,
}
impl Default for Tabs<'_> {
/// Returns a default `Tabs` widget.
///
/// The default widget has:
/// - No tabs
/// - No selected tab
/// - The highlight style is set to reversed.
/// - The divider is set to a pipe (`|`).
/// - The padding on the left and right is set to a space.
///
/// This is rarely useful on its own without calling [`Tabs::titles`].
///
/// # Examples
///
/// ```
/// use ratatui::widgets::Tabs;
///
/// let tabs = Tabs::default().titles(["Tab 1", "Tab 2"]);
/// ```
fn default() -> Self {
Self::new(Vec::<Line>::new())
}
}
impl<'a> Tabs<'a> {
/// Creates new `Tabs` from their titles.
///
@@ -75,13 +112,15 @@ impl<'a> Tabs<'a> {
///
/// Basic titles.
/// ```
/// # use ratatui::{prelude::*, widgets::Tabs};
/// use ratatui::widgets::Tabs;
///
/// let tabs = Tabs::new(vec!["Tab 1", "Tab 2"]);
/// ```
///
/// Styled titles
/// ```
/// # use ratatui::{prelude::*, widgets::Tabs};
/// use ratatui::{style::Stylize, widgets::Tabs};
///
/// let tabs = Tabs::new(vec!["Tab 1".red(), "Tab 2".blue()]);
/// ```
pub fn new<Iter>(titles: Iter) -> Self
@@ -89,10 +128,12 @@ impl<'a> Tabs<'a> {
Iter: IntoIterator,
Iter::Item: Into<Line<'a>>,
{
let titles = titles.into_iter().map(Into::into).collect_vec();
let selected = if titles.is_empty() { None } else { Some(0) };
Self {
block: None,
titles: titles.into_iter().map(Into::into).collect(),
selected: 0,
titles,
selected,
style: Style::default(),
highlight_style: DEFAULT_HIGHLIGHT_STYLE,
divider: Span::raw(symbols::line::VERTICAL),
@@ -101,6 +142,48 @@ impl<'a> Tabs<'a> {
}
}
/// Sets the titles of the tabs.
///
/// `titles` is an iterator whose elements can be converted into `Line`.
///
/// The selected tab can be set with [`Tabs::select`]. The first tab has index 0 (this is also
/// the default index).
///
/// # Examples
///
/// Basic titles.
///
/// ```
/// use ratatui::widgets::Tabs;
///
/// let tabs = Tabs::default().titles(vec!["Tab 1", "Tab 2"]);
/// ```
///
/// Styled titles.
///
/// ```
/// use ratatui::{style::Stylize, widgets::Tabs};
///
/// let tabs = Tabs::default().titles(vec!["Tab 1".red(), "Tab 2".blue()]);
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn titles<Iter>(mut self, titles: Iter) -> Self
where
Iter: IntoIterator,
Iter::Item: Into<Line<'a>>,
{
self.titles = titles.into_iter().map(Into::into).collect_vec();
self.selected = if self.titles.is_empty() {
None
} else {
// Ensure selected is within bounds, and default to 0 if no selected tab
self.selected
.map(|selected| selected.min(self.titles.len() - 1))
.or(Some(0))
};
self
}
/// Surrounds the `Tabs` with a [`Block`].
#[must_use = "method moves the value of self and returns the modified value"]
pub fn block(mut self, block: Block<'a>) -> Self {
@@ -112,9 +195,27 @@ impl<'a> Tabs<'a> {
///
/// The first tab has index 0 (this is also the default index).
/// The selected tab can have a different style with [`Tabs::highlight_style`].
///
/// # Examples
///
/// Select the second tab.
///
/// ```
/// use ratatui::widgets::Tabs;
///
/// let tabs = Tabs::new(vec!["Tab 1", "Tab 2"]).select(1);
/// ```
///
/// Deselect the selected tab.
///
/// ```
/// use ratatui::widgets::Tabs;
///
/// let tabs = Tabs::new(vec!["Tab 1", "Tab 2"]).select(None);
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub const fn select(mut self, selected: usize) -> Self {
self.selected = selected;
pub fn select<T: Into<Option<usize>>>(mut self, selected: T) -> Self {
self.selected = selected.into();
self
}
@@ -126,6 +227,8 @@ impl<'a> Tabs<'a> {
/// This will set the given style on the entire render area.
/// More precise style can be applied to the titles by styling the ones given to [`Tabs::new`].
/// The selected tab can be styled differently using [`Tabs::highlight_style`].
///
/// [`Color`]: crate::style::Color
#[must_use = "method moves the value of self and returns the modified value"]
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = style.into();
@@ -139,6 +242,8 @@ impl<'a> Tabs<'a> {
///
/// Highlighted tab can be selected with [`Tabs::select`].
#[must_use = "method moves the value of self and returns the modified value"]
///
/// [`Color`]: crate::style::Color
pub fn highlight_style<S: Into<Style>>(mut self, style: S) -> Self {
self.highlight_style = style.into();
self
@@ -152,12 +257,14 @@ impl<'a> Tabs<'a> {
///
/// Use a dot (`•`) as separator.
/// ```
/// # use ratatui::{prelude::*, widgets::Tabs, symbols};
/// use ratatui::{symbols, widgets::Tabs};
///
/// let tabs = Tabs::new(vec!["Tab 1", "Tab 2"]).divider(symbols::DOT);
/// ```
/// Use dash (`-`) as separator.
/// ```
/// # use ratatui::{prelude::*, widgets::Tabs};
/// use ratatui::widgets::Tabs;
///
/// let tabs = Tabs::new(vec!["Tab 1", "Tab 2"]).divider("-");
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
@@ -177,12 +284,14 @@ impl<'a> Tabs<'a> {
///
/// A space on either side of the tabs.
/// ```
/// # use ratatui::{prelude::*, widgets::Tabs};
/// use ratatui::widgets::Tabs;
///
/// let tabs = Tabs::new(vec!["Tab 1", "Tab 2"]).padding(" ", " ");
/// ```
/// Nothing on either side of the tabs.
/// ```
/// # use ratatui::{prelude::*, widgets::Tabs};
/// use ratatui::widgets::Tabs;
///
/// let tabs = Tabs::new(vec!["Tab 1", "Tab 2"]).padding("", "");
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
@@ -204,7 +313,8 @@ impl<'a> Tabs<'a> {
///
/// An arrow on the left of tabs.
/// ```
/// # use ratatui::{prelude::*, widgets::Tabs};
/// use ratatui::widgets::Tabs;
///
/// let tabs = Tabs::new(vec!["Tab 1", "Tab 2"]).padding_left("->");
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
@@ -224,7 +334,8 @@ impl<'a> Tabs<'a> {
///
/// An arrow on the right of tabs.
/// ```
/// # use ratatui::{prelude::*, widgets::Tabs};
/// use ratatui::widgets::Tabs;
///
/// let tabs = Tabs::new(vec!["Tab 1", "Tab 2"]).padding_right("<-");
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
@@ -290,7 +401,7 @@ impl Tabs<'_> {
// Title
let pos = buf.set_line(x, tabs_area.top(), title, remaining_width);
if i == self.selected {
if Some(i) == self.selected {
buf.set_style(
Rect {
x,
@@ -333,6 +444,7 @@ where
#[cfg(test)]
mod tests {
use super::*;
use crate::style::{Color, Stylize};
#[test]
fn new() {
@@ -348,7 +460,7 @@ mod tests {
Line::from("Tab3"),
Line::from("Tab4"),
],
selected: 0,
selected: Some(0),
style: Style::default(),
highlight_style: DEFAULT_HIGHLIGHT_STYLE,
divider: Span::raw(symbols::line::VERTICAL),
@@ -358,6 +470,37 @@ mod tests {
);
}
#[test]
fn default() {
assert_eq!(
Tabs::default(),
Tabs {
block: None,
titles: vec![],
selected: None,
style: Style::default(),
highlight_style: DEFAULT_HIGHLIGHT_STYLE,
divider: Span::raw(symbols::line::VERTICAL),
padding_right: Line::from(" "),
padding_left: Line::from(" "),
}
);
}
#[test]
fn select_into() {
let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"]);
assert_eq!(tabs.clone().select(2).selected, Some(2));
assert_eq!(tabs.clone().select(None).selected, None);
assert_eq!(tabs.clone().select(1u8 as usize).selected, Some(1));
}
#[test]
fn select_before_titles() {
let tabs = Tabs::default().select(1).titles(["Tab1", "Tab2"]);
assert_eq!(tabs.selected, Some(1));
}
#[test]
fn new_from_vec_of_str() {
Tabs::new(vec!["a", "b"]);
@@ -386,7 +529,7 @@ mod tests {
}
#[test]
fn render_default() {
fn render_new() {
let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"]);
let mut expected = Buffer::with_lines([" Tab1 │ Tab2 │ Tab3 │ Tab4 "]);
// first tab selected
@@ -466,6 +609,10 @@ mod tests {
// out of bounds selects no tab
let expected = Buffer::with_lines([" Tab1 │ Tab2 │ Tab3 │ Tab4 "]);
test_case(tabs.clone().select(4), Rect::new(0, 0, 30, 1), &expected);
// deselect
let expected = Buffer::with_lines([" Tab1 │ Tab2 │ Tab3 │ Tab4 "]);
test_case(tabs.clone().select(None), Rect::new(0, 0, 30, 1), &expected);
}
#[test]

View File

@@ -14,7 +14,16 @@
// not too happy about the redundancy in these tests,
// but if that helps readability then it's ok i guess /shrug
use ratatui::{backend::TestBackend, prelude::*, widgets::*};
use ratatui::{
backend::TestBackend,
layout::{Constraint, Direction, Layout},
text::Line,
widgets::{
Block, Borders, List, ListState, Row, Scrollbar, ScrollbarOrientation, ScrollbarState,
Table, TableState,
},
Terminal,
};
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
struct AppState {
@@ -35,7 +44,7 @@ impl Default for AppState {
impl AppState {
fn select(&mut self, index: usize) {
self.list.select(Some(index));
self.table.select(Some(index));
self.table.select_cell(Some((index, index)));
self.scrollbar = self.scrollbar.position(index);
}
}
@@ -98,7 +107,8 @@ const DEFAULT_STATE_REPR: &str = r#"{
},
"table": {
"offset": 0,
"selected": null
"selected": null,
"selected_column": null
},
"scrollbar": {
"content_length": 10,
@@ -135,7 +145,8 @@ const SELECTED_STATE_REPR: &str = r#"{
},
"table": {
"offset": 0,
"selected": 1
"selected": 1,
"selected_column": 0
},
"scrollbar": {
"content_length": 10,
@@ -174,7 +185,8 @@ const SCROLLED_STATE_REPR: &str = r#"{
},
"table": {
"offset": 4,
"selected": 8
"selected": 8,
"selected_column": 0
},
"scrollbar": {
"content_length": 10,
@@ -197,3 +209,23 @@ fn scrolled_state_deserialize() {
let mut state: AppState = serde_json::from_str(SCROLLED_STATE_REPR).unwrap();
assert_buffer(&mut state, SCROLLED_STATE_BUFFER);
}
// For backwards compatibility these fields should be enough to deserialize the state.
const OLD_TABLE_DESERIALIZE: &str = r#"{
"offset": 0,
"selected": 1
}"#;
const NEW_TABLE_DESERIALIZE: &str = r#"{
"offset": 0,
"selected": 1,
"selected_column": null
}"#;
// This test is to check for backwards compatibility with the old states.
#[test]
fn table_state_backwards_compatibility() {
let old_state: TableState = serde_json::from_str(OLD_TABLE_DESERIALIZE).unwrap();
let new_state: TableState = serde_json::from_str(NEW_TABLE_DESERIALIZE).unwrap();
assert_eq!(old_state, new_state);
}

View File

@@ -1,21 +1,12 @@
use std::error::Error;
use ratatui::{
backend::{Backend, TestBackend},
backend::TestBackend,
layout::Rect,
widgets::{Block, Paragraph, Widget},
Terminal, TerminalOptions, Viewport,
};
#[test]
fn terminal_buffer_size_should_be_limited() {
let backend = TestBackend::new(400, 400);
let terminal = Terminal::new(backend).unwrap();
let size = terminal.backend().size().unwrap();
assert_eq!(size.width, 255);
assert_eq!(size.height, 255);
}
#[test]
fn swap_buffer_clears_prev_buffer() {
let backend = TestBackend::new(100, 50);
@@ -115,6 +106,46 @@ fn terminal_insert_before_moves_viewport() -> Result<(), Box<dyn Error>> {
Ok(())
}
#[test]
#[cfg(feature = "scrolling-regions")]
fn terminal_insert_before_moves_viewport_does_not_clobber() -> Result<(), Box<dyn Error>> {
// This is like terminal_insert_before_moves_viewport, except it draws first before calling
// insert_before, and doesn't draw again afterwards. When using scrolling regions, we
// shouldn't clobber the viewport.
let backend = TestBackend::new(20, 5);
let mut terminal = Terminal::with_options(
backend,
TerminalOptions {
viewport: Viewport::Inline(1),
},
)?;
terminal.draw(|f| {
let paragraph = Paragraph::new("[---- Viewport ----]");
f.render_widget(paragraph, f.area());
})?;
terminal.insert_before(2, |buf| {
Paragraph::new(vec![
"------ Line 1 ------".into(),
"------ Line 2 ------".into(),
])
.render(buf.area, buf);
})?;
terminal.backend().assert_scrollback_empty();
terminal.backend().assert_buffer_lines([
"------ Line 1 ------",
"------ Line 2 ------",
"[---- Viewport ----]",
" ",
" ",
]);
Ok(())
}
#[test]
fn terminal_insert_before_scrolls_on_large_input() -> Result<(), Box<dyn Error>> {
// When we have a terminal with 5 lines, and a single line viewport, if we insert many
@@ -160,6 +191,51 @@ fn terminal_insert_before_scrolls_on_large_input() -> Result<(), Box<dyn Error>>
Ok(())
}
#[test]
#[cfg(feature = "scrolling-regions")]
fn terminal_insert_before_scrolls_on_large_input_does_not_clobber() -> Result<(), Box<dyn Error>> {
// This is like terminal_insert_scrolls_on_large_input, except it draws first before calling
// insert_before, and doesn't draw again afterwards. When using scrolling regions, we
// shouldn't clobber the viewport.
let backend = TestBackend::new(20, 5);
let mut terminal = Terminal::with_options(
backend,
TerminalOptions {
viewport: Viewport::Inline(1),
},
)?;
terminal.draw(|f| {
let paragraph = Paragraph::new("[---- Viewport ----]");
f.render_widget(paragraph, f.area());
})?;
terminal.insert_before(5, |buf| {
Paragraph::new(vec![
"------ Line 1 ------".into(),
"------ Line 2 ------".into(),
"------ Line 3 ------".into(),
"------ Line 4 ------".into(),
"------ Line 5 ------".into(),
])
.render(buf.area, buf);
})?;
terminal
.backend()
.assert_scrollback_lines(["------ Line 1 ------"]);
terminal.backend().assert_buffer_lines([
"------ Line 2 ------",
"------ Line 3 ------",
"------ Line 4 ------",
"------ Line 5 ------",
"[---- Viewport ----]",
]);
Ok(())
}
#[test]
fn terminal_insert_before_scrolls_on_many_inserts() -> Result<(), Box<dyn Error>> {
// This test ensures similar behaviour to `terminal_insert_before_scrolls_on_large_input`
@@ -215,6 +291,60 @@ fn terminal_insert_before_scrolls_on_many_inserts() -> Result<(), Box<dyn Error>
Ok(())
}
#[test]
#[cfg(feature = "scrolling-regions")]
fn terminal_insert_before_scrolls_on_many_inserts_does_not_clobber() -> Result<(), Box<dyn Error>> {
// This is like terminal_insert_before_scrolls_on_many_inserts, except it draws first before
// calling insert_before, and doesn't draw again afterwards. When using scrolling regions, we
// shouldn't clobber the viewport.
let backend = TestBackend::new(20, 5);
let mut terminal = Terminal::with_options(
backend,
TerminalOptions {
viewport: Viewport::Inline(1),
},
)?;
terminal.draw(|f| {
let paragraph = Paragraph::new("[---- Viewport ----]");
f.render_widget(paragraph, f.area());
})?;
terminal.insert_before(1, |buf| {
Paragraph::new(vec!["------ Line 1 ------".into()]).render(buf.area, buf);
})?;
terminal.insert_before(1, |buf| {
Paragraph::new(vec!["------ Line 2 ------".into()]).render(buf.area, buf);
})?;
terminal.insert_before(1, |buf| {
Paragraph::new(vec!["------ Line 3 ------".into()]).render(buf.area, buf);
})?;
terminal.insert_before(1, |buf| {
Paragraph::new(vec!["------ Line 4 ------".into()]).render(buf.area, buf);
})?;
terminal.insert_before(1, |buf| {
Paragraph::new(vec!["------ Line 5 ------".into()]).render(buf.area, buf);
})?;
terminal
.backend()
.assert_scrollback_lines(["------ Line 1 ------"]);
terminal.backend().assert_buffer_lines([
"------ Line 2 ------",
"------ Line 3 ------",
"------ Line 4 ------",
"------ Line 5 ------",
"[---- Viewport ----]",
]);
Ok(())
}
#[test]
fn terminal_insert_before_large_viewport() -> Result<(), Box<dyn Error>> {
// This test covers a bug previously present whereby doing an insert_before when the
@@ -282,3 +412,73 @@ fn terminal_insert_before_large_viewport() -> Result<(), Box<dyn Error>> {
Ok(())
}
#[test]
#[cfg(feature = "scrolling-regions")]
fn terminal_insert_before_large_viewport_does_not_clobber() -> Result<(), Box<dyn Error>> {
// This is like terminal_insert_before_large_viewport, except it draws first before calling
// insert_before, and doesn't draw again afterwards. When using scrolling regions, we shouldn't
// clobber the viewport.
let backend = TestBackend::new(20, 3);
let mut terminal = Terminal::with_options(
backend,
TerminalOptions {
viewport: Viewport::Inline(3),
},
)?;
terminal.draw(|f| {
let paragraph = Paragraph::new("Viewport")
.centered()
.block(Block::bordered());
f.render_widget(paragraph, f.area());
})?;
terminal.insert_before(1, |buf| {
Paragraph::new(vec!["------ Line 1 ------".into()]).render(buf.area, buf);
})?;
terminal.insert_before(3, |buf| {
Paragraph::new(vec![
"------ Line 2 ------".into(),
"------ Line 3 ------".into(),
"------ Line 4 ------".into(),
])
.render(buf.area, buf);
})?;
terminal.insert_before(7, |buf| {
Paragraph::new(vec![
"------ Line 5 ------".into(),
"------ Line 6 ------".into(),
"------ Line 7 ------".into(),
"------ Line 8 ------".into(),
"------ Line 9 ------".into(),
"----- Line 10 ------".into(),
"----- Line 11 ------".into(),
])
.render(buf.area, buf);
})?;
terminal.backend().assert_buffer_lines([
"┌──────────────────┐",
"│ Viewport │",
"└──────────────────┘",
]);
terminal.backend().assert_scrollback_lines([
"------ Line 1 ------",
"------ Line 2 ------",
"------ Line 3 ------",
"------ Line 4 ------",
"------ Line 5 ------",
"------ Line 6 ------",
"------ Line 7 ------",
"------ Line 8 ------",
"------ Line 9 ------",
"----- Line 10 ------",
"----- Line 11 ------",
]);
Ok(())
}

View File

@@ -632,6 +632,7 @@ fn widgets_table_can_have_elements_styled_individually() {
let mut terminal = Terminal::new(backend).unwrap();
let mut state = TableState::default();
state.select(Some(0));
state.select_column(Some(1));
terminal
.draw(|f| {
let table = Table::new(
@@ -658,7 +659,9 @@ fn widgets_table_can_have_elements_styled_individually() {
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
.block(Block::new().borders(Borders::LEFT | Borders::RIGHT))
.highlight_symbol(">> ")
.highlight_style(Style::default().add_modifier(Modifier::BOLD))
.row_highlight_style(Style::default().add_modifier(Modifier::BOLD))
.column_highlight_style(Style::default().add_modifier(Modifier::ITALIC))
.cell_highlight_style(Style::default().add_modifier(Modifier::DIM))
.column_spacing(1);
f.render_stateful_widget(table, f.area(), &mut state);
})
@@ -678,6 +681,19 @@ fn widgets_table_can_have_elements_styled_individually() {
.add_modifier(Modifier::BOLD),
);
}
// Second column highlight style
for row in 2..=3 {
for col in 11..=16 {
expected[(col, row)].set_style(Style::default().add_modifier(Modifier::ITALIC));
}
}
// First row, second column highlight style (cell highlight)
for col in 11..=16 {
expected[(col, 2)].set_style(Style::default().add_modifier(Modifier::DIM));
}
// Second row:
// 1. row color
for col in 1..=28 {