Compare commits

..

30 Commits

Author SHA1 Message Date
Josh McKinney
60bd7f4814 chore(deps): use instabilty instead of stabilty (#1208)
https://github.com/ratatui-org/instability
2024-06-27 08:44:52 -07:00
leohscl
55e0880d2f docs(block): update block documentation (#1206)
Update block documentation with constructor methods and setter methods
in the main doc comment Added an example for using it to surround
widgets

Fixes: https://github.com/ratatui-org/ratatui/issues/914
2024-06-27 04:13:47 -07:00
Josh McKinney
3e7458fdb8 chore(github): add forums and faqs to the issue template (#1201) 2024-06-25 23:13:43 +03:00
tranzystorekk
32a0b26525 refactor: simplify WordWrapper implementation (#1193) 2024-06-25 12:14:32 -07:00
Robert Soane
36d49e549b feat(table): select first, last, etc to table state (#1198)
Add select_previous, select_next, select_first & select_last to
TableState

Used equivalent API as in ListState
2024-06-25 10:45:59 -07:00
Orhun Parmaksız
0a18dcb329 chore(release): prepare for 0.27.0 (#1196)
🧀

Highlights: https://github.com/ratatui-org/ratatui-website/pull/644
2024-06-24 13:59:16 +03:00
Orhun Parmaksız
7ef2daee06 feat(text): support constructing Line and Text from usize (#1167)
Now you can create `Line` and `Text` from numbers like so:

```rust
let line = Line::from(42);
let text = Text::from(666);
```

(I was doing little testing for my TUI app and saw that this isn't
supported - then I was like WHA and decided to make it happen ™️)
2024-06-24 13:23:33 +03:00
Josh McKinney
46977d8851 feat(list)!: add list navigation methods (first, last, previous, next) (#1159)
Also cleans up the list example significantly (see also
<https://github.com/ratatui-org/ratatui/issues/1157>)
    
Fixes: <https://github.com/ratatui-org/ratatui/pull/1159>
    
BREAKING CHANGE: The `List` widget now clamps the selected index to the
bounds of the list when navigating with `first`, `last`, `previous`, and
`next`, as well as when setting the index directly with `select`.
2024-06-24 11:37:22 +03:00
Orhun Parmaksız
38bb196404 docs(breaking-changes): mention LineGauge::gauge_style (#1194)
see #565
2024-06-24 11:27:22 +03:00
Orhun Parmaksız
1908b06b4a docs(borders): add missing closing code blocks (#1195) 2024-06-24 11:27:14 +03:00
Josh McKinney
3f2f2cd6ab feat(docs): add tracing example (#1192)
Add an example that demonstrates logging to a file for:

<https://forum.ratatui.rs/t/how-do-you-println-debug-your-tui-programs/66>

```shell
cargo run --example tracing
RUST_LOG=trace cargo run --example=tracing
cat tracing.log
```

![Made with VHS](https://vhs.charm.sh/vhs-21jgJCedh2YnFDONw0JW7l.gif)
2024-06-19 17:29:19 -07:00
Josh McKinney
efa965e1e8 fix(line): remove newlines when converting strings to Lines (#1191)
`Line::from("a\nb")` now returns a line with two `Span`s instead of 1
Fixes: https://github.com/ratatui-org/ratatui/issues/1111
2024-06-18 21:35:03 -07:00
Josh McKinney
127d706ee4 fix(table): ensure render offset without selection properly (#1187)
Fixes: <https://github.com/ratatui-org/ratatui/issues/1179>
2024-06-18 03:55:24 -07:00
Josh McKinney
1365620606 feat(borders): Add FULL and EMPTY border sets (#1182)
`border::FULL` uses a full block symbol, while `border::EMPTY` uses an
empty space. This is useful for when you need to allocate space for the
border and apply the border style to a block without actually drawing a
border. This makes it possible to style the entire title area or a block
rather than just the title content.

```rust
use ratatui::{symbols::border, widgets::Block};
let block = Block::bordered().title("Title").border_set(border::FULL);
let block = Block::bordered().title("Title").border_set(border::EMPTY);
```
2024-06-17 17:59:24 -07:00
Josh McKinney
cd64367e24 chore(symbols): add tests for line symbols (#1186) 2024-06-17 15:11:42 -07:00
Josh McKinney
07efde5233 docs(examples): add hyperlink example (#1063) 2024-06-17 15:11:02 -07:00
Josh McKinney
308c1df649 docs(readme): add links to forum (#1188) 2024-06-17 17:53:32 +03:00
thscharler
4bfdc15b80 fix: render of &str and String doesn't respect area.width (#1177) 2024-06-16 00:50:24 +02:00
EdJoPaTo
7d175f85c1 refactor(lint): fix new lint warnings (#1178) 2024-06-14 11:26:57 +02:00
Josh McKinney
d370aa75af fix(span): ensure that zero-width characters are rendered correctly (#1165) 2024-06-10 11:51:54 -07:00
Josh McKinney
7fdccafd52 docs(examples): add vhs tapes for constraint-explorer and minimal examples (#1164) 2024-06-08 19:28:07 +03:00
Josh McKinney
10d778866e feat(style): add conversions from the palette crate colors (#1172)
This is behind the "palette" feature flag.

```rust
use palette::{LinSrgb, Srgb};
use ratatui::style::Color;

let color = Color::from(Srgb::new(1.0f32, 0.0, 0.0));
let color = Color::from(LinSrgb::new(1.0f32, 0.0, 0.0));
```
2024-06-07 22:31:46 -07:00
Josh McKinney
e6871b9e21 fix: avoid unicode-width breaking change in tests (#1171)
unicode-width 0.1.13 changed the width of \u{1} from 0 to 1.
Our tests assumed that \u{1} had a width of 0, so this change replaces
the \u{1} character with \u{200B} (zero width space) in the tests.

Upstream issue (closed as won't fix):
https://github.com/unicode-rs/unicode-width/issues/55
2024-06-06 23:06:14 -07:00
Josh McKinney
4f307e69db docs(examples): simplify paragraph example (#1169)
Related: https://github.com/ratatui-org/ratatui/issues/1157
2024-06-05 17:41:46 -07:00
Josh McKinney
7f3efb02e6 fix: pin unicode-width crate to 0.1.13 (#1170)
semver breaking change in 0.1.13
<https://github.com/unicode-rs/unicode-width/issues/55>

<!-- Please read CONTRIBUTING.md before submitting any pull request. -->
2024-06-05 17:26:45 -07:00
Josh McKinney
7b45f74b71 chore(prelude)!: add / remove items (#1149)
his PR removes the items from the prelude that don't form a coherent
common vocabulary and adds the missing items that do.

Based on a comment at
<https://www.reddit.com/r/rust/comments/1cle18j/comment/l2uuuh7/>

BREAKING CHANGE:
The following items have been removed from the prelude:
- `style::Styled` - this trait is useful for widgets that want to
  support the Stylize trait, but it adds complexity as widgets have two
  `style` methods and a `set_style` method.
- `symbols::Marker` - this item is used by code that needs to draw to
  the `Canvas` widget, but it's not a common item that would be used by
  most users of the library.
- `terminal::{CompletedFrame, TerminalOptions, Viewport}` - these items
  are rarely used by code that needs to interact with the terminal, and
  they're generally only ever used once in any app.

The following items have been added to the prelude:
- `layout::{Position, Size}` - these items are used by code that needs
  to interact with the layout system. These are newer items that were
  added in the last few releases, which should be used more liberally.
2024-06-04 20:59:51 -07:00
Josh McKinney
1520ed9d10 feat(layout): impl Display for Position and Size (#1162) 2024-06-03 18:54:50 -07:00
dependabot[bot]
2a74f9d8c1 chore(deps): update rstest requirement from 0.19.0 to 0.21.0 (#1163)
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.21.0</h2>
<p>Use <code>crate-name</code> feature to enable the crate rename
support (enabled by default)</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.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>
<ul>
<li>Defined <code>rust-version</code> for each crate (see <a
href="https://redirect.github.com/la10736/rstest/issues/227">#227</a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li>
<p><code>#[once]</code> fixtures now require the returned type to be
<a
href="https://doc.rust-lang.org/std/marker/trait.Sync.html"><code>Sync</code></a>
to prevent UB
when tests are executed in parallel. (see <a
href="https://redirect.github.com/la10736/rstest/issues/235">#235</a>
for more details)</p>
</li>
<li>
<p><code>#[future(awt)]</code> and <code>#[awt]</code> now properly
handle mutable (<code>mut</code>) parameters by treating futures as
immutable and
treating the awaited rebinding as mutable.</p>
</li>
</ul>
<h2>[0.18.2] 2023/8/13</h2>
<h3>Changed</h3>
<ul>
<li>Now <code>#[files]</code> accept also parent folders (see <a
href="https://redirect.github.com/la10736/rstest/issues/205">#205</a>
for more details).</li>
</ul>
<h2>[0.18.1] 2023/7/5</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="38da6bfb96"><code>38da6bf</code></a>
Prepare 0.21.0 Release</li>
<li><a
href="ca69788392"><code>ca69788</code></a>
bump version rstest_test to 0.13.0</li>
<li><a
href="b6b43c6740"><code>b6b43c6</code></a>
Clean chackoutlist</li>
<li><a
href="fef4f7b4f4"><code>fef4f7b</code></a>
Implemented Opt-in crate-name support Fix <a
href="https://redirect.github.com/la10736/rstest/issues/258">#258</a></li>
<li><a
href="236be92a8a"><code>236be92</code></a>
Build should use build tests target</li>
<li><a
href="8fde5be94f"><code>8fde5be</code></a>
Prepare next changelog</li>
<li><a
href="f29e6346fe"><code>f29e634</code></a>
Dependency should have a n explicit version to be published</li>
<li><a
href="e27ad2a4db"><code>e27ad2a</code></a>
Removed empty section</li>
<li><a
href="3867794483"><code>3867794</code></a>
Fixed docs</li>
<li><a
href="b90fb8e092"><code>b90fb8e</code></a>
Fix checkout list</li>
<li>Additional commits viewable in <a
href="https://github.com/la10736/rstest/compare/v0.19.0...v0.21.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-06-03 19:38:06 +03:00
dependabot[bot]
d7ed6c8bad chore(deps)!: update termion requirement from 3.0 to 4.0 (#1106)
Updates the requirements on termion to permit the latest version.

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>

BREAKING CHANGE: Changelog:
<https://gitlab.redox-os.org/redox-os/termion/-/blob/master/CHANGELOG.md>
A change is only necessary if you were matching on all variants of the
`MouseEvent` enum without a
wildcard. In this case, you need to either handle the two new variants,
`MouseLeft` and
`MouseRight`, or add a wildcard.

Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2024-06-02 18:30:46 -07:00
dependabot[bot]
313135c68e chore(deps): update itertools requirement from 0.12 to 0.13 (#1120)
Updates the requirements on
[itertools](https://github.com/rust-itertools/itertools) to permit the
latest version.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/rust-itertools/itertools/blob/master/CHANGELOG.md">itertools's
changelog</a>.</em></p>
<blockquote>
<h2>0.13.0</h2>
<h3>Breaking</h3>
<ul>
<li>Removed implementation of <code>DoubleEndedIterator</code> for
<code>ConsTuples</code> (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/853">#853</a>)</li>
<li>Made <code>MultiProduct</code> fused and fixed on an empty iterator
(<a
href="https://redirect.github.com/rust-itertools/itertools/issues/835">#835</a>,
<a
href="https://redirect.github.com/rust-itertools/itertools/issues/834">#834</a>)</li>
<li>Changed <code>iproduct!</code> to return tuples for maxi one
iterator too (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/870">#870</a>)</li>
<li>Changed <code>PutBack::put_back</code> to return the old value (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/880">#880</a>)</li>
<li>Removed deprecated <code>repeat_call, Itertools::{foreach, step,
map_results, fold_results}</code> (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/878">#878</a>)</li>
<li>Removed <code>TakeWhileInclusive::new</code> (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/912">#912</a>)</li>
</ul>
<h3>Added</h3>
<ul>
<li>Added <code>Itertools::{smallest_by, smallest_by_key, largest,
largest_by, largest_by_key}</code> (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/654">#654</a>,
<a
href="https://redirect.github.com/rust-itertools/itertools/issues/885">#885</a>)</li>
<li>Added <code>Itertools::tail</code> (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/899">#899</a>)</li>
<li>Implemented <code>DoubleEndedIterator</code> for
<code>ProcessResults</code> (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/910">#910</a>)</li>
<li>Implemented <code>Debug</code> for <code>FormatWith</code> (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/931">#931</a>)</li>
<li>Added <code>Itertools::get</code> (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/891">#891</a>)</li>
</ul>
<h3>Changed</h3>
<ul>
<li>Deprecated <code>Itertools::group_by</code> (renamed
<code>chunk_by</code>) (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/866">#866</a>,
<a
href="https://redirect.github.com/rust-itertools/itertools/issues/879">#879</a>)</li>
<li>Deprecated <code>unfold</code> (use <code>std::iter::from_fn</code>
instead) (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/871">#871</a>)</li>
<li>Optimized <code>GroupingMapBy</code> (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/873">#873</a>,
<a
href="https://redirect.github.com/rust-itertools/itertools/issues/876">#876</a>)</li>
<li>Relaxed <code>Fn</code> bounds to <code>FnMut</code> in
<code>diff_with, Itertools::into_group_map_by</code> (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/886">#886</a>)</li>
<li>Relaxed <code>Debug/Clone</code> bounds for <code>MapInto</code> (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/889">#889</a>)</li>
<li>Documented the <code>use_alloc</code> feature (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/887">#887</a>)</li>
<li>Optimized <code>Itertools::set_from</code> (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/888">#888</a>)</li>
<li>Removed badges in <code>README.md</code> (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/890">#890</a>)</li>
<li>Added &quot;no-std&quot; categories in <code>Cargo.toml</code> (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/894">#894</a>)</li>
<li>Fixed <code>Itertools::k_smallest</code> on short unfused iterators
(<a
href="https://redirect.github.com/rust-itertools/itertools/issues/900">#900</a>)</li>
<li>Deprecated <code>Itertools::tree_fold1</code> (renamed
<code>tree_reduce</code>) (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/895">#895</a>)</li>
<li>Deprecated <code>GroupingMap::fold_first</code> (renamed
<code>reduce</code>) (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/902">#902</a>)</li>
<li>Fixed <code>Itertools::k_smallest(0)</code> to consume the iterator,
optimized <code>Itertools::k_smallest(1)</code> (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/909">#909</a>)</li>
<li>Specialized <code>Combinations::nth</code> (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/914">#914</a>)</li>
<li>Specialized <code>MergeBy::fold</code> (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/920">#920</a>)</li>
<li>Specialized <code>CombinationsWithReplacement::nth</code> (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/923">#923</a>)</li>
<li>Specialized <code>FlattenOk::{fold, rfold}</code> (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/927">#927</a>)</li>
<li>Specialized <code>Powerset::nth</code> (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/924">#924</a>)</li>
<li>Documentation fixes (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/882">#882</a>,
<a
href="https://redirect.github.com/rust-itertools/itertools/issues/936">#936</a>)</li>
<li>Fixed <code>assert_equal</code> for iterators longer than
<code>i32::MAX</code> (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/932">#932</a>)</li>
<li>Updated the <code>must_use</code> message of non-lazy
<code>KMergeBy</code> and <code>TupleCombinations</code> (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/939">#939</a>)</li>
</ul>
<h3>Notable Internal Changes</h3>
<ul>
<li>Tested iterator laziness (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/792">#792</a>)</li>
<li>Created <code>CONTRIBUTING.md</code> (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/767">#767</a>)</li>
</ul>
<h2>0.12.1</h2>
<h3>Added</h3>
<ul>
<li>Documented iteration order guarantee for
<code>Itertools::[tuple_]combinations</code> (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/822">#822</a>)</li>
<li>Documented possible panic in <code>iterate</code> (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/842">#842</a>)</li>
<li>Implemented <code>Clone</code> and <code>Debug</code> for
<code>Diff</code> (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/845">#845</a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="d5084d15e9"><code>d5084d1</code></a>
Prepare v0.13.0 release (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/937">#937</a>)</li>
<li><a
href="d7c99d55da"><code>d7c99d5</code></a>
<code>TupleCombinations</code> is not lazy but must be used
nonetheless</li>
<li><a
href="074c7fcc07"><code>074c7fc</code></a>
<code>KMergeBy</code> is not lazy but must be used nonetheless</li>
<li><a
href="2ad9e07ae8"><code>2ad9e07</code></a>
<code>assert_equal</code>: fix
<code>clippy::default_numeric_fallback</code></li>
<li><a
href="0d4efc8432"><code>0d4efc8</code></a>
Remove free function <code>get</code></li>
<li><a
href="05cc0ee256"><code>05cc0ee</code></a>
<code>get(s..=usize::MAX)</code> should be fine when <code>s !=
0</code></li>
<li><a
href="3c16f14baa"><code>3c16f14</code></a>
<code>get</code>: when is it ESI and/or DEI</li>
<li><a
href="4dd6ba0e7c"><code>4dd6ba0</code></a>
<code>get</code>: panics if the range includes
<code>usize::MAX</code></li>
<li><a
href="7a9ce56fc5"><code>7a9ce56</code></a>
<code>get(r: Range)</code> as <code>Skip\&lt;Take&gt;</code></li>
<li><a
href="f676f2f964"><code>f676f2f</code></a>
Remove the unspecified check about
<code>.get(exhausted_range_inclusive)</code></li>
<li>Additional commits viewable in <a
href="https://github.com/rust-itertools/itertools/compare/v0.12.0...v0.13.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 will merge this PR once CI passes on it, as requested by
@joshka.

[//]: # (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>
Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2024-06-02 18:18:36 -07:00
58 changed files with 3159 additions and 1132 deletions

View File

@@ -1,5 +1,11 @@
blank_issues_enabled: false
contact_links:
- name: Frequently Asked Questions
url: https://ratatui.rs/faq/
about: Check the website FAQ section to see if your question has already been answered
- name: Ratatui Forum
url: https://forum.ratatui.rs
about: Ask questions about ratatui on our Forum
- name: Discord Chat
url: https://discord.gg/pMCEU9hNEj
about: Ask questions about ratatui on Discord

View File

@@ -10,11 +10,15 @@ GitHub with a [breaking change] label.
This is a quick summary of the sections below:
- [Unreleased](#unreleased)
- [v0.27.0](#v0270)
- List no clamps the selected index to list
- Prelude items added / removed
- 'termion' updated to 4.0
- `Rect::inner` takes `Margin` directly instead of reference
- `Buffer::filled` takes `Cell` directly instead of reference
- `Stylize::bg()` now accepts `Into<Color>`
- Removed deprecated `List::start_corner`
- `LineGauge::gauge_style` is deprecated
- [v0.26.0](#v0260)
- `Flex::Start` is the new default flex mode for `Layout`
- `patch_style` & `reset_style` now consume and return `Self`
@@ -52,7 +56,72 @@ This is a quick summary of the sections below:
- MSRV is now 1.63.0
- `List` no longer ignores empty strings
## Unreleased
## [v0.27.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.27.0)
### List no clamps the selected index to list ([#1159])
[#1149]: https://github.com/ratatui-org/ratatui/pull/1149
The `List` widget now clamps the selected index to the bounds of the list when navigating with
`first`, `last`, `previous`, and `next`, as well as when setting the index directly with `select`.
Previously selecting an index past the end of the list would show treat the list as having a
selection which was not visible. Now the last item in the list will be selected instead.
### Prelude items added / removed ([#1149])
The following items have been removed from the prelude:
- `style::Styled` - this trait is useful for widgets that want to
support the Stylize trait, but it adds complexity as widgets have two
`style` methods and a `set_style` method.
- `symbols::Marker` - this item is used by code that needs to draw to
the `Canvas` widget, but it's not a common item that would be used by
most users of the library.
- `terminal::{CompletedFrame, TerminalOptions, Viewport}` - these items
are rarely used by code that needs to interact with the terminal, and
they're generally only ever used once in any app.
The following items have been added to the prelude:
- `layout::{Position, Size}` - these items are used by code that needs
to interact with the layout system. These are newer items that were
added in the last few releases, which should be used more liberally.
This may cause conflicts for types defined elsewhere with a similar
name.
To update your app:
```diff
// if your app uses Styled::style() or Styled::set_style():
-use ratatui::prelude::*;
+use ratatui::{prelude::*, style::Styled};
// if your app uses symbols::Marker:
-use ratatui::prelude::*;
+use ratatui::{prelude::*, symbols::Marker}
// if your app uses terminal::{CompletedFrame, TerminalOptions, Viewport}
-use ratatui::prelude::*;
+use ratatui::{prelude::*, terminal::{CompletedFrame, TerminalOptions, Viewport}};
// to disambiguate existing types named Position or Size:
- use some_crate::{Position, Size};
- let size: Size = ...;
- let position: Position = ...;
+ let size: some_crate::Size = ...;
+ let position: some_crate::Position = ...;
```
### Termion is updated to 4.0 [#1106]
Changelog: <https://gitlab.redox-os.org/redox-os/termion/-/blob/master/CHANGELOG.md>
A change is only necessary if you were matching on all variants of the `MouseEvent` enum without a
wildcard. In this case, you need to either handle the two new variants, `MouseLeft` and
`MouseRight`, or add a wildcard.
[#1106]: https://github.com/ratatui-org/ratatui/pull/1106
### `Rect::inner` takes `Margin` directly instead of reference ([#1008])
@@ -86,9 +155,9 @@ This is a quick summary of the sections below:
Previously, `Stylize::bg()` accepted `Color` but now accepts `Into<Color>`. This allows more
flexible types from calling scopes, though it can break some type inference in the calling scope.
### Remove deprecated `List::start_corner` and `layout::Corner` ([#757])
### Remove deprecated `List::start_corner` and `layout::Corner` ([#759])
[#757]: https://github.com/ratatui-org/ratatui/pull/757
[#759]: https://github.com/ratatui-org/ratatui/pull/759
`List::start_corner` was deprecated in v0.25. Use `List::direction` and `ListDirection` instead.
@@ -109,6 +178,19 @@ flexible types from calling scopes, though it can break some type inference in t
`layout::Corner` was removed entirely.
### `LineGauge::gauge_style` is deprecated ([#565])
[#565]: https://github.com/ratatui-org/ratatui/pull/1148
`LineGauge::gauge_style` is deprecated and replaced with `LineGauge::filled_style` and `LineGauge::unfilled_style`:
```diff
let gauge = LineGauge::default()
- .gauge_style(Style::default().fg(Color::Red).bg(Color::Blue)
+ .filled_style(Style::default().fg(Color::Green))
+ .unfilled_style(Style::default().fg(Color::White));
```
## [v0.26.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.26.0)
### `Flex::Start` is the new default flex mode for `Layout` ([#881])
@@ -194,8 +276,6 @@ The following example shows how to migrate for `Line`, but the same applies for
### Remove deprecated `Block::title_on_bottom` ([#757])
[#757]: https://github.com/ratatui-org/ratatui/pull/757
`Block::title_on_bottom` was deprecated in v0.22. Use `Block::title` and `Title::position` instead.
```diff
@@ -451,8 +531,8 @@ The MSRV of ratatui is now 1.67 due to an MSRV update in a dependency (`time`).
[#205]: https://github.com/ratatui-org/ratatui/issues/205
The `serde` representation of `bitflags` has changed. Any existing serialized types that have Borders or
Modifiers will need to be re-serialized. This is documented in the [`bitflags`
The `serde` representation of `bitflags` has changed. Any existing serialized types that have
Borders or Modifiers will need to be re-serialized. This is documented in the [`bitflags`
changelog](https://github.com/bitflags/bitflags/blob/main/CHANGELOG.md#200-rc2)..
## [v0.21.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.21.0)

View File

@@ -2,6 +2,379 @@
All notable changes to this project will be documented in this file.
## [0.27.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.27.0) - 2024-06-24
In this version, we have focused on enhancing usability and functionality with new features like
background styles for LineGauge, palette colors, and various other improvements including
improved performance. Also, we added brand new examples for tracing and creating hyperlinks!
**Release highlights**: <https://ratatui.rs/highlights/v027/>
⚠️ List of breaking changes can be found [here](https://github.com/ratatui-org/ratatui/blob/main/BREAKING-CHANGES.md).
### Features
- [eef1afe](https://github.com/ratatui-org/ratatui/commit/eef1afe9155089dca489a9159c368a5ac07e7585) _(linegauge)_ Allow LineGauge background styles by @nowNick in [#565](https://github.com/ratatui-org/ratatui/pull/565)
```text
This PR deprecates `gauge_style` in favor of `filled_style` and
`unfilled_style` which can have it's foreground and background styled.
`cargo run --example=line_gauge --features=crossterm`
```
https://github.com/ratatui-org/ratatui/assets/5149215/5fb2ce65-8607-478f-8be4-092e08612f5b
Implements:<https://github.com/ratatui-org/ratatui/issues/424>
- [1365620](https://github.com/ratatui-org/ratatui/commit/13656206064b53c7f86f179b570c7769399212a3) _(borders)_ Add FULL and EMPTY border sets by @joshka in [#1182](https://github.com/ratatui-org/ratatui/pull/1182)
`border::FULL` uses a full block symbol, while `border::EMPTY` uses an
empty space. This is useful for when you need to allocate space for the
border and apply the border style to a block without actually drawing a
border. This makes it possible to style the entire title area or a block
rather than just the title content.
```rust
use ratatui::{symbols::border, widgets::Block};
let block = Block::bordered().title("Title").border_set(border::FULL);
let block = Block::bordered().title("Title").border_set(border::EMPTY);
```
- [7a48c5b](https://github.com/ratatui-org/ratatui/commit/7a48c5b11b3d51b915ccc187d0499b6e0e88b89d) _(cell)_ Add EMPTY and (const) new method by @EdJoPaTo in [#1143](https://github.com/ratatui-org/ratatui/pull/1143)
```text
This simplifies calls to `Buffer::filled` in tests.
```
- [3f2f2cd](https://github.com/ratatui-org/ratatui/commit/3f2f2cd6abf67a04809ff314025a462a3c2e2446) _(docs)_ Add tracing example by @joshka in [#1192](https://github.com/ratatui-org/ratatui/pull/1192)
```text
Add an example that demonstrates logging to a file for:
```
<https://forum.ratatui.rs/t/how-do-you-println-debug-your-tui-programs/66>
```shell
cargo run --example tracing
RUST_LOG=trace cargo run --example=tracing
cat tracing.log
```
![Made with VHS](https://vhs.charm.sh/vhs-21jgJCedh2YnFDONw0JW7l.gif)
- [1520ed9](https://github.com/ratatui-org/ratatui/commit/1520ed9d106f99580a9e529212e43dac06a2f6d2) _(layout)_ Impl Display for Position and Size by @joshka in [#1162](https://github.com/ratatui-org/ratatui/pull/1162)
- [46977d8](https://github.com/ratatui-org/ratatui/commit/46977d88519d28ccac1c94e171af0c9cca071dbc) _(list)_ Add list navigation methods (first, last, previous, next) by @joshka in [#1159](https://github.com/ratatui-org/ratatui/pull/1159) [**breaking**]
```text
Also cleans up the list example significantly (see also
<https://github.com/ratatui-org/ratatui/issues/1157>)
```
Fixes:<https://github.com/ratatui-org/ratatui/pull/1159>
BREAKING CHANGE:The `List` widget now clamps the selected index to the
bounds of the list when navigating with `first`, `last`, `previous`, and
`next`, as well as when setting the index directly with `select`.
- [10d7788](https://github.com/ratatui-org/ratatui/commit/10d778866edea55207ff3f03d063c9fec619b9c9) _(style)_ Add conversions from the palette crate colors by @joshka in [#1172](https://github.com/ratatui-org/ratatui/pull/1172)
````text
This is behind the "palette" feature flag.
```rust
use palette::{LinSrgb, Srgb};
use ratatui::style::Color;
let color = Color::from(Srgb::new(1.0f32, 0.0, 0.0));
let color = Color::from(LinSrgb::new(1.0f32, 0.0, 0.0));
```
````
- [7ef2dae](https://github.com/ratatui-org/ratatui/commit/7ef2daee060a7fe964a8de64eafcb6062228e035) _(text)_ support conversion from Display to Span, Line and Text by @orhun in [#1167](https://github.com/ratatui-org/ratatui/pull/1167)
````text
Now you can create `Line` and `Text` from numbers like so:
```rust
let line = 42.to_line();
let text = 666.to_text();
```
````
- [74a32af](https://github.com/ratatui-org/ratatui/commit/74a32afbaef8851f9462b27094d88d518e56addf) _(uncategorized)_ Re-export backends from the ratatui crate by @joshka in [#1151](https://github.com/ratatui-org/ratatui/pull/1151)
```text
`crossterm`, `termion`, and `termwiz` can now be accessed as
`ratatui::{crossterm, termion, termwiz}` respectively. This makes it
possible to just add the Ratatui crate as a dependency and use the
backend of choice without having to add the backend crates as
dependencies.
To update existing code, replace all instances of `crossterm::` with
`ratatui::crossterm::`, `termion::` with `ratatui::termion::`, and
`termwiz::` with `ratatui::termwiz::`.
```
- [3594180](https://github.com/ratatui-org/ratatui/commit/35941809e11ab43309dd83a8f67bb375f5e7ff2b) _(uncategorized)_ Make Stylize's `.bg(color)` generic by @kdheepak in [#1103](https://github.com/ratatui-org/ratatui/pull/1103) [**breaking**]
- [0b5fd6b](https://github.com/ratatui-org/ratatui/commit/0b5fd6bf8eb64662df96900faea3608d4cbb3984) _(uncategorized)_ Add writer() and writer_mut() to termion and crossterm backends by @enricozb in [#991](https://github.com/ratatui-org/ratatui/pull/991)
```text
It is sometimes useful to obtain access to the writer if we want to see
what has been written so far. For example, when using &mut [u8] as a
writer.
```
### Bug Fixes
- [efa965e](https://github.com/ratatui-org/ratatui/commit/efa965e1e806c60cb1bdb2d1715f960db0857704) _(line)_ Remove newlines when converting strings to Lines by @joshka in [#1191](https://github.com/ratatui-org/ratatui/pull/1191)
`Line::from("a\nb")` now returns a line with two `Span`s instead of 1
Fixes:https://github.com/ratatui-org/ratatui/issues/1111
- [d370aa7](https://github.com/ratatui-org/ratatui/commit/d370aa75af99da3e0c41ceb28e2d02ee81cd2538) _(span)_ Ensure that zero-width characters are rendered correctly by @joshka in [#1165](https://github.com/ratatui-org/ratatui/pull/1165)
- [127d706](https://github.com/ratatui-org/ratatui/commit/127d706ee4876a58230f42f4a730b18671eae167) _(table)_ Ensure render offset without selection properly by @joshka in [#1187](https://github.com/ratatui-org/ratatui/pull/1187)
Fixes:<https://github.com/ratatui-org/ratatui/issues/1179>
- [4bfdc15](https://github.com/ratatui-org/ratatui/commit/4bfdc15b80ba14489d359ab1f88564c3bd016c19) _(uncategorized)_ Render of &str and String doesn't respect area.width by @thscharler in [#1177](https://github.com/ratatui-org/ratatui/pull/1177)
- [e6871b9](https://github.com/ratatui-org/ratatui/commit/e6871b9e21c25acf1e203f4860198c37aa9429a1) _(uncategorized)_ Avoid unicode-width breaking change in tests by @joshka in [#1171](https://github.com/ratatui-org/ratatui/pull/1171)
```text
unicode-width 0.1.13 changed the width of \u{1} from 0 to 1.
Our tests assumed that \u{1} had a width of 0, so this change replaces
the \u{1} character with \u{200B} (zero width space) in the tests.
Upstream issue (closed as won't fix):
https://github.com/unicode-rs/unicode-width/issues/55
```
- [7f3efb0](https://github.com/ratatui-org/ratatui/commit/7f3efb02e6f846fc72079f0921abd2cee09d2d83) _(uncategorized)_ Pin unicode-width crate to 0.1.13 by @joshka in [#1170](https://github.com/ratatui-org/ratatui/pull/1170)
```text
semver breaking change in 0.1.13
<https://github.com/unicode-rs/unicode-width/issues/55>
<!-- Please read CONTRIBUTING.md before submitting any pull request. -->
```
- [42cda6d](https://github.com/ratatui-org/ratatui/commit/42cda6d28706bf83308787ca784f374f6c286a02) _(uncategorized)_ Prevent panic from string_slice by @EdJoPaTo in [#1140](https://github.com/ratatui-org/ratatui/pull/1140)
<https://rust-lang.github.io/rust-clippy/master/index.html#string_slice>
### Refactor
- [73fd367](https://github.com/ratatui-org/ratatui/commit/73fd367a740924ce80ef7a0cd13a66b5094f7a54) _(block)_ Group builder pattern methods by @EdJoPaTo in [#1134](https://github.com/ratatui-org/ratatui/pull/1134)
- [257db62](https://github.com/ratatui-org/ratatui/commit/257db6257f392a07ee238b439344d91566beb740) _(cell)_ Must_use and simplify style() by @EdJoPaTo in [#1124](https://github.com/ratatui-org/ratatui/pull/1124)
```text
<!-- Please read CONTRIBUTING.md before submitting any pull request. -->
```
- [bf20369](https://github.com/ratatui-org/ratatui/commit/bf2036987f04d83f4f2b8338fab1b4fd7f4cc81d) _(cell)_ Reset instead of applying default by @EdJoPaTo in [#1127](https://github.com/ratatui-org/ratatui/pull/1127)
```text
Using reset is clearer to me what actually happens. On the other case a
struct is created to override the old one completely which basically
does the same in a less clear way.
```
- [7d175f8](https://github.com/ratatui-org/ratatui/commit/7d175f85c1905c08adf547dd37cc89c63039f480) _(lint)_ Fix new lint warnings by @EdJoPaTo in [#1178](https://github.com/ratatui-org/ratatui/pull/1178)
- [cf67ed9](https://github.com/ratatui-org/ratatui/commit/cf67ed9b884347cef034b09e0e9f9d4aff74ab0a) _(lint)_ Use clippy::or_fun_call by @EdJoPaTo in [#1138](https://github.com/ratatui-org/ratatui/pull/1138)
<https://rust-lang.github.io/rust-clippy/master/index.html#or_fun_call>
- [4770e71](https://github.com/ratatui-org/ratatui/commit/4770e715819475cdca2f2ccdbac00cba203cd6d2) _(list)_ Remove deprecated `start_corner` and `Corner` by @Valentin271 in [#759](https://github.com/ratatui-org/ratatui/pull/759) [**breaking**]
`List::start_corner` was deprecated in v0.25. Use `List::direction` and
`ListDirection` instead.
```diff
- list.start_corner(Corner::TopLeft);
- list.start_corner(Corner::TopRight);
// This is not an error, BottomRight rendered top to bottom previously
- list.start_corner(Corner::BottomRight);
// all becomes
+ list.direction(ListDirection::TopToBottom);
```
```diff
- list.start_corner(Corner::BottomLeft);
// becomes
+ list.direction(ListDirection::BottomToTop);
```
`layout::Corner` is removed entirely.
- [4f77910](https://github.com/ratatui-org/ratatui/commit/4f7791079edd16b54dc8cdfc95bb72b282a09576) _(padding)_ Add Padding::ZERO as a constant by @EdJoPaTo in [#1133](https://github.com/ratatui-org/ratatui/pull/1133)
```text
Deprecate Padding::zero()
```
- [8061813](https://github.com/ratatui-org/ratatui/commit/8061813f324c08e11196e62fca22c2f6b9216b7e) _(uncategorized)_ Expand glob imports by @joshka in [#1152](https://github.com/ratatui-org/ratatui/pull/1152)
```text
Consensus is that explicit imports make it easier to understand the
example code. This commit removes the prelude import from all examples
and replaces it with the necessary imports, and expands other glob
imports (widget::*, Constraint::*, KeyCode::*, etc.) everywhere else.
Prelude glob imports not in examples are not covered by this PR.
See https://github.com/ratatui-org/ratatui/issues/1150 for more details.
```
- [d929971](https://github.com/ratatui-org/ratatui/commit/d92997105bde15a1fd43829466ec8cc46bffe121) _(uncategorized)_ Dont manually impl Default for defaults by @EdJoPaTo in [#1142](https://github.com/ratatui-org/ratatui/pull/1142)
```text
Replace `impl Default` by `#[derive(Default)]` when its implementation
equals.
```
- [8a60a56](https://github.com/ratatui-org/ratatui/commit/8a60a561c95691912cbf41d55866abafcba0127d) _(uncategorized)_ Needless_pass_by_ref_mut by @EdJoPaTo in [#1137](https://github.com/ratatui-org/ratatui/pull/1137)
<https://rust-lang.github.io/rust-clippy/master/index.html#needless_pass_by_ref_mut>
- [1de9a82](https://github.com/ratatui-org/ratatui/commit/1de9a82b7a871a83995d224785cae139c6f4787b) _(uncategorized)_ Simplify if let by @EdJoPaTo in [#1135](https://github.com/ratatui-org/ratatui/pull/1135)
```text
While looking through lints
[`clippy::option_if_let_else`](https://rust-lang.github.io/rust-clippy/master/index.html#option_if_let_else)
found these. Other findings are more complex so I skipped them.
```
### Documentation
- [1908b06](https://github.com/ratatui-org/ratatui/commit/1908b06b4a497ff1cfb2c8d8c165d2a241ee1864) _(borders)_ Add missing closing code blocks by @orhun in [#1195](https://github.com/ratatui-org/ratatui/pull/1195)
- [38bb196](https://github.com/ratatui-org/ratatui/commit/38bb19640449c7a3eee3a2fba6450071395e5e06) _(breaking-changes)_ Mention `LineGauge::gauge_style` by @orhun in [#1194](https://github.com/ratatui-org/ratatui/pull/1194)
see #565
- [07efde5](https://github.com/ratatui-org/ratatui/commit/07efde5233752e1bcb7ae94a91b9e36b7fadb01b) _(examples)_ Add hyperlink example by @joshka in [#1063](https://github.com/ratatui-org/ratatui/pull/1063)
- [7fdccaf](https://github.com/ratatui-org/ratatui/commit/7fdccafd52f4ddad1a3c9dda59fada59af21ecfa) _(examples)_ Add vhs tapes for constraint-explorer and minimal examples by @joshka in [#1164](https://github.com/ratatui-org/ratatui/pull/1164)
- [4f307e6](https://github.com/ratatui-org/ratatui/commit/4f307e69db058891675d0f12d75ef49006c511d6) _(examples)_ Simplify paragraph example by @joshka in [#1169](https://github.com/ratatui-org/ratatui/pull/1169)
Related:https://github.com/ratatui-org/ratatui/issues/1157
- [f429f68](https://github.com/ratatui-org/ratatui/commit/f429f688da536a52266144e63a1a7897ec6b7f26) _(examples)_ Remove lifetimes from the List example by @matta in [#1132](https://github.com/ratatui-org/ratatui/pull/1132)
```text
Simplify the List example by removing lifetimes not strictly necessary
to demonstrate how Ratatui lists work. Instead, the sample strings are
copied into each `TodoItem`. To further simplify, I changed the code to
use a new TodoItem::new function, rather than an implementation of the
`From` trait.
```
- [308c1df](https://github.com/ratatui-org/ratatui/commit/308c1df6495ee4373f808007a1566ca7e9279933) _(readme)_ Add links to forum by @joshka in [#1188](https://github.com/ratatui-org/ratatui/pull/1188)
- [2f8a936](https://github.com/ratatui-org/ratatui/commit/2f8a9363fc6c54fe2b10792c9f57fbb40b06bc0f) _(uncategorized)_ Fix links on docs.rs by @EdJoPaTo in [#1144](https://github.com/ratatui-org/ratatui/pull/1144)
```text
This also results in a more readable Cargo.toml as the locations of the
things are more obvious now.
Includes rewording of the underline-color feature.
Logs of the errors: https://docs.rs/crate/ratatui/0.26.3/builds/1224962
Also see #989
```
### Performance
- [4ce67fc](https://github.com/ratatui-org/ratatui/commit/4ce67fc84e3bc472e9ae97aece85f8ffae091834) _(buffer)_ Filled moves the cell to be filled by @EdJoPaTo in [#1148](https://github.com/ratatui-org/ratatui/pull/1148) [**breaking**]
- [8b447ec](https://github.com/ratatui-org/ratatui/commit/8b447ec4d6276c3110285e663417487ff18dafc1) _(rect)_ `Rect::inner` takes `Margin` directly instead of reference by @EdJoPaTo in [#1008](https://github.com/ratatui-org/ratatui/pull/1008) [**breaking**]
BREAKING CHANGE:Margin needs to be passed without reference now.
```diff
-let area = area.inner(&Margin {
+let area = area.inner(Margin {
vertical: 0,
horizontal: 2,
});
```
### Styling
- [df4b706](https://github.com/ratatui-org/ratatui/commit/df4b706674de806bdf2a1fb8c04d0654b6b0b891) _(uncategorized)_ Enable more rustfmt settings by @EdJoPaTo in [#1125](https://github.com/ratatui-org/ratatui/pull/1125)
### Testing
- [d6587bc](https://github.com/ratatui-org/ratatui/commit/d6587bc6b0db955aeac6af167e1b8ef81a3afcc9) _(style)_ Use rstest by @EdJoPaTo in [#1136](https://github.com/ratatui-org/ratatui/pull/1136)
```text
<!-- Please read CONTRIBUTING.md before submitting any pull request. -->
```
### Miscellaneous Tasks
- [7b45f74](https://github.com/ratatui-org/ratatui/commit/7b45f74b719ff18329ddbf9f05a9ac53bf06f71d) _(prelude)_ Add / remove items by @joshka in [#1149](https://github.com/ratatui-org/ratatui/pull/1149) [**breaking**]
```text
his PR removes the items from the prelude that don't form a coherent
common vocabulary and adds the missing items that do.
Based on a comment at
<https://www.reddit.com/r/rust/comments/1cle18j/comment/l2uuuh7/>
```
BREAKING CHANGE:The following items have been removed from the prelude:
- `style::Styled` - this trait is useful for widgets that want to
support the Stylize trait, but it adds complexity as widgets have two
`style` methods and a `set_style` method.
- `symbols::Marker` - this item is used by code that needs to draw to
the `Canvas` widget, but it's not a common item that would be used by
most users of the library.
- `terminal::{CompletedFrame, TerminalOptions, Viewport}` - these items
are rarely used by code that needs to interact with the terminal, and
they're generally only ever used once in any app.
The following items have been added to the prelude:
- `layout::{Position, Size}` - these items are used by code that needs
to interact with the layout system. These are newer items that were
added in the last few releases, which should be used more liberally.
- [cd64367](https://github.com/ratatui-org/ratatui/commit/cd64367e244a1588206f653fd79678ce62a86a2f) _(symbols)_ Add tests for line symbols by @joshka in [#1186](https://github.com/ratatui-org/ratatui/pull/1186)
- [8cfc316](https://github.com/ratatui-org/ratatui/commit/8cfc316bccb48e88660d14cd18c0df2264c4d6ce) _(uncategorized)_ Alphabetize examples in Cargo.toml by @joshka in [#1145](https://github.com/ratatui-org/ratatui/pull/1145)
### Build
- [70df102](https://github.com/ratatui-org/ratatui/commit/70df102de0154cdfbd6508659cf6ed649f820bc8) _(bench)_ Improve benchmark consistency by @EdJoPaTo in [#1126](https://github.com/ratatui-org/ratatui/pull/1126)
```text
Codegen units are optimized on their own. Per default bench / release
have 16 codegen units. What ends up in a codeget unit is rather random
and can influence a benchmark result as a code change can move stuff
into a different codegen unit → prevent / allow LLVM optimizations
unrelated to the actual change.
More details: https://doc.rust-lang.org/cargo/reference/profiles.html
```
### New Contributors
- @thscharler made their first contribution in [#1177](https://github.com/ratatui-org/ratatui/pull/1177)
- @matta made their first contribution in [#1132](https://github.com/ratatui-org/ratatui/pull/1132)
- @nowNick made their first contribution in [#565](https://github.com/ratatui-org/ratatui/pull/565)
- @enricozb made their first contribution in [#991](https://github.com/ratatui-org/ratatui/pull/991)
**Full Changelog**: https://github.com/ratatui-org/ratatui/compare/v0.26.3...v0.27.0
## [0.26.3](https://github.com/ratatui-org/ratatui/releases/tag/v0.26.3) - 2024-05-19
We are happy to announce a brand new [**Ratatui Forum**](https://forum.ratatui.rs) 🐭 for Rust & TUI enthusiasts.

View File

@@ -1,6 +1,6 @@
[package]
name = "ratatui"
version = "0.26.3" # crate version
version = "0.27.0" # crate version
authors = ["Florian Dehau <work@fdehau.com>", "The Ratatui Developers"]
description = "A library that's all about cooking up terminal user interfaces"
documentation = "https://docs.rs/ratatui/latest/ratatui/"
@@ -30,18 +30,20 @@ cassowary = "0.3"
compact_str = "0.7.1"
crossterm = { version = "0.27", optional = true }
document-features = { version = "0.2.7", optional = true }
itertools = "0.12"
instability = "0.3.1"
itertools = "0.13"
lru = "0.12.0"
paste = "1.0.2"
palette = { version = "0.7.6", optional = true }
serde = { version = "1", optional = true, features = ["derive"] }
stability = "0.2.0"
strum = { version = "0.26", features = ["derive"] }
termion = { version = "3.0", optional = true }
strum_macros = { version = "0.26.3" }
termion = { version = "4.0.0", optional = true }
termwiz = { version = "0.22.0", optional = true }
time = { version = "0.3.11", optional = true, features = ["local-offset"] }
unicode-segmentation = "1.10"
unicode-truncate = "1"
unicode-width = "0.1"
unicode-width = "0.1.13"
[dev-dependencies]
anyhow = "1.0.71"
@@ -57,8 +59,11 @@ palette = "0.7.3"
pretty_assertions = "1.4.0"
rand = "0.8.5"
rand_chacha = "0.3.1"
rstest = "0.19.0"
rstest = "0.21.0"
serde_json = "1.0.109"
tracing = "0.1.40"
tracing-appender = "0.2.3"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
[lints.rust]
unsafe_code = "forbid"
@@ -121,6 +126,9 @@ serde = ["dep:serde", "bitflags/serde", "compact_str/serde"]
## enables the [`border!`] macro.
macros = []
## enables conversions from colors in the [`palette`] crate to [`Color`](crate::style::Color).
palette = ["dep:palette"]
## enables all widgets.
all-widgets = ["widget-calendar"]
@@ -286,6 +294,11 @@ name = "line_gauge"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "hyperlink"
required-features = ["crossterm", "unstable-widget-ref"]
doc-scrape-examples = true
[[example]]
name = "list"
required-features = ["crossterm"]
@@ -343,6 +356,11 @@ name = "tabs"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "tracing"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "user_input"
required-features = ["crossterm"]

View File

@@ -25,10 +25,10 @@
<div align="center">
[![Crate Badge]][Crate] [![Docs Badge]][API Docs] [![CI Badge]][CI Workflow] [![License
Badge]](./LICENSE) [![Sponsors Badge]][GitHub Sponsors]<br>
[![Codecov Badge]][Codecov] [![Deps.rs Badge]][Deps.rs] [![Discord Badge]][Discord Server]
[![Matrix Badge]][Matrix]<br>
[![Crate Badge]][Crate] [![Docs Badge]][API Docs] [![CI Badge]][CI Workflow] [![Deps.rs
Badge]][Deps.rs]<br> [![Codecov Badge]][Codecov] [![License Badge]](./LICENSE) [![Sponsors
Badge]][GitHub Sponsors]<br> [![Discord Badge]][Discord Server] [![Matrix Badge]][Matrix]
[![Forum Badge]][Forum]<br>
[Ratatui Website] · [API Docs] · [Examples] · [Changelog] · [Breaking Changes]<br>
[Contributing] · [Report a bug] · [Request a Feature] · [Create a Pull Request]
@@ -67,6 +67,7 @@ terminal user interfaces and showcases the features of Ratatui, along with a hel
## Other documentation
- [Ratatui Website] - explains the library's concepts and provides step-by-step tutorials
- [Ratatui Forum][Forum] - a place to ask questions and discuss the library
- [API Docs] - the full API documentation for the library on docs.rs.
- [Examples] - a collection of examples that demonstrate how to use the library.
- [Contributing] - Please read this if you are interested in contributing to the project.
@@ -339,6 +340,8 @@ Running this example produces the following output:
[Docs Badge]: https://img.shields.io/docsrs/ratatui?logo=rust&style=flat-square&logoColor=E05D44
[Matrix Badge]: https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix&color=C43AC3
[Matrix]: https://matrix.to/#/#ratatui:matrix.org
[Forum Badge]: https://img.shields.io/discourse/likes?server=https%3A%2F%2Fforum.ratatui.rs&style=flat-square&logo=discourse&label=forum&color=C43AC3
[Forum]: https://forum.ratatui.rs
[Sponsors Badge]: https://img.shields.io/github/sponsors/ratatui-org?logo=github&style=flat-square&color=1370D3
<!-- cargo-rdme end -->
@@ -354,9 +357,10 @@ In order to organize ourselves, we currently use a [Discord server](https://disc
feel free to join and come chat! There is also a [Matrix](https://matrix.org/) bridge available at
[#ratatui:matrix.org](https://matrix.to/#/#ratatui:matrix.org).
While we do utilize Discord for coordinating, it's not essential for contributing.
Our primary open-source workflow is centered around GitHub.
For significant discussions, we rely on GitHub — please open an issue, a discussion or a PR.
While we do utilize Discord for coordinating, it's not essential for contributing. We have recently
launched the [Ratatui Forum][Forum], and our primary open-source workflow is centered around GitHub.
For bugs and features, we rely on GitHub. Please [Report a bug], [Request a Feature] or [Create a
Pull Request].
Please make sure you read the updated [contributing](./CONTRIBUTING.md) guidelines, especially if
you are interested in working on a PR or issue opened in the previous repository.

View File

@@ -122,8 +122,8 @@ pub struct TabsState<'a> {
}
impl<'a> TabsState<'a> {
pub fn new(titles: Vec<&'a str>) -> TabsState {
TabsState { titles, index: 0 }
pub fn new(titles: Vec<&'a str>) -> Self {
Self { titles, index: 0 }
}
pub fn next(&mut self) {
self.index = (self.index + 1) % self.titles.len();

151
examples/hyperlink.rs Normal file
View File

@@ -0,0 +1,151 @@
//! # [Ratatui] Hyperlink examplew
//!
//! Shows how to use [OSC 8] to create hyperlinks in the terminal.
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [OSC 8]: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
//! [Ratatui]: https://github.com/ratatui-org/ratatui
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use std::{
io::{self, stdout, Stdout},
panic,
};
use color_eyre::{
config::{EyreHook, HookBuilder, PanicHook},
eyre, Result,
};
use crossterm::{
event::{self, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
};
use itertools::Itertools;
use ratatui::{prelude::*, widgets::WidgetRef};
fn main() -> Result<()> {
init_error_handling()?;
let mut terminal = init_terminal()?;
let app = App::new();
app.run(&mut terminal)?;
restore_terminal()?;
Ok(())
}
struct App {
hyperlink: Hyperlink<'static>,
}
impl App {
fn new() -> Self {
let text = Line::from(vec!["Example ".into(), "hyperlink".blue()]);
let hyperlink = Hyperlink::new(text, "https://example.com");
Self { hyperlink }
}
fn run(self, terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> io::Result<()> {
loop {
terminal.draw(|frame| frame.render_widget(&self.hyperlink, frame.size()))?;
if let Event::Key(key) = event::read()? {
if matches!(key.code, KeyCode::Char('q') | KeyCode::Esc) {
break;
}
}
}
Ok(())
}
}
/// A hyperlink widget that renders a hyperlink in the terminal using [OSC 8].
///
/// [OSC 8]: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
struct Hyperlink<'content> {
text: Text<'content>,
url: String,
}
impl<'content> Hyperlink<'content> {
fn new(text: impl Into<Text<'content>>, url: impl Into<String>) -> Self {
Self {
text: text.into(),
url: url.into(),
}
}
}
impl WidgetRef for Hyperlink<'_> {
fn render_ref(&self, area: Rect, buffer: &mut Buffer) {
self.text.render_ref(area, buffer);
// this is a hacky workaround for https://github.com/ratatui-org/ratatui/issues/902, a bug
// in the terminal code that incorrectly calculates the width of ANSI escape sequences. It
// works by rendering the hyperlink as a series of 2-character chunks, which is the
// calculated width of the hyperlink text.
for (i, two_chars) in self
.text
.to_string()
.chars()
.chunks(2)
.into_iter()
.enumerate()
{
let text = two_chars.collect::<String>();
let hyperlink = format!("\x1B]8;;{}\x07{}\x1B]8;;\x07", self.url, text);
buffer
.get_mut(area.x + i as u16 * 2, area.y)
.set_symbol(hyperlink.as_str());
}
}
}
/// Initialize the terminal with raw mode and alternate screen.
fn init_terminal() -> io::Result<Terminal<CrosstermBackend<Stdout>>> {
enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
Terminal::new(CrosstermBackend::new(stdout()))
}
/// Restore the terminal to its original state.
fn restore_terminal() -> io::Result<()> {
disable_raw_mode()?;
execute!(stdout(), LeaveAlternateScreen)?;
Ok(())
}
/// Initialize error handling with color-eyre.
pub fn init_error_handling() -> Result<()> {
let (panic_hook, eyre_hook) = HookBuilder::default().into_hooks();
set_panic_hook(panic_hook);
set_error_hook(eyre_hook)?;
Ok(())
}
/// Install a panic hook that restores the terminal before printing the panic.
fn set_panic_hook(panic_hook: PanicHook) {
let panic_hook = panic_hook.into_panic_hook();
panic::set_hook(Box::new(move |panic_info| {
let _ = restore_terminal();
panic_hook(panic_info);
}));
}
/// Install an error hook that restores the terminal before printing the error.
fn set_error_hook(eyre_hook: EyreHook) -> Result<()> {
let eyre_hook = eyre_hook.into_eyre_hook();
eyre::set_hook(Box::new(move |error| {
let _ = restore_terminal();
eyre_hook(error)
}))?;
Ok(())
}

View File

@@ -13,19 +13,19 @@
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use std::{error::Error, io, io::stdout};
use std::{error::Error, io};
use color_eyre::config::HookBuilder;
use crossterm::event::KeyEvent;
use ratatui::{
backend::{Backend, CrosstermBackend},
backend::Backend,
buffer::Buffer,
crossterm::{
event::{self, Event, KeyCode, KeyEventKind},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
crossterm::event::{self, Event, KeyCode, KeyEventKind},
layout::{Constraint, Layout, Rect},
style::{
palette::tailwind::{BLUE, GREEN, SLATE},
Color, Modifier, Style, Stylize,
},
layout::{Alignment, Constraint, Layout, Rect},
style::{palette::tailwind, Color, Modifier, Style, Stylize},
symbols,
terminal::Terminal,
text::Line,
widgets::{
@@ -34,342 +34,302 @@ use ratatui::{
},
};
const TODO_HEADER_BG: Color = tailwind::BLUE.c950;
const NORMAL_ROW_COLOR: Color = tailwind::SLATE.c950;
const ALT_ROW_COLOR: Color = tailwind::SLATE.c900;
const SELECTED_STYLE_FG: Color = tailwind::BLUE.c300;
const TEXT_COLOR: Color = tailwind::SLATE.c200;
const COMPLETED_TEXT_COLOR: Color = tailwind::GREEN.c500;
const TODO_HEADER_STYLE: Style = Style::new().fg(SLATE.c100).bg(BLUE.c800);
const NORMAL_ROW_BG: Color = SLATE.c950;
const ALT_ROW_BG_COLOR: Color = SLATE.c900;
const SELECTED_STYLE: Style = Style::new().bg(SLATE.c800).add_modifier(Modifier::BOLD);
const TEXT_FG_COLOR: Color = SLATE.c200;
const COMPLETED_TEXT_FG_COLOR: Color = GREEN.c500;
#[derive(Copy, Clone)]
enum Status {
Todo,
Completed,
fn main() -> Result<(), Box<dyn Error>> {
tui::init_error_hooks()?;
let terminal = tui::init_terminal()?;
let mut app = App::default();
app.run(terminal)?;
tui::restore_terminal()?;
Ok(())
}
/// This struct holds the current state of the app. In particular, it has the `todo_list` field
/// which is a wrapper around `ListState`. Keeping track of the state lets us render the
/// associated widget with its state and have access to features such as natural scrolling.
///
/// Check the event handling at the bottom to see how to change the state on incoming events. Check
/// the drawing logic for items on how to specify the highlighting style for selected items.
struct App {
should_exit: bool,
todo_list: TodoList,
}
struct TodoList {
items: Vec<TodoItem>,
state: ListState,
}
#[derive(Debug)]
struct TodoItem {
todo: String,
info: String,
status: Status,
}
impl TodoItem {
fn new(todo: &str, info: &str, status: Status) -> Self {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Status {
Todo,
Completed,
}
impl Default for App {
fn default() -> Self {
Self {
todo: todo.to_string(),
info: info.to_string(),
status,
}
}
}
struct TodoList {
state: ListState,
items: Vec<TodoItem>,
last_selected: Option<usize>,
}
/// This struct holds the current state of the app. In particular, it has the `items` field which is
/// a wrapper around `ListState`. Keeping track of the items state let us render the associated
/// widget with its state and have access to features such as natural scrolling.
///
/// Check the event handling at the bottom to see how to change the state on incoming events.
/// Check the drawing logic for items on how to specify the highlighting style for selected items.
struct App {
items: TodoList,
}
fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
init_error_hooks()?;
let terminal = init_terminal()?;
// create app and run it
App::new().run(terminal)?;
restore_terminal()?;
Ok(())
}
fn init_error_hooks() -> color_eyre::Result<()> {
let (panic, error) = HookBuilder::default().into_hooks();
let panic = panic.into_panic_hook();
let error = error.into_eyre_hook();
color_eyre::eyre::set_hook(Box::new(move |e| {
let _ = restore_terminal();
error(e)
}))?;
std::panic::set_hook(Box::new(move |info| {
let _ = restore_terminal();
panic(info);
}));
Ok(())
}
fn init_terminal() -> color_eyre::Result<Terminal<impl Backend>> {
enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout());
let terminal = Terminal::new(backend)?;
Ok(terminal)
}
fn restore_terminal() -> color_eyre::Result<()> {
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
Ok(())
}
impl App {
fn new() -> Self {
Self {
items: TodoList::with_items(&[
("Rewrite everything with Rust!", "I can't hold my inner voice. He tells me to rewrite the complete universe with Rust", Status::Todo),
("Rewrite all of your tui apps with Ratatui", "Yes, you heard that right. Go and replace your tui with Ratatui.", Status::Completed),
("Pet your cat", "Minnak loves to be pet by you! Don't forget to pet and give some treats!", Status::Todo),
("Walk with your dog", "Max is bored, go walk with him!", Status::Todo),
("Pay the bills", "Pay the train subscription!!!", Status::Completed),
("Refactor list example", "If you see this info that means I completed this task!", Status::Completed),
should_exit: false,
todo_list: TodoList::from_iter([
(Status::Todo, "Rewrite everything with Rust!", "I can't hold my inner voice. He tells me to rewrite the complete universe with Rust"),
(Status::Completed, "Rewrite all of your tui apps with Ratatui", "Yes, you heard that right. Go and replace your tui with Ratatui."),
(Status::Todo, "Pet your cat", "Minnak loves to be pet by you! Don't forget to pet and give some treats!"),
(Status::Todo, "Walk with your dog", "Max is bored, go walk with him!"),
(Status::Completed, "Pay the bills", "Pay the train subscription!!!"),
(Status::Completed, "Refactor list example", "If you see this info that means I completed this task!"),
]),
}
}
}
/// Changes the status of the selected list item
fn change_status(&mut self) {
if let Some(i) = self.items.state.selected() {
self.items.items[i].status = match self.items.items[i].status {
Status::Completed => Status::Todo,
Status::Todo => Status::Completed,
}
impl FromIterator<(Status, &'static str, &'static str)> for TodoList {
fn from_iter<I: IntoIterator<Item = (Status, &'static str, &'static str)>>(iter: I) -> Self {
let items = iter
.into_iter()
.map(|(status, todo, info)| TodoItem::new(status, todo, info))
.collect();
let state = ListState::default();
Self { items, state }
}
}
impl TodoItem {
fn new(status: Status, todo: &str, info: &str) -> Self {
Self {
status,
todo: todo.to_string(),
info: info.to_string(),
}
}
fn go_top(&mut self) {
self.items.state.select(Some(0));
}
fn go_bottom(&mut self) {
self.items.state.select(Some(self.items.items.len() - 1));
}
}
impl App {
fn run(&mut self, mut terminal: Terminal<impl Backend>) -> io::Result<()> {
loop {
self.draw(&mut terminal)?;
while !self.should_exit {
terminal.draw(|f| f.render_widget(&mut *self, f.size()))?;
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Char('q') | KeyCode::Esc => return Ok(()),
KeyCode::Char('h') | KeyCode::Left => self.items.unselect(),
KeyCode::Char('j') | KeyCode::Down => self.items.next(),
KeyCode::Char('k') | KeyCode::Up => self.items.previous(),
KeyCode::Char('l') | KeyCode::Right | KeyCode::Enter => {
self.change_status();
}
KeyCode::Char('g') => self.go_top(),
KeyCode::Char('G') => self.go_bottom(),
_ => {}
}
}
self.handle_key(key);
};
}
Ok(())
}
fn handle_key(&mut self, key: KeyEvent) {
if key.kind != KeyEventKind::Press {
return;
}
match key.code {
KeyCode::Char('q') | KeyCode::Esc => self.should_exit = true,
KeyCode::Char('h') | KeyCode::Left => self.select_none(),
KeyCode::Char('j') | KeyCode::Down => self.select_next(),
KeyCode::Char('k') | KeyCode::Up => self.select_previous(),
KeyCode::Char('g') | KeyCode::Home => self.select_first(),
KeyCode::Char('G') | KeyCode::End => self.select_last(),
KeyCode::Char('l') | KeyCode::Right | KeyCode::Enter => {
self.toggle_status();
}
_ => {}
}
}
fn draw(&mut self, terminal: &mut Terminal<impl Backend>) -> io::Result<()> {
terminal.draw(|f| f.render_widget(self, f.size()))?;
Ok(())
fn select_none(&mut self) {
self.todo_list.state.select(None);
}
fn select_next(&mut self) {
self.todo_list.state.select_next();
}
fn select_previous(&mut self) {
self.todo_list.state.select_previous();
}
fn select_first(&mut self) {
self.todo_list.state.select_first();
}
fn select_last(&mut self) {
self.todo_list.state.select_last();
}
/// Changes the status of the selected list item
fn toggle_status(&mut self) {
if let Some(i) = self.todo_list.state.selected() {
self.todo_list.items[i].status = match self.todo_list.items[i].status {
Status::Completed => Status::Todo,
Status::Todo => Status::Completed,
}
}
}
}
impl Widget for &mut App {
fn render(self, area: Rect, buf: &mut Buffer) {
// Create a space for header, todo list and the footer.
let vertical = Layout::vertical([
let [header_area, main_area, footer_area] = Layout::vertical([
Constraint::Length(2),
Constraint::Min(0),
Constraint::Length(2),
]);
let [header_area, rest_area, footer_area] = vertical.areas(area);
Constraint::Fill(1),
Constraint::Length(1),
])
.areas(area);
// Create two chunks with equal vertical screen space. One for the list and the other for
// the info block.
let vertical = Layout::vertical([Constraint::Percentage(50), Constraint::Percentage(50)]);
let [upper_item_list_area, lower_item_list_area] = vertical.areas(rest_area);
let [list_area, item_area] =
Layout::vertical([Constraint::Fill(1), Constraint::Fill(1)]).areas(main_area);
render_title(header_area, buf);
self.render_todo(upper_item_list_area, buf);
self.render_info(lower_item_list_area, buf);
render_footer(footer_area, buf);
App::render_header(header_area, buf);
App::render_footer(footer_area, buf);
self.render_list(list_area, buf);
self.render_selected_item(item_area, buf);
}
}
/// Rendering logic for the app
impl App {
fn render_todo(&mut self, area: Rect, buf: &mut Buffer) {
// We create two blocks, one is for the header (outer) and the other is for list (inner).
let outer_block = Block::new()
.borders(Borders::NONE)
.title_alignment(Alignment::Center)
.title("TODO List")
.fg(TEXT_COLOR)
.bg(TODO_HEADER_BG);
let inner_block = Block::new()
.borders(Borders::NONE)
.fg(TEXT_COLOR)
.bg(NORMAL_ROW_COLOR);
fn render_header(area: Rect, buf: &mut Buffer) {
Paragraph::new("Ratatui List Example")
.bold()
.centered()
.render(area, buf);
}
// We get the inner area from outer_block. We'll use this area later to render the table.
let outer_area = area;
let inner_area = outer_block.inner(outer_area);
fn render_footer(area: Rect, buf: &mut Buffer) {
Paragraph::new("Use ↓↑ to move, ← to unselect, → to change status, g/G to go top/bottom.")
.centered()
.render(area, buf);
}
// We can render the header in outer_area.
outer_block.render(outer_area, buf);
fn render_list(&mut self, area: Rect, buf: &mut Buffer) {
let block = Block::new()
.title(Line::raw("TODO List").centered())
.borders(Borders::TOP)
.border_set(symbols::border::EMPTY)
.border_style(TODO_HEADER_STYLE)
.bg(NORMAL_ROW_BG);
// Iterate through all elements in the `items` and stylize them.
let items: Vec<ListItem> = self
.items
.todo_list
.items
.iter()
.enumerate()
.map(|(i, todo_item)| todo_item.to_list_item(i))
.map(|(i, todo_item)| {
let color = alternate_colors(i);
ListItem::from(todo_item).bg(color)
})
.collect();
// Create a List from all list items and highlight the currently selected one
let items = List::new(items)
.block(inner_block)
.highlight_style(
Style::default()
.add_modifier(Modifier::BOLD)
.add_modifier(Modifier::REVERSED)
.fg(SELECTED_STYLE_FG),
)
let list = List::new(items)
.block(block)
.highlight_style(SELECTED_STYLE)
.highlight_symbol(">")
.highlight_spacing(HighlightSpacing::Always);
// We can now render the item list
// (look careful we are using StatefulWidget's render.)
// ratatui::widgets::StatefulWidget::render as stateful_render
StatefulWidget::render(items, inner_area, buf, &mut self.items.state);
// We need to disambiguate this trait method as both `Widget` and `StatefulWidget` share the
// same method name `render`.
StatefulWidget::render(list, area, buf, &mut self.todo_list.state);
}
fn render_info(&self, area: Rect, buf: &mut Buffer) {
fn render_selected_item(&self, area: Rect, buf: &mut Buffer) {
// We get the info depending on the item's state.
let info = if let Some(i) = self.items.state.selected() {
match self.items.items[i].status {
Status::Completed => format!("✓ DONE: {}", self.items.items[i].info),
Status::Todo => format!("TODO: {}", self.items.items[i].info),
let info = if let Some(i) = self.todo_list.state.selected() {
match self.todo_list.items[i].status {
Status::Completed => format!("✓ DONE: {}", self.todo_list.items[i].info),
Status::Todo => format!("TODO: {}", self.todo_list.items[i].info),
}
} else {
"Nothing to see here...".to_string()
"Nothing selected...".to_string()
};
// We show the list item's info under the list in this paragraph
let outer_info_block = Block::new()
.borders(Borders::NONE)
.title_alignment(Alignment::Center)
.title("TODO Info")
.fg(TEXT_COLOR)
.bg(TODO_HEADER_BG);
let inner_info_block = Block::new()
.borders(Borders::NONE)
.padding(Padding::horizontal(1))
.bg(NORMAL_ROW_COLOR);
// This is a similar process to what we did for list. outer_info_area will be used for
// header inner_info_area will be used for the list info.
let outer_info_area = area;
let inner_info_area = outer_info_block.inner(outer_info_area);
// We can render the header. Inner info will be rendered later
outer_info_block.render(outer_info_area, buf);
let info_paragraph = Paragraph::new(info)
.block(inner_info_block)
.fg(TEXT_COLOR)
.wrap(Wrap { trim: false });
let block = Block::new()
.title(Line::raw("TODO Info").centered())
.borders(Borders::TOP)
.border_set(symbols::border::EMPTY)
.border_style(TODO_HEADER_STYLE)
.bg(NORMAL_ROW_BG)
.padding(Padding::horizontal(1));
// We can now render the item info
info_paragraph.render(inner_info_area, buf);
Paragraph::new(info)
.block(block)
.fg(TEXT_FG_COLOR)
.wrap(Wrap { trim: false })
.render(area, buf);
}
}
fn render_title(area: Rect, buf: &mut Buffer) {
Paragraph::new("Ratatui List Example")
.bold()
.centered()
.render(area, buf);
}
fn render_footer(area: Rect, buf: &mut Buffer) {
Paragraph::new("\nUse ↓↑ to move, ← to unselect, → to change status, g/G to go top/bottom.")
.centered()
.render(area, buf);
}
impl TodoList {
fn with_items(items: &[(&str, &str, Status)]) -> Self {
Self {
state: ListState::default(),
items: items
.iter()
.map(|(todo, info, status)| TodoItem::new(todo, info, *status))
.collect(),
last_selected: None,
}
const fn alternate_colors(i: usize) -> Color {
if i % 2 == 0 {
NORMAL_ROW_BG
} else {
ALT_ROW_BG_COLOR
}
}
fn next(&mut self) {
let i = match self.state.selected() {
Some(i) => {
if i >= self.items.len() - 1 {
0
} else {
i + 1
}
impl From<&TodoItem> for ListItem<'_> {
fn from(value: &TodoItem) -> Self {
let line = match value.status {
Status::Todo => Line::styled(format!("{}", value.todo), TEXT_FG_COLOR),
Status::Completed => {
Line::styled(format!("{}", value.todo), COMPLETED_TEXT_FG_COLOR)
}
None => self.last_selected.unwrap_or(0),
};
self.state.select(Some(i));
}
fn previous(&mut self) {
let i = match self.state.selected() {
Some(i) => {
if i == 0 {
self.items.len() - 1
} else {
i - 1
}
}
None => self.last_selected.unwrap_or(0),
};
self.state.select(Some(i));
}
fn unselect(&mut self) {
let offset = self.state.offset();
self.last_selected = self.state.selected();
self.state.select(None);
*self.state.offset_mut() = offset;
ListItem::new(line)
}
}
impl TodoItem {
fn to_list_item(&self, index: usize) -> ListItem {
let bg_color = match index % 2 {
0 => NORMAL_ROW_COLOR,
_ => ALT_ROW_COLOR,
};
let line = match self.status {
Status::Todo => Line::styled(format!("{}", self.todo), TEXT_COLOR),
Status::Completed => Line::styled(
format!("{}", self.todo),
(COMPLETED_TEXT_COLOR, bg_color),
),
};
mod tui {
use std::{io, io::stdout};
ListItem::new(line).bg(bg_color)
use color_eyre::config::HookBuilder;
use ratatui::{
backend::{Backend, CrosstermBackend},
crossterm::{
terminal::{
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
},
ExecutableCommand,
},
terminal::Terminal,
};
pub fn init_error_hooks() -> color_eyre::Result<()> {
let (panic, error) = HookBuilder::default().into_hooks();
let panic = panic.into_panic_hook();
let error = error.into_eyre_hook();
color_eyre::eyre::set_hook(Box::new(move |e| {
let _ = restore_terminal();
error(e)
}))?;
std::panic::set_hook(Box::new(move |info| {
let _ = restore_terminal();
panic(info);
}));
Ok(())
}
pub fn init_terminal() -> io::Result<Terminal<impl Backend>> {
stdout().execute(EnterAlternateScreen)?;
enable_raw_mode()?;
Terminal::new(CrosstermBackend::new(stdout()))
}
pub fn restore_terminal() -> io::Result<()> {
stdout().execute(LeaveAlternateScreen)?;
disable_raw_mode()
}
}

View File

@@ -14,154 +14,218 @@
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use std::{
error::Error,
io,
io::{self},
time::{Duration, Instant},
};
use crossterm::event::KeyEventKind;
use ratatui::{
backend::{Backend, CrosstermBackend},
crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
},
layout::{Constraint, Layout},
style::{Color, Modifier, Style, Stylize},
terminal::{Frame, Terminal},
buffer::Buffer,
crossterm::event::{self, Event, KeyCode},
layout::{Constraint, Layout, Rect},
style::{Color, Stylize},
text::{Line, Masked, Span},
widgets::{Block, Paragraph, Wrap},
widgets::{Block, Paragraph, Widget, Wrap},
};
struct App {
scroll: u16,
}
impl App {
const fn new() -> Self {
Self { scroll: 0 }
}
fn on_tick(&mut self) {
self.scroll += 1;
self.scroll %= 10;
}
}
fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
// create app and run it
let tick_rate = Duration::from_millis(250);
let app = App::new();
let res = run_app(&mut terminal, app, tick_rate);
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
}
use self::common::{init_terminal, install_hooks, restore_terminal, Tui};
fn main() -> color_eyre::Result<()> {
install_hooks()?;
let mut terminal = init_terminal()?;
let mut app = App::new();
app.run(&mut terminal)?;
restore_terminal()?;
Ok(())
}
fn run_app<B: Backend>(
terminal: &mut Terminal<B>,
mut app: App,
tick_rate: Duration,
) -> io::Result<()> {
let mut last_tick = Instant::now();
loop {
terminal.draw(|f| ui(f, &app))?;
#[derive(Debug)]
struct App {
should_exit: bool,
scroll: u16,
last_tick: Instant,
}
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if crossterm::event::poll(timeout)? {
impl App {
/// The duration between each tick.
const TICK_RATE: Duration = Duration::from_millis(250);
/// Create a new instance of the app.
fn new() -> Self {
Self {
should_exit: false,
scroll: 0,
last_tick: Instant::now(),
}
}
/// Run the app until the user exits.
fn run(&mut self, terminal: &mut Tui) -> io::Result<()> {
while !self.should_exit {
self.draw(terminal)?;
self.handle_events()?;
if self.last_tick.elapsed() >= Self::TICK_RATE {
self.on_tick();
self.last_tick = Instant::now();
}
}
Ok(())
}
/// Draw the app to the terminal.
fn draw(&mut self, terminal: &mut Tui) -> io::Result<()> {
terminal.draw(|frame| frame.render_widget(self, frame.size()))?;
Ok(())
}
/// Handle events from the terminal.
fn handle_events(&mut self) -> io::Result<()> {
let timeout = Self::TICK_RATE.saturating_sub(self.last_tick.elapsed());
while event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
if key.code == KeyCode::Char('q') {
return Ok(());
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
self.should_exit = true;
}
}
}
if last_tick.elapsed() >= tick_rate {
app.on_tick();
last_tick = Instant::now();
}
Ok(())
}
/// Update the app state on each tick.
fn on_tick(&mut self) {
self.scroll = (self.scroll + 1) % 10;
}
}
fn ui(f: &mut Frame, app: &App) {
let size = f.size();
impl Widget for &mut App {
fn render(self, area: Rect, buf: &mut Buffer) {
let areas = Layout::vertical([Constraint::Max(9); 4]).split(area);
Paragraph::new(create_lines(area))
.block(title_block("Default alignment (Left), no wrap"))
.gray()
.render(areas[0], buf);
Paragraph::new(create_lines(area))
.block(title_block("Default alignment (Left), with wrap"))
.gray()
.wrap(Wrap { trim: true })
.render(areas[1], buf);
Paragraph::new(create_lines(area))
.block(title_block("Right alignment, with wrap"))
.gray()
.right_aligned()
.wrap(Wrap { trim: true })
.render(areas[2], buf);
Paragraph::new(create_lines(area))
.block(title_block("Center alignment, with wrap, with scroll"))
.gray()
.centered()
.wrap(Wrap { trim: true })
.scroll((self.scroll, 0))
.render(areas[3], buf);
}
}
// Words made "loooong" to demonstrate line breaking.
let s = "Veeeeeeeeeeeeeeeery loooooooooooooooooong striiiiiiiiiiiiiiiiiiiiiiiiiing. ";
let mut long_line = s.repeat(usize::from(size.width) / s.len() + 4);
long_line.push('\n');
/// Create a bordered block with a title.
fn title_block(title: &str) -> Block {
Block::bordered()
.gray()
.title(title.bold().into_centered_line())
}
let block = Block::new().black();
f.render_widget(block, size);
let layout = Layout::vertical([Constraint::Ratio(1, 4); 4]).split(size);
let text = vec![
Line::from("This is a line "),
Line::from("This is a line ".red()),
Line::from("This is a line".on_blue()),
Line::from("This is a longer line".crossed_out()),
Line::from(long_line.on_green()),
Line::from("This is a line".green().italic()),
Line::from(vec![
/// Create some lines to display in the paragraph.
fn create_lines(area: Rect) -> Vec<Line<'static>> {
let short_line = "A long line to demonstrate line wrapping. ";
let long_line = short_line.repeat(usize::from(area.width) / short_line.len() + 4);
let mut styled_spans = vec![];
for span in [
"Styled".blue(),
"Spans".red().on_white(),
"Bold".bold(),
"Italic".italic(),
"Underlined".underlined(),
"Strikethrough".crossed_out(),
] {
styled_spans.push(span);
styled_spans.push(" ".into());
}
vec![
Line::raw("Unstyled Line"),
Line::raw("Styled Line").black().on_red().bold().italic(),
Line::from(styled_spans),
Line::from(long_line.green().italic()),
Line::from_iter([
"Masked text: ".into(),
Span::styled(
Masked::new("password", '*'),
Style::default().fg(Color::Red),
),
Span::styled(Masked::new("my secret password", '*'), Color::Red),
]),
];
]
}
let create_block = |title| {
Block::bordered()
.style(Style::default().fg(Color::Gray))
.title(Span::styled(
title,
Style::default().add_modifier(Modifier::BOLD),
))
/// A module for common functionality used in the examples.
mod common {
use std::{
io::{self, stdout, Stdout},
panic,
};
let paragraph = Paragraph::new(text.clone())
.style(Style::default().fg(Color::Gray))
.block(create_block("Default alignment (Left), no wrap"));
f.render_widget(paragraph, layout[0]);
use color_eyre::{
config::{EyreHook, HookBuilder, PanicHook},
eyre,
};
use crossterm::ExecutableCommand;
use ratatui::{
backend::CrosstermBackend,
crossterm::terminal::{
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
},
terminal::Terminal,
};
let paragraph = Paragraph::new(text.clone())
.style(Style::default().fg(Color::Gray))
.block(create_block("Default alignment (Left), with wrap"))
.wrap(Wrap { trim: true });
f.render_widget(paragraph, layout[1]);
// A simple alias for the terminal type used in this example.
pub type Tui = Terminal<CrosstermBackend<Stdout>>;
let paragraph = Paragraph::new(text.clone())
.style(Style::default().fg(Color::Gray))
.block(create_block("Right alignment, with wrap"))
.right_aligned()
.wrap(Wrap { trim: true });
f.render_widget(paragraph, layout[2]);
/// Initialize the terminal and enter alternate screen mode.
pub fn init_terminal() -> io::Result<Tui> {
enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout());
Terminal::new(backend)
}
let paragraph = Paragraph::new(text)
.style(Style::default().fg(Color::Gray))
.block(create_block("Center alignment, with wrap, with scroll"))
.centered()
.wrap(Wrap { trim: true })
.scroll((app.scroll, 0));
f.render_widget(paragraph, layout[3]);
/// Restore the terminal to its original state.
pub fn restore_terminal() -> io::Result<()> {
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
Ok(())
}
/// Installs hooks for panic and error handling.
///
/// Makes the app resilient to panics and errors by restoring the terminal before printing the
/// panic or error message. This prevents error messages from being messed up by the terminal
/// state.
pub fn install_hooks() -> color_eyre::Result<()> {
let (panic_hook, eyre_hook) = HookBuilder::default().into_hooks();
install_panic_hook(panic_hook);
install_error_hook(eyre_hook)?;
Ok(())
}
/// Install a panic hook that restores the terminal before printing the panic.
fn install_panic_hook(panic_hook: PanicHook) {
let panic_hook = panic_hook.into_panic_hook();
panic::set_hook(Box::new(move |panic_info| {
let _ = restore_terminal();
panic_hook(panic_info);
}));
}
/// Install an error hook that restores the terminal before printing the error.
fn install_error_hook(eyre_hook: EyreHook) -> color_eyre::Result<()> {
let eyre_hook = eyre_hook.into_eyre_hook();
eyre::set_hook(Box::new(move |error| {
let _ = restore_terminal();
eyre_hook(error)
}))?;
Ok(())
}
}

154
examples/tracing.rs Normal file
View File

@@ -0,0 +1,154 @@
//! # [Ratatui] Tracing example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui-org/ratatui
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
// A simple example demonstrating how to use the [tracing] with Ratatui to log to a file.
//
// This example demonstrates how to use the [tracing] crate with Ratatui to log to a file. The
// example sets up a simple logger that logs to a file named `tracing.log` in the current directory.
//
// Run the example with `cargo run --example tracing` and then view the `tracing.log` file to see
// the logs. To see more logs, you can run the example with `RUST_LOG=tracing=debug cargo run
// --example`
//
// For a helpful widget that handles logging, see the [tui-logger] crate.
//
// [tracing]: https://crates.io/crates/tracing
// [tui-logger]: https://crates.io/crates/tui-logger
use std::{fs::File, io::stdout, panic, time::Duration};
use color_eyre::{
config::HookBuilder,
eyre::{self, Context},
Result,
};
use crossterm::{
event::{self, Event, KeyCode},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
};
use ratatui::{
backend::{Backend, CrosstermBackend},
terminal::Terminal,
widgets::{Block, Paragraph},
};
use tracing::{debug, info, instrument, trace, Level};
use tracing_appender::{non_blocking, non_blocking::WorkerGuard};
use tracing_subscriber::EnvFilter;
fn main() -> Result<()> {
init_error_hooks()?;
let _guard = init_tracing()?;
info!("Starting tracing example");
let mut terminal = init_terminal()?;
let mut events = vec![]; // a buffer to store the recent events to display in the UI
while !should_exit(&events) {
handle_events(&mut events)?;
terminal.draw(|frame| ui(frame, &events))?;
}
restore_terminal()?;
info!("Exiting tracing example");
println!("See the tracing.log file for the logs");
Ok(())
}
fn should_exit(events: &[Event]) -> bool {
events
.iter()
.any(|event| matches!(event, Event::Key(key) if key.code == KeyCode::Char('q')))
}
/// Handle events and insert them into the events vector keeping only the last 10 events
#[instrument(skip(events))]
fn handle_events(events: &mut Vec<Event>) -> Result<()> {
// Render the UI at least once every 100ms
if event::poll(Duration::from_millis(100))? {
let event = event::read()?;
debug!(?event);
events.insert(0, event);
}
events.truncate(10);
Ok(())
}
#[instrument(skip_all)]
fn ui(frame: &mut ratatui::Frame, events: &[Event]) {
// To view this event, run the example with `RUST_LOG=tracing=debug cargo run --example tracing`
trace!(frame_count = frame.count(), event_count = events.len());
let area = frame.size();
let events = events.iter().map(|e| format!("{e:?}")).collect::<Vec<_>>();
let paragraph = Paragraph::new(events.join("\n"))
.block(Block::bordered().title("Tracing example. Press 'q' to quit."));
frame.render_widget(paragraph, area);
}
/// Initialize the tracing subscriber to log to a file
///
/// This function initializes the tracing subscriber to log to a file named `tracing.log` in the
/// current directory. The function returns a [`WorkerGuard`] that must be kept alive for the
/// duration of the program to ensure that logs are flushed to the file on shutdown. The logs are
/// written in a non-blocking fashion to ensure that the logs do not block the main thread.
fn init_tracing() -> Result<WorkerGuard> {
let file = File::create("tracing.log").wrap_err("failed to create tracing.log")?;
let (non_blocking, guard) = non_blocking(file);
// By default, the subscriber is configured to log all events with a level of `DEBUG` or higher,
// but this can be changed by setting the `RUST_LOG` environment variable.
let env_filter = EnvFilter::builder()
.with_default_directive(Level::DEBUG.into())
.from_env_lossy();
tracing_subscriber::fmt()
.with_writer(non_blocking)
.with_env_filter(env_filter)
.init();
Ok(guard)
}
/// Initialize the error hooks to ensure that the terminal is restored to a sane state before
/// exiting
fn init_error_hooks() -> Result<()> {
let (panic, error) = HookBuilder::default().into_hooks();
let panic = panic.into_panic_hook();
let error = error.into_eyre_hook();
eyre::set_hook(Box::new(move |e| {
let _ = restore_terminal();
error(e)
}))?;
panic::set_hook(Box::new(move |info| {
let _ = restore_terminal();
panic(info);
}));
Ok(())
}
#[instrument]
fn init_terminal() -> Result<Terminal<impl Backend>> {
enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout());
let terminal = Terminal::new(backend)?;
debug!("terminal initialized");
Ok(terminal)
}
#[instrument]
fn restore_terminal() -> Result<()> {
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
debug!("terminal restored");
Ok(())
}

View File

@@ -0,0 +1,30 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/constraints.tape`
Output "target/constraint-explorer.gif"
Set Theme "Aardvark Blue"
Set FontSize 18
Set Width 1200
Set Height 950
Hide
Type "cargo run --example=constraint-explorer --features=crossterm"
Enter
Sleep 2s
Show
Set TypingSpeed 2s
Type "1"
Type "2"
Right
Type "4"
Type "5"
Up
Up
Down
Down
Right
Set TypingSpeed 0.5s
Type "++++++++"
Type "--------"
Type "aaa"
Sleep 2s
Type "xxx"
Hide

View File

@@ -0,0 +1,15 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/hello_world.tape`
Output "target/hyperlink.gif"
Set Theme "Aardvark Blue"
Set Width 600
Set Height 150
Hide
Type "cargo run --example=hyperlink --features=crossterm,unstable-widget-ref"
Enter
Sleep 2s
Show
Sleep 1s
Hide
Type "q"

12
examples/vhs/minimal.tape Normal file
View File

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

12
examples/vhs/tracing.tape Normal file
View File

@@ -0,0 +1,12 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/barchart.tape`
Output "target/tracing.gif"
Set Theme "Aardvark Blue"
Set Width 1200
Set Height 800
Type "RUST_LOG=trace cargo run --example=tracing" Enter
Sleep 1s
Type @100ms "jjjjq"
Sleep 1s
Type "cat tracing.log" Enter
Sleep 10s

View File

@@ -106,7 +106,7 @@ where
}
/// Gets the writer.
#[stability::unstable(
#[instability::unstable(
feature = "backend-writer",
issue = "https://github.com/ratatui-org/ratatui/pull/991"
)]
@@ -118,7 +118,7 @@ where
///
/// Note: writing to the writer may cause incorrect output after the write. This is due to the
/// way that the Terminal implements diffing Buffers.
#[stability::unstable(
#[instability::unstable(
feature = "backend-writer",
issue = "https://github.com/ratatui-org/ratatui/pull/991"
)]

View File

@@ -88,7 +88,7 @@ where
}
/// Gets the writer.
#[stability::unstable(
#[instability::unstable(
feature = "backend-writer",
issue = "https://github.com/ratatui-org/ratatui/pull/991"
)]
@@ -99,7 +99,7 @@ where
/// Gets the writer as a mutable reference.
/// Note: writing to the writer may cause incorrect output after the write. This is due to the
/// way that the Terminal implements diffing Buffers.
#[stability::unstable(
#[instability::unstable(
feature = "backend-writer",
issue = "https://github.com/ratatui-org/ratatui/pull/991"
)]

View File

@@ -609,16 +609,18 @@ mod tests {
#[test]
fn set_string_zero_width() {
assert_eq!("\u{200B}".width(), 0);
let area = Rect::new(0, 0, 1, 1);
let mut buffer = Buffer::empty(area);
// Leading grapheme with zero width
let s = "\u{1}a";
let s = "\u{200B}a";
buffer.set_stringn(0, 0, s, 1, Style::default());
assert_eq!(buffer, Buffer::with_lines(["a"]));
// Trailing grapheme with zero with
let s = "a\u{1}";
let s = "a\u{200B}";
buffer.set_stringn(0, 0, s, 1, Style::default());
assert_eq!(buffer, Buffer::with_lines(["a"]));
}

View File

@@ -67,6 +67,14 @@ impl Cell {
self
}
/// Appends a symbol to the 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
}
/// Sets the symbol of the cell to a single character.
pub fn set_char(&mut self, ch: char) -> &mut Self {
let mut buf = [0; 4];
@@ -154,16 +162,128 @@ mod tests {
use super::*;
#[test]
fn symbol_field() {
let mut cell = Cell::EMPTY;
fn new() {
let cell = Cell::new("");
assert_eq!(
cell,
Cell {
symbol: CompactString::new_inline(""),
fg: Color::Reset,
bg: Color::Reset,
#[cfg(feature = "underline-color")]
underline_color: Color::Reset,
modifier: Modifier::empty(),
skip: false,
}
);
}
#[test]
fn empty() {
let cell = Cell::EMPTY;
assert_eq!(cell.symbol(), " ");
}
#[test]
fn set_symbol() {
let mut cell = Cell::EMPTY;
cell.set_symbol(""); // Multi-byte character
assert_eq!(cell.symbol(), "");
cell.set_symbol("👨‍👩‍👧‍👦"); // Multiple code units combined with ZWJ
assert_eq!(cell.symbol(), "👨‍👩‍👧‍👦");
}
// above Cell::EMPTY is put into a mutable variable and is changed then.
// While this looks like it might change the constant, it actually doesnt:
assert_eq!(Cell::EMPTY.symbol(), " ");
#[test]
fn append_symbol() {
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}");
}
#[test]
fn set_char() {
let mut cell = Cell::EMPTY;
cell.set_char('あ'); // Multi-byte character
assert_eq!(cell.symbol(), "");
}
#[test]
fn set_fg() {
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;
cell.set_bg(Color::Red);
assert_eq!(cell.bg, Color::Red);
}
#[test]
fn set_style() {
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);
}
#[test]
fn set_skip() {
let mut cell = Cell::EMPTY;
cell.set_skip(true);
assert!(cell.skip);
}
#[test]
fn reset() {
let mut cell = Cell::EMPTY;
cell.set_symbol("");
cell.set_fg(Color::Red);
cell.set_bg(Color::Blue);
cell.set_skip(true);
cell.reset();
assert_eq!(cell.symbol(), " ");
assert_eq!(cell.fg, Color::Reset);
assert_eq!(cell.bg, Color::Reset);
assert!(!cell.skip);
}
#[test]
fn style() {
let cell = Cell::EMPTY;
assert_eq!(
cell.style(),
Style {
fg: Some(Color::Reset),
bg: Some(Color::Reset),
#[cfg(feature = "underline-color")]
underline_color: Some(Color::Reset),
add_modifier: Modifier::empty(),
sub_modifier: Modifier::empty(),
}
);
}
#[test]
fn default() {
let cell = Cell::default();
assert_eq!(cell.symbol(), " ");
}
#[test]
fn cell_eq() {
let cell1 = Cell::new("");
let cell2 = Cell::new("");
assert_eq!(cell1, cell2);
}
#[test]
fn cell_ne() {
let cell1 = Cell::new("");
let cell2 = Cell::new("");
assert_ne!(cell1, cell2);
}
}

View File

@@ -1881,14 +1881,11 @@ mod tests {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Percentage(10),
Constraint::Max(5),
Constraint::Min(1),
]
.as_ref(),
)
.constraints([
Constraint::Percentage(10),
Constraint::Max(5),
Constraint::Min(1),
])
.split(target);
assert_eq!(target.height, chunks.iter().map(|r| r.height).sum::<u16>());

View File

@@ -1,4 +1,6 @@
#![warn(missing_docs)]
use std::fmt;
use crate::layout::Rect;
/// Position in the terminal
@@ -61,6 +63,12 @@ impl From<Rect> for Position {
}
}
impl fmt::Display for Position {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -94,4 +102,10 @@ mod tests {
assert_eq!(position.x, 1);
assert_eq!(position.y, 2);
}
#[test]
fn to_string() {
let position = Position::new(1, 2);
assert_eq!(position.to_string(), "(1, 2)");
}
}

View File

@@ -58,7 +58,7 @@ impl Rect {
/// 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_value();
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);

View File

@@ -1,4 +1,3 @@
use self::layout::Position;
use crate::prelude::*;
/// An iterator over rows within a `Rect`.

View File

@@ -1,4 +1,6 @@
#![warn(missing_docs)]
use std::fmt;
use crate::prelude::*;
/// A simple size struct
@@ -32,6 +34,12 @@ impl From<Rect> for Size {
}
}
impl fmt::Display for Size {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}x{}", self.width, self.height)
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -56,4 +64,9 @@ mod tests {
assert_eq!(size.width, 10);
assert_eq!(size.height, 20);
}
#[test]
fn display() {
assert_eq!(Size::new(10, 20).to_string(), "10x20");
}
}

View File

@@ -2,10 +2,10 @@
//!
//! <div align="center">
//!
//! [![Crate Badge]][Crate] [![Docs Badge]][API Docs] [![CI Badge]][CI Workflow] [![License
//! Badge]](./LICENSE) [![Sponsors Badge]][GitHub Sponsors]<br>
//! [![Codecov Badge]][Codecov] [![Deps.rs Badge]][Deps.rs] [![Discord Badge]][Discord Server]
//! [![Matrix Badge]][Matrix]<br>
//! [![Crate Badge]][Crate] [![Docs Badge]][API Docs] [![CI Badge]][CI Workflow] [![Deps.rs
//! Badge]][Deps.rs]<br> [![Codecov Badge]][Codecov] [![License Badge]](./LICENSE) [![Sponsors
//! Badge]][GitHub Sponsors]<br> [![Discord Badge]][Discord Server] [![Matrix Badge]][Matrix]
//! [![Forum Badge]][Forum]<br>
//!
//! [Ratatui Website] · [API Docs] · [Examples] · [Changelog] · [Breaking Changes]<br>
//! [Contributing] · [Report a bug] · [Request a Feature] · [Create a Pull Request]
@@ -44,6 +44,7 @@
//! ## Other documentation
//!
//! - [Ratatui Website] - explains the library's concepts and provides step-by-step tutorials
//! - [Ratatui Forum][Forum] - a place to ask questions and discuss the library
//! - [API Docs] - the full API documentation for the library on docs.rs.
//! - [Examples] - a collection of examples that demonstrate how to use the library.
//! - [Contributing] - Please read this if you are interested in contributing to the project.
@@ -318,6 +319,8 @@
//! [Docs Badge]: https://img.shields.io/docsrs/ratatui?logo=rust&style=flat-square&logoColor=E05D44
//! [Matrix Badge]: https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix&color=C43AC3
//! [Matrix]: https://matrix.to/#/#ratatui:matrix.org
//! [Forum Badge]: https://img.shields.io/discourse/likes?server=https%3A%2F%2Fforum.ratatui.rs&style=flat-square&logo=discourse&label=forum&color=C43AC3
//! [Forum]: https://forum.ratatui.rs
//! [Sponsors Badge]: https://img.shields.io/github/sponsors/ratatui-org?logo=github&style=flat-square&color=1370D3
// show the feature flags in the generated documentation
@@ -327,26 +330,24 @@
html_favicon_url = "https://raw.githubusercontent.com/ratatui-org/ratatui/main/assets/favicon.ico"
)]
pub mod backend;
pub mod buffer;
pub mod layout;
pub mod style;
pub mod symbols;
pub mod terminal;
pub mod text;
pub mod widgets;
#[doc(inline)]
pub use self::terminal::{CompletedFrame, Frame, Terminal, TerminalOptions, Viewport};
pub mod prelude;
/// re-export the `crossterm` crate so that users don't have to add it as a dependency
#[cfg(feature = "crossterm")]
pub use crossterm;
#[doc(inline)]
pub use terminal::{CompletedFrame, Frame, Terminal, TerminalOptions, Viewport};
/// re-export the `termion` crate so that users don't have to add it as a dependency
#[cfg(feature = "termion")]
pub use termion;
/// re-export the `termwiz` crate so that users don't have to add it as a dependency
#[cfg(feature = "termwiz")]
pub use termwiz;
pub mod backend;
pub mod buffer;
pub mod layout;
pub mod prelude;
pub mod style;
pub mod symbols;
pub mod terminal;
pub mod text;
pub mod widgets;

View File

@@ -27,10 +27,10 @@ pub(crate) use crate::widgets::{StatefulWidgetRef, WidgetRef};
pub use crate::{
backend::{self, Backend},
buffer::{self, Buffer},
layout::{self, Alignment, Constraint, Direction, Layout, Margin, Rect},
style::{self, Color, Modifier, Style, Styled, Stylize},
symbols::{self, Marker},
terminal::{CompletedFrame, Frame, Terminal, TerminalOptions, Viewport},
layout::{self, Alignment, Constraint, Direction, Layout, Margin, Position, Rect, Size},
style::{self, Color, Modifier, Style, Stylize},
symbols::{self},
terminal::{Frame, Terminal},
text::{self, Line, Masked, Span, Text},
widgets::{block::BlockExt, StatefulWidget, Widget},
};

View File

@@ -71,13 +71,14 @@
use std::fmt;
use bitflags::bitflags;
mod color;
mod stylize;
pub use color::{Color, ParseColorError};
pub use stylize::{Styled, Stylize};
mod color;
pub mod palette;
#[cfg(feature = "palette")]
mod palette_conversion;
mod stylize;
bitflags! {
/// Modifier changes the way a piece of text is displayed.

View File

@@ -0,0 +1,83 @@
//! Conversions from colors in the `palette` crate to [`Color`].
use ::palette::{
bool_mask::LazySelect,
num::{Arithmetics, MulSub, PartialCmp, Powf, Real},
LinSrgb,
};
use palette::{stimulus::IntoStimulus, Srgb};
use super::Color;
/// Convert an [`palette::Srgb`] color to a [`Color`].
///
/// # Examples
///
/// ```
/// use palette::Srgb;
/// use ratatui::style::Color;
///
/// let color = Color::from(Srgb::new(1.0f32, 0.0, 0.0));
/// assert_eq!(color, Color::Rgb(255, 0, 0));
/// ```
impl<T: IntoStimulus<u8>> From<Srgb<T>> for Color {
fn from(color: Srgb<T>) -> Self {
let (red, green, blue) = color.into_format().into_components();
Self::Rgb(red, green, blue)
}
}
/// Convert a [`palette::LinSrgb`] color to a [`Color`].
///
/// Note: this conversion only works for floating point linear sRGB colors. If you have a linear
/// sRGB color in another format, you need to convert it to floating point first.
///
/// # Examples
///
/// ```
/// use palette::LinSrgb;
/// use ratatui::style::Color;
///
/// let color = Color::from(LinSrgb::new(1.0f32, 0.0, 0.0));
/// assert_eq!(color, Color::Rgb(255, 0, 0));
/// ```
impl<T: IntoStimulus<u8>> From<LinSrgb<T>> for Color
where
T: Real + Powf + MulSub + Arithmetics + PartialCmp + Clone,
T::Mask: LazySelect<T>,
{
fn from(color: LinSrgb<T>) -> Self {
let srgb_color = Srgb::<T>::from_linear(color);
Self::from(srgb_color)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_srgb() {
const RED: Color = Color::Rgb(255, 0, 0);
assert_eq!(Color::from(Srgb::new(255u8, 0, 0)), RED);
assert_eq!(Color::from(Srgb::new(65535u16, 0, 0)), RED);
assert_eq!(Color::from(Srgb::new(1.0f32, 0.0, 0.0)), RED);
assert_eq!(
Color::from(Srgb::new(0.5f32, 0.5, 0.5)),
Color::Rgb(128, 128, 128)
);
}
#[test]
fn from_lin_srgb() {
const RED: Color = Color::Rgb(255, 0, 0);
assert_eq!(Color::from(LinSrgb::new(1.0f32, 0.0, 0.0)), RED);
assert_eq!(Color::from(LinSrgb::new(1.0f64, 0.0, 0.0)), RED);
assert_eq!(
Color::from(LinSrgb::new(0.5f32, 0.5, 0.5)),
Color::Rgb(188, 188, 188)
);
}
}

View File

@@ -1,5 +1,8 @@
use strum::{Display, EnumString};
pub mod border;
pub mod line;
pub mod block {
pub const FULL: &str = "";
pub const SEVEN_EIGHTHS: &str = "";
@@ -114,364 +117,6 @@ pub mod bar {
};
}
pub mod line {
pub const VERTICAL: &str = "";
pub const DOUBLE_VERTICAL: &str = "";
pub const THICK_VERTICAL: &str = "";
pub const HORIZONTAL: &str = "";
pub const DOUBLE_HORIZONTAL: &str = "";
pub const THICK_HORIZONTAL: &str = "";
pub const TOP_RIGHT: &str = "";
pub const ROUNDED_TOP_RIGHT: &str = "";
pub const DOUBLE_TOP_RIGHT: &str = "";
pub const THICK_TOP_RIGHT: &str = "";
pub const TOP_LEFT: &str = "";
pub const ROUNDED_TOP_LEFT: &str = "";
pub const DOUBLE_TOP_LEFT: &str = "";
pub const THICK_TOP_LEFT: &str = "";
pub const BOTTOM_RIGHT: &str = "";
pub const ROUNDED_BOTTOM_RIGHT: &str = "";
pub const DOUBLE_BOTTOM_RIGHT: &str = "";
pub const THICK_BOTTOM_RIGHT: &str = "";
pub const BOTTOM_LEFT: &str = "";
pub const ROUNDED_BOTTOM_LEFT: &str = "";
pub const DOUBLE_BOTTOM_LEFT: &str = "";
pub const THICK_BOTTOM_LEFT: &str = "";
pub const VERTICAL_LEFT: &str = "";
pub const DOUBLE_VERTICAL_LEFT: &str = "";
pub const THICK_VERTICAL_LEFT: &str = "";
pub const VERTICAL_RIGHT: &str = "";
pub const DOUBLE_VERTICAL_RIGHT: &str = "";
pub const THICK_VERTICAL_RIGHT: &str = "";
pub const HORIZONTAL_DOWN: &str = "";
pub const DOUBLE_HORIZONTAL_DOWN: &str = "";
pub const THICK_HORIZONTAL_DOWN: &str = "";
pub const HORIZONTAL_UP: &str = "";
pub const DOUBLE_HORIZONTAL_UP: &str = "";
pub const THICK_HORIZONTAL_UP: &str = "";
pub const CROSS: &str = "";
pub const DOUBLE_CROSS: &str = "";
pub const THICK_CROSS: &str = "";
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub struct Set {
pub vertical: &'static str,
pub horizontal: &'static str,
pub top_right: &'static str,
pub top_left: &'static str,
pub bottom_right: &'static str,
pub bottom_left: &'static str,
pub vertical_left: &'static str,
pub vertical_right: &'static str,
pub horizontal_down: &'static str,
pub horizontal_up: &'static str,
pub cross: &'static str,
}
impl Default for Set {
fn default() -> Self {
NORMAL
}
}
pub const NORMAL: Set = Set {
vertical: VERTICAL,
horizontal: HORIZONTAL,
top_right: TOP_RIGHT,
top_left: TOP_LEFT,
bottom_right: BOTTOM_RIGHT,
bottom_left: BOTTOM_LEFT,
vertical_left: VERTICAL_LEFT,
vertical_right: VERTICAL_RIGHT,
horizontal_down: HORIZONTAL_DOWN,
horizontal_up: HORIZONTAL_UP,
cross: CROSS,
};
pub const ROUNDED: Set = Set {
top_right: ROUNDED_TOP_RIGHT,
top_left: ROUNDED_TOP_LEFT,
bottom_right: ROUNDED_BOTTOM_RIGHT,
bottom_left: ROUNDED_BOTTOM_LEFT,
..NORMAL
};
pub const DOUBLE: Set = Set {
vertical: DOUBLE_VERTICAL,
horizontal: DOUBLE_HORIZONTAL,
top_right: DOUBLE_TOP_RIGHT,
top_left: DOUBLE_TOP_LEFT,
bottom_right: DOUBLE_BOTTOM_RIGHT,
bottom_left: DOUBLE_BOTTOM_LEFT,
vertical_left: DOUBLE_VERTICAL_LEFT,
vertical_right: DOUBLE_VERTICAL_RIGHT,
horizontal_down: DOUBLE_HORIZONTAL_DOWN,
horizontal_up: DOUBLE_HORIZONTAL_UP,
cross: DOUBLE_CROSS,
};
pub const THICK: Set = Set {
vertical: THICK_VERTICAL,
horizontal: THICK_HORIZONTAL,
top_right: THICK_TOP_RIGHT,
top_left: THICK_TOP_LEFT,
bottom_right: THICK_BOTTOM_RIGHT,
bottom_left: THICK_BOTTOM_LEFT,
vertical_left: THICK_VERTICAL_LEFT,
vertical_right: THICK_VERTICAL_RIGHT,
horizontal_down: THICK_HORIZONTAL_DOWN,
horizontal_up: THICK_HORIZONTAL_UP,
cross: THICK_CROSS,
};
}
pub mod border {
use super::line;
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub struct Set {
pub top_left: &'static str,
pub top_right: &'static str,
pub bottom_left: &'static str,
pub bottom_right: &'static str,
pub vertical_left: &'static str,
pub vertical_right: &'static str,
pub horizontal_top: &'static str,
pub horizontal_bottom: &'static str,
}
impl Default for Set {
fn default() -> Self {
PLAIN
}
}
/// Border Set with a single line width
///
/// ```text
/// ┌─────┐
/// │xxxxx│
/// │xxxxx│
/// └─────┘
pub const PLAIN: Set = Set {
top_left: line::NORMAL.top_left,
top_right: line::NORMAL.top_right,
bottom_left: line::NORMAL.bottom_left,
bottom_right: line::NORMAL.bottom_right,
vertical_left: line::NORMAL.vertical,
vertical_right: line::NORMAL.vertical,
horizontal_top: line::NORMAL.horizontal,
horizontal_bottom: line::NORMAL.horizontal,
};
/// Border Set with a single line width and rounded corners
///
/// ```text
/// ╭─────╮
/// │xxxxx│
/// │xxxxx│
/// ╰─────╯
pub const ROUNDED: Set = Set {
top_left: line::ROUNDED.top_left,
top_right: line::ROUNDED.top_right,
bottom_left: line::ROUNDED.bottom_left,
bottom_right: line::ROUNDED.bottom_right,
vertical_left: line::ROUNDED.vertical,
vertical_right: line::ROUNDED.vertical,
horizontal_top: line::ROUNDED.horizontal,
horizontal_bottom: line::ROUNDED.horizontal,
};
/// Border Set with a double line width
///
/// ```text
/// ╔═════╗
/// ║xxxxx║
/// ║xxxxx║
/// ╚═════╝
pub const DOUBLE: Set = Set {
top_left: line::DOUBLE.top_left,
top_right: line::DOUBLE.top_right,
bottom_left: line::DOUBLE.bottom_left,
bottom_right: line::DOUBLE.bottom_right,
vertical_left: line::DOUBLE.vertical,
vertical_right: line::DOUBLE.vertical,
horizontal_top: line::DOUBLE.horizontal,
horizontal_bottom: line::DOUBLE.horizontal,
};
/// Border Set with a thick line width
///
/// ```text
/// ┏━━━━━┓
/// ┃xxxxx┃
/// ┃xxxxx┃
/// ┗━━━━━┛
pub const THICK: Set = Set {
top_left: line::THICK.top_left,
top_right: line::THICK.top_right,
bottom_left: line::THICK.bottom_left,
bottom_right: line::THICK.bottom_right,
vertical_left: line::THICK.vertical,
vertical_right: line::THICK.vertical,
horizontal_top: line::THICK.horizontal,
horizontal_bottom: line::THICK.horizontal,
};
pub const QUADRANT_TOP_LEFT: &str = "";
pub const QUADRANT_TOP_RIGHT: &str = "";
pub const QUADRANT_BOTTOM_LEFT: &str = "";
pub const QUADRANT_BOTTOM_RIGHT: &str = "";
pub const QUADRANT_TOP_HALF: &str = "";
pub const QUADRANT_BOTTOM_HALF: &str = "";
pub const QUADRANT_LEFT_HALF: &str = "";
pub const QUADRANT_RIGHT_HALF: &str = "";
pub const QUADRANT_TOP_LEFT_BOTTOM_LEFT_BOTTOM_RIGHT: &str = "";
pub const QUADRANT_TOP_LEFT_TOP_RIGHT_BOTTOM_LEFT: &str = "";
pub const QUADRANT_TOP_LEFT_TOP_RIGHT_BOTTOM_RIGHT: &str = "";
pub const QUADRANT_TOP_RIGHT_BOTTOM_LEFT_BOTTOM_RIGHT: &str = "";
pub const QUADRANT_TOP_LEFT_BOTTOM_RIGHT: &str = "";
pub const QUADRANT_TOP_RIGHT_BOTTOM_LEFT: &str = "";
pub const QUADRANT_BLOCK: &str = "";
/// Quadrant used for setting a border outside a block by one half cell "pixel".
///
/// ```text
/// ▛▀▀▀▀▀▜
/// ▌xxxxx▐
/// ▌xxxxx▐
/// ▙▄▄▄▄▄▟
/// ```
pub const QUADRANT_OUTSIDE: Set = Set {
top_left: QUADRANT_TOP_LEFT_TOP_RIGHT_BOTTOM_LEFT,
top_right: QUADRANT_TOP_LEFT_TOP_RIGHT_BOTTOM_RIGHT,
bottom_left: QUADRANT_TOP_LEFT_BOTTOM_LEFT_BOTTOM_RIGHT,
bottom_right: QUADRANT_TOP_RIGHT_BOTTOM_LEFT_BOTTOM_RIGHT,
vertical_left: QUADRANT_LEFT_HALF,
vertical_right: QUADRANT_RIGHT_HALF,
horizontal_top: QUADRANT_TOP_HALF,
horizontal_bottom: QUADRANT_BOTTOM_HALF,
};
/// Quadrant used for setting a border inside a block by one half cell "pixel".
///
/// ```text
/// ▗▄▄▄▄▄▖
/// ▐xxxxx▌
/// ▐xxxxx▌
/// ▝▀▀▀▀▀▘
/// ```
pub const QUADRANT_INSIDE: Set = Set {
top_right: QUADRANT_BOTTOM_LEFT,
top_left: QUADRANT_BOTTOM_RIGHT,
bottom_right: QUADRANT_TOP_LEFT,
bottom_left: QUADRANT_TOP_RIGHT,
vertical_left: QUADRANT_RIGHT_HALF,
vertical_right: QUADRANT_LEFT_HALF,
horizontal_top: QUADRANT_BOTTOM_HALF,
horizontal_bottom: QUADRANT_TOP_HALF,
};
pub const ONE_EIGHTH_TOP_EIGHT: &str = "";
pub const ONE_EIGHTH_BOTTOM_EIGHT: &str = "";
pub const ONE_EIGHTH_LEFT_EIGHT: &str = "";
pub const ONE_EIGHTH_RIGHT_EIGHT: &str = "";
/// Wide border set based on McGugan box technique
///
/// ```text
/// ▁▁▁▁▁▁▁
/// ▏xxxxx▕
/// ▏xxxxx▕
/// ▔▔▔▔▔▔▔
/// ```
#[allow(clippy::doc_markdown)]
pub const ONE_EIGHTH_WIDE: Set = Set {
top_right: ONE_EIGHTH_BOTTOM_EIGHT,
top_left: ONE_EIGHTH_BOTTOM_EIGHT,
bottom_right: ONE_EIGHTH_TOP_EIGHT,
bottom_left: ONE_EIGHTH_TOP_EIGHT,
vertical_left: ONE_EIGHTH_LEFT_EIGHT,
vertical_right: ONE_EIGHTH_RIGHT_EIGHT,
horizontal_top: ONE_EIGHTH_BOTTOM_EIGHT,
horizontal_bottom: ONE_EIGHTH_TOP_EIGHT,
};
/// Tall border set based on McGugan box technique
///
/// ```text
/// ▕▔▔▏
/// ▕xx▏
/// ▕xx▏
/// ▕▁▁▏
/// ```
#[allow(clippy::doc_markdown)]
pub const ONE_EIGHTH_TALL: Set = Set {
top_right: ONE_EIGHTH_LEFT_EIGHT,
top_left: ONE_EIGHTH_RIGHT_EIGHT,
bottom_right: ONE_EIGHTH_LEFT_EIGHT,
bottom_left: ONE_EIGHTH_RIGHT_EIGHT,
vertical_left: ONE_EIGHTH_RIGHT_EIGHT,
vertical_right: ONE_EIGHTH_LEFT_EIGHT,
horizontal_top: ONE_EIGHTH_TOP_EIGHT,
horizontal_bottom: ONE_EIGHTH_BOTTOM_EIGHT,
};
/// Wide proportional (visually equal width and height) border with using set of quadrants.
///
/// The border is created by using half blocks for top and bottom, and full
/// blocks for right and left sides to make horizontal and vertical borders seem equal.
///
/// ```text
/// ▄▄▄▄
/// █xx█
/// █xx█
/// ▀▀▀▀
/// ```
pub const PROPORTIONAL_WIDE: Set = Set {
top_right: QUADRANT_BOTTOM_HALF,
top_left: QUADRANT_BOTTOM_HALF,
bottom_right: QUADRANT_TOP_HALF,
bottom_left: QUADRANT_TOP_HALF,
vertical_left: QUADRANT_BLOCK,
vertical_right: QUADRANT_BLOCK,
horizontal_top: QUADRANT_BOTTOM_HALF,
horizontal_bottom: QUADRANT_TOP_HALF,
};
/// Tall proportional (visually equal width and height) border with using set of quadrants.
///
/// The border is created by using full blocks for all sides, except for the top and bottom,
/// which use half blocks to make horizontal and vertical borders seem equal.
///
/// ```text
/// ▕█▀▀█
/// ▕█xx█
/// ▕█xx█
/// ▕█▄▄█
/// ```
pub const PROPORTIONAL_TALL: Set = Set {
top_right: QUADRANT_BLOCK,
top_left: QUADRANT_BLOCK,
bottom_right: QUADRANT_BLOCK,
bottom_left: QUADRANT_BLOCK,
vertical_left: QUADRANT_BLOCK,
vertical_right: QUADRANT_BLOCK,
horizontal_top: QUADRANT_TOP_HALF,
horizontal_bottom: QUADRANT_BOTTOM_HALF,
};
}
pub const DOT: &str = "";
pub mod braille {

509
src/symbols/border.rs Normal file
View File

@@ -0,0 +1,509 @@
use super::{block, line};
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub struct Set {
pub top_left: &'static str,
pub top_right: &'static str,
pub bottom_left: &'static str,
pub bottom_right: &'static str,
pub vertical_left: &'static str,
pub vertical_right: &'static str,
pub horizontal_top: &'static str,
pub horizontal_bottom: &'static str,
}
impl Default for Set {
fn default() -> Self {
PLAIN
}
}
/// Border Set with a single line width
///
/// ```text
/// ┌─────┐
/// │xxxxx│
/// │xxxxx│
/// └─────┘
/// ```
pub const PLAIN: Set = Set {
top_left: line::NORMAL.top_left,
top_right: line::NORMAL.top_right,
bottom_left: line::NORMAL.bottom_left,
bottom_right: line::NORMAL.bottom_right,
vertical_left: line::NORMAL.vertical,
vertical_right: line::NORMAL.vertical,
horizontal_top: line::NORMAL.horizontal,
horizontal_bottom: line::NORMAL.horizontal,
};
/// Border Set with a single line width and rounded corners
///
/// ```text
/// ╭─────╮
/// │xxxxx│
/// │xxxxx│
/// ╰─────╯
/// ```
pub const ROUNDED: Set = Set {
top_left: line::ROUNDED.top_left,
top_right: line::ROUNDED.top_right,
bottom_left: line::ROUNDED.bottom_left,
bottom_right: line::ROUNDED.bottom_right,
vertical_left: line::ROUNDED.vertical,
vertical_right: line::ROUNDED.vertical,
horizontal_top: line::ROUNDED.horizontal,
horizontal_bottom: line::ROUNDED.horizontal,
};
/// Border Set with a double line width
///
/// ```text
/// ╔═════╗
/// ║xxxxx║
/// ║xxxxx║
/// ╚═════╝
/// ```
pub const DOUBLE: Set = Set {
top_left: line::DOUBLE.top_left,
top_right: line::DOUBLE.top_right,
bottom_left: line::DOUBLE.bottom_left,
bottom_right: line::DOUBLE.bottom_right,
vertical_left: line::DOUBLE.vertical,
vertical_right: line::DOUBLE.vertical,
horizontal_top: line::DOUBLE.horizontal,
horizontal_bottom: line::DOUBLE.horizontal,
};
/// Border Set with a thick line width
///
/// ```text
/// ┏━━━━━┓
/// ┃xxxxx┃
/// ┃xxxxx┃
/// ┗━━━━━┛
/// ```
pub const THICK: Set = Set {
top_left: line::THICK.top_left,
top_right: line::THICK.top_right,
bottom_left: line::THICK.bottom_left,
bottom_right: line::THICK.bottom_right,
vertical_left: line::THICK.vertical,
vertical_right: line::THICK.vertical,
horizontal_top: line::THICK.horizontal,
horizontal_bottom: line::THICK.horizontal,
};
pub const QUADRANT_TOP_LEFT: &str = "";
pub const QUADRANT_TOP_RIGHT: &str = "";
pub const QUADRANT_BOTTOM_LEFT: &str = "";
pub const QUADRANT_BOTTOM_RIGHT: &str = "";
pub const QUADRANT_TOP_HALF: &str = "";
pub const QUADRANT_BOTTOM_HALF: &str = "";
pub const QUADRANT_LEFT_HALF: &str = "";
pub const QUADRANT_RIGHT_HALF: &str = "";
pub const QUADRANT_TOP_LEFT_BOTTOM_LEFT_BOTTOM_RIGHT: &str = "";
pub const QUADRANT_TOP_LEFT_TOP_RIGHT_BOTTOM_LEFT: &str = "";
pub const QUADRANT_TOP_LEFT_TOP_RIGHT_BOTTOM_RIGHT: &str = "";
pub const QUADRANT_TOP_RIGHT_BOTTOM_LEFT_BOTTOM_RIGHT: &str = "";
pub const QUADRANT_TOP_LEFT_BOTTOM_RIGHT: &str = "";
pub const QUADRANT_TOP_RIGHT_BOTTOM_LEFT: &str = "";
pub const QUADRANT_BLOCK: &str = "";
/// Quadrant used for setting a border outside a block by one half cell "pixel".
///
/// ```text
/// ▛▀▀▀▀▀▜
/// ▌xxxxx▐
/// ▌xxxxx▐
/// ▙▄▄▄▄▄▟
/// ```
pub const QUADRANT_OUTSIDE: Set = Set {
top_left: QUADRANT_TOP_LEFT_TOP_RIGHT_BOTTOM_LEFT,
top_right: QUADRANT_TOP_LEFT_TOP_RIGHT_BOTTOM_RIGHT,
bottom_left: QUADRANT_TOP_LEFT_BOTTOM_LEFT_BOTTOM_RIGHT,
bottom_right: QUADRANT_TOP_RIGHT_BOTTOM_LEFT_BOTTOM_RIGHT,
vertical_left: QUADRANT_LEFT_HALF,
vertical_right: QUADRANT_RIGHT_HALF,
horizontal_top: QUADRANT_TOP_HALF,
horizontal_bottom: QUADRANT_BOTTOM_HALF,
};
/// Quadrant used for setting a border inside a block by one half cell "pixel".
///
/// ```text
/// ▗▄▄▄▄▄▖
/// ▐xxxxx▌
/// ▐xxxxx▌
/// ▝▀▀▀▀▀▘
/// ```
pub const QUADRANT_INSIDE: Set = Set {
top_right: QUADRANT_BOTTOM_LEFT,
top_left: QUADRANT_BOTTOM_RIGHT,
bottom_right: QUADRANT_TOP_LEFT,
bottom_left: QUADRANT_TOP_RIGHT,
vertical_left: QUADRANT_RIGHT_HALF,
vertical_right: QUADRANT_LEFT_HALF,
horizontal_top: QUADRANT_BOTTOM_HALF,
horizontal_bottom: QUADRANT_TOP_HALF,
};
pub const ONE_EIGHTH_TOP_EIGHT: &str = "";
pub const ONE_EIGHTH_BOTTOM_EIGHT: &str = "";
pub const ONE_EIGHTH_LEFT_EIGHT: &str = "";
pub const ONE_EIGHTH_RIGHT_EIGHT: &str = "";
/// Wide border set based on McGugan box technique
///
/// ```text
/// ▁▁▁▁▁▁▁
/// ▏xxxxx▕
/// ▏xxxxx▕
/// ▔▔▔▔▔▔▔
/// ```
#[allow(clippy::doc_markdown)]
pub const ONE_EIGHTH_WIDE: Set = Set {
top_right: ONE_EIGHTH_BOTTOM_EIGHT,
top_left: ONE_EIGHTH_BOTTOM_EIGHT,
bottom_right: ONE_EIGHTH_TOP_EIGHT,
bottom_left: ONE_EIGHTH_TOP_EIGHT,
vertical_left: ONE_EIGHTH_LEFT_EIGHT,
vertical_right: ONE_EIGHTH_RIGHT_EIGHT,
horizontal_top: ONE_EIGHTH_BOTTOM_EIGHT,
horizontal_bottom: ONE_EIGHTH_TOP_EIGHT,
};
/// Tall border set based on McGugan box technique
///
/// ```text
/// ▕▔▔▏
/// ▕xx▏
/// ▕xx▏
/// ▕▁▁▏
/// ```
#[allow(clippy::doc_markdown)]
pub const ONE_EIGHTH_TALL: Set = Set {
top_right: ONE_EIGHTH_LEFT_EIGHT,
top_left: ONE_EIGHTH_RIGHT_EIGHT,
bottom_right: ONE_EIGHTH_LEFT_EIGHT,
bottom_left: ONE_EIGHTH_RIGHT_EIGHT,
vertical_left: ONE_EIGHTH_RIGHT_EIGHT,
vertical_right: ONE_EIGHTH_LEFT_EIGHT,
horizontal_top: ONE_EIGHTH_TOP_EIGHT,
horizontal_bottom: ONE_EIGHTH_BOTTOM_EIGHT,
};
/// Wide proportional (visually equal width and height) border with using set of quadrants.
///
/// The border is created by using half blocks for top and bottom, and full
/// blocks for right and left sides to make horizontal and vertical borders seem equal.
///
/// ```text
/// ▄▄▄▄
/// █xx█
/// █xx█
/// ▀▀▀▀
/// ```
pub const PROPORTIONAL_WIDE: Set = Set {
top_right: QUADRANT_BOTTOM_HALF,
top_left: QUADRANT_BOTTOM_HALF,
bottom_right: QUADRANT_TOP_HALF,
bottom_left: QUADRANT_TOP_HALF,
vertical_left: QUADRANT_BLOCK,
vertical_right: QUADRANT_BLOCK,
horizontal_top: QUADRANT_BOTTOM_HALF,
horizontal_bottom: QUADRANT_TOP_HALF,
};
/// Tall proportional (visually equal width and height) border with using set of quadrants.
///
/// The border is created by using full blocks for all sides, except for the top and bottom,
/// which use half blocks to make horizontal and vertical borders seem equal.
///
/// ```text
/// ▕█▀▀█
/// ▕█xx█
/// ▕█xx█
/// ▕█▄▄█
/// ```
pub const PROPORTIONAL_TALL: Set = Set {
top_right: QUADRANT_BLOCK,
top_left: QUADRANT_BLOCK,
bottom_right: QUADRANT_BLOCK,
bottom_left: QUADRANT_BLOCK,
vertical_left: QUADRANT_BLOCK,
vertical_right: QUADRANT_BLOCK,
horizontal_top: QUADRANT_TOP_HALF,
horizontal_bottom: QUADRANT_BOTTOM_HALF,
};
/// Solid border set
///
/// The border is created by using full blocks for all sides.
///
/// ```text
/// ████
/// █xx█
/// █xx█
/// ████
/// ```
pub const FULL: Set = Set {
top_left: block::FULL,
top_right: block::FULL,
bottom_left: block::FULL,
bottom_right: block::FULL,
vertical_left: block::FULL,
vertical_right: block::FULL,
horizontal_top: block::FULL,
horizontal_bottom: block::FULL,
};
/// Empty border set
///
/// The border is created by using empty strings for all sides.
///
/// This is useful for ensuring that the border style is applied to a border on a block with a title
/// without actually drawing a border.
///
/// ░ Example
///
/// `░` represents the content in the area not covered by the border to make it easier to see the
/// blank symbols.
///
/// ```text
/// ░░░░░░░░
/// ░░ ░░
/// ░░ ░░ ░░
/// ░░ ░░ ░░
/// ░░ ░░
/// ░░░░░░░░
/// ```
pub const EMPTY: Set = Set {
top_left: " ",
top_right: " ",
bottom_left: " ",
bottom_right: " ",
vertical_left: " ",
vertical_right: " ",
horizontal_top: " ",
horizontal_bottom: " ",
};
#[cfg(test)]
mod tests {
use indoc::{formatdoc, indoc};
use super::*;
#[test]
fn default() {
assert_eq!(Set::default(), PLAIN);
}
/// A helper function to render a border set to a string.
///
/// '░' (U+2591 Light Shade) is used as a placeholder for empty space to make it easier to see
/// the size of the border symbols.
fn render(set: Set) -> String {
formatdoc!(
"░░░░░░
░{}{}{}{}░
░{}░░{}░
░{}░░{}░
░{}{}{}{}░
░░░░░░",
set.top_left,
set.horizontal_top,
set.horizontal_top,
set.top_right,
set.vertical_left,
set.vertical_right,
set.vertical_left,
set.vertical_right,
set.bottom_left,
set.horizontal_bottom,
set.horizontal_bottom,
set.bottom_right
)
}
#[test]
fn plain() {
assert_eq!(
render(PLAIN),
indoc!(
"░░░░░░
░┌──┐░
░│░░│░
░│░░│░
░└──┘░
░░░░░░"
)
);
}
#[test]
fn rounded() {
assert_eq!(
render(ROUNDED),
indoc!(
"░░░░░░
░╭──╮░
░│░░│░
░│░░│░
░╰──╯░
░░░░░░"
)
);
}
#[test]
fn double() {
assert_eq!(
render(DOUBLE),
indoc!(
"░░░░░░
░╔══╗░
░║░░║░
░║░░║░
░╚══╝░
░░░░░░"
)
);
}
#[test]
fn thick() {
assert_eq!(
render(THICK),
indoc!(
"░░░░░░
░┏━━┓░
░┃░░┃░
░┃░░┃░
░┗━━┛░
░░░░░░"
)
);
}
#[test]
fn quadrant_outside() {
assert_eq!(
render(QUADRANT_OUTSIDE),
indoc!(
"░░░░░░
░▛▀▀▜░
░▌░░▐░
░▌░░▐░
░▙▄▄▟░
░░░░░░"
)
);
}
#[test]
fn quadrant_inside() {
assert_eq!(
render(QUADRANT_INSIDE),
indoc!(
"░░░░░░
░▗▄▄▖░
░▐░░▌░
░▐░░▌░
░▝▀▀▘░
░░░░░░"
)
);
}
#[test]
fn one_eighth_wide() {
assert_eq!(
render(ONE_EIGHTH_WIDE),
indoc!(
"░░░░░░
░▁▁▁▁░
░▏░░▕░
░▏░░▕░
░▔▔▔▔░
░░░░░░"
)
);
}
#[test]
fn one_eighth_tall() {
assert_eq!(
render(ONE_EIGHTH_TALL),
indoc!(
"░░░░░░
░▕▔▔▏░
░▕░░▏░
░▕░░▏░
░▕▁▁▏░
░░░░░░"
)
);
}
#[test]
fn proportional_wide() {
assert_eq!(
render(PROPORTIONAL_WIDE),
indoc!(
"░░░░░░
░▄▄▄▄░
░█░░█░
░█░░█░
░▀▀▀▀░
░░░░░░"
)
);
}
#[test]
fn proportional_tall() {
assert_eq!(
render(PROPORTIONAL_TALL),
indoc!(
"░░░░░░
░█▀▀█░
░█░░█░
░█░░█░
░█▄▄█░
░░░░░░"
)
);
}
#[test]
fn full() {
assert_eq!(
render(FULL),
indoc!(
"░░░░░░
░████░
░█░░█░
░█░░█░
░████░
░░░░░░"
)
);
}
#[test]
fn empty() {
assert_eq!(
render(EMPTY),
indoc!(
"░░░░░░
░ ░
░ ░░ ░
░ ░░ ░
░ ░
░░░░░░"
)
);
}
}

208
src/symbols/line.rs Normal file
View File

@@ -0,0 +1,208 @@
pub const VERTICAL: &str = "";
pub const DOUBLE_VERTICAL: &str = "";
pub const THICK_VERTICAL: &str = "";
pub const HORIZONTAL: &str = "";
pub const DOUBLE_HORIZONTAL: &str = "";
pub const THICK_HORIZONTAL: &str = "";
pub const TOP_RIGHT: &str = "";
pub const ROUNDED_TOP_RIGHT: &str = "";
pub const DOUBLE_TOP_RIGHT: &str = "";
pub const THICK_TOP_RIGHT: &str = "";
pub const TOP_LEFT: &str = "";
pub const ROUNDED_TOP_LEFT: &str = "";
pub const DOUBLE_TOP_LEFT: &str = "";
pub const THICK_TOP_LEFT: &str = "";
pub const BOTTOM_RIGHT: &str = "";
pub const ROUNDED_BOTTOM_RIGHT: &str = "";
pub const DOUBLE_BOTTOM_RIGHT: &str = "";
pub const THICK_BOTTOM_RIGHT: &str = "";
pub const BOTTOM_LEFT: &str = "";
pub const ROUNDED_BOTTOM_LEFT: &str = "";
pub const DOUBLE_BOTTOM_LEFT: &str = "";
pub const THICK_BOTTOM_LEFT: &str = "";
pub const VERTICAL_LEFT: &str = "";
pub const DOUBLE_VERTICAL_LEFT: &str = "";
pub const THICK_VERTICAL_LEFT: &str = "";
pub const VERTICAL_RIGHT: &str = "";
pub const DOUBLE_VERTICAL_RIGHT: &str = "";
pub const THICK_VERTICAL_RIGHT: &str = "";
pub const HORIZONTAL_DOWN: &str = "";
pub const DOUBLE_HORIZONTAL_DOWN: &str = "";
pub const THICK_HORIZONTAL_DOWN: &str = "";
pub const HORIZONTAL_UP: &str = "";
pub const DOUBLE_HORIZONTAL_UP: &str = "";
pub const THICK_HORIZONTAL_UP: &str = "";
pub const CROSS: &str = "";
pub const DOUBLE_CROSS: &str = "";
pub const THICK_CROSS: &str = "";
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub struct Set {
pub vertical: &'static str,
pub horizontal: &'static str,
pub top_right: &'static str,
pub top_left: &'static str,
pub bottom_right: &'static str,
pub bottom_left: &'static str,
pub vertical_left: &'static str,
pub vertical_right: &'static str,
pub horizontal_down: &'static str,
pub horizontal_up: &'static str,
pub cross: &'static str,
}
impl Default for Set {
fn default() -> Self {
NORMAL
}
}
pub const NORMAL: Set = Set {
vertical: VERTICAL,
horizontal: HORIZONTAL,
top_right: TOP_RIGHT,
top_left: TOP_LEFT,
bottom_right: BOTTOM_RIGHT,
bottom_left: BOTTOM_LEFT,
vertical_left: VERTICAL_LEFT,
vertical_right: VERTICAL_RIGHT,
horizontal_down: HORIZONTAL_DOWN,
horizontal_up: HORIZONTAL_UP,
cross: CROSS,
};
pub const ROUNDED: Set = Set {
top_right: ROUNDED_TOP_RIGHT,
top_left: ROUNDED_TOP_LEFT,
bottom_right: ROUNDED_BOTTOM_RIGHT,
bottom_left: ROUNDED_BOTTOM_LEFT,
..NORMAL
};
pub const DOUBLE: Set = Set {
vertical: DOUBLE_VERTICAL,
horizontal: DOUBLE_HORIZONTAL,
top_right: DOUBLE_TOP_RIGHT,
top_left: DOUBLE_TOP_LEFT,
bottom_right: DOUBLE_BOTTOM_RIGHT,
bottom_left: DOUBLE_BOTTOM_LEFT,
vertical_left: DOUBLE_VERTICAL_LEFT,
vertical_right: DOUBLE_VERTICAL_RIGHT,
horizontal_down: DOUBLE_HORIZONTAL_DOWN,
horizontal_up: DOUBLE_HORIZONTAL_UP,
cross: DOUBLE_CROSS,
};
pub const THICK: Set = Set {
vertical: THICK_VERTICAL,
horizontal: THICK_HORIZONTAL,
top_right: THICK_TOP_RIGHT,
top_left: THICK_TOP_LEFT,
bottom_right: THICK_BOTTOM_RIGHT,
bottom_left: THICK_BOTTOM_LEFT,
vertical_left: THICK_VERTICAL_LEFT,
vertical_right: THICK_VERTICAL_RIGHT,
horizontal_down: THICK_HORIZONTAL_DOWN,
horizontal_up: THICK_HORIZONTAL_UP,
cross: THICK_CROSS,
};
#[cfg(test)]
mod tests {
use indoc::{formatdoc, indoc};
use super::*;
#[test]
fn default() {
assert_eq!(Set::default(), NORMAL);
}
/// A helper function to render a set of symbols.
fn render(set: Set) -> String {
formatdoc!(
"{}{}{}{}
{}{}{}{}
{}{}{}{}
{}{}{}{}",
set.top_left,
set.horizontal,
set.horizontal_down,
set.top_right,
set.vertical,
" ",
set.vertical,
set.vertical,
set.vertical_right,
set.horizontal,
set.cross,
set.vertical_left,
set.bottom_left,
set.horizontal,
set.horizontal_up,
set.bottom_right
)
}
#[test]
fn normal() {
assert_eq!(
render(NORMAL),
indoc!(
"┌─┬┐
│ ││
├─┼┤
└─┴┘"
)
);
}
#[test]
fn rounded() {
assert_eq!(
render(ROUNDED),
indoc!(
"╭─┬╮
│ ││
├─┼┤
╰─┴╯"
)
);
}
#[test]
fn double() {
assert_eq!(
render(DOUBLE),
indoc!(
"╔═╦╗
║ ║║
╠═╬╣
╚═╩╝"
)
);
}
#[test]
fn thick() {
assert_eq!(
render(THICK),
indoc!(
"┏━┳┓
┃ ┃┃
┣━╋┫
┗━┻┛"
)
);
}
}

View File

@@ -94,7 +94,7 @@ impl Frame<'_> {
/// # }
/// ```
#[allow(clippy::needless_pass_by_value)]
#[stability::unstable(feature = "widget-ref")]
#[instability::unstable(feature = "widget-ref")]
pub fn render_widget_ref<W: WidgetRef>(&mut self, widget: W, area: Rect) {
widget.render_ref(area, self.buffer);
}
@@ -152,7 +152,7 @@ impl Frame<'_> {
/// # }
/// ```
#[allow(clippy::needless_pass_by_value)]
#[stability::unstable(feature = "widget-ref")]
#[instability::unstable(feature = "widget-ref")]
pub fn render_stateful_widget_ref<W>(&mut self, widget: W, area: Rect, state: &mut W::State)
where
W: StatefulWidgetRef,

View File

@@ -1,6 +1,6 @@
use std::io;
use crate::{backend::ClearType, prelude::*};
use crate::{backend::ClearType, prelude::*, CompletedFrame, TerminalOptions, Viewport};
/// An interface to interact and draw [`Frame`]s on the user's terminal.
///
@@ -126,7 +126,7 @@ where
///
/// ```rust
/// # use std::io::stdout;
/// # use ratatui::{prelude::*, backend::TestBackend};
/// # use ratatui::{prelude::*, backend::TestBackend, terminal::{Viewport, TerminalOptions}};
/// let backend = CrosstermBackend::new(stdout());
/// let viewport = Viewport::Fixed(Rect::new(0, 0, 10, 10));
/// let terminal = Terminal::with_options(backend, TerminalOptions { viewport })?;

View File

@@ -48,14 +48,14 @@ mod grapheme;
pub use grapheme::StyledGrapheme;
mod line;
pub use line::Line;
pub use line::{Line, ToLine};
mod masked;
pub use masked::Masked;
mod span;
pub use span::Span;
pub use span::{Span, ToSpan};
#[allow(clippy::module_inception)]
mod text;
pub use text::Text;
pub use text::{Text, ToText};

View File

@@ -1,4 +1,7 @@
use crate::prelude::*;
use crate::{prelude::*, style::Styled};
const NBSP: &str = "\u{00a0}";
const ZWSP: &str = "\u{200b}";
/// A grapheme associated to a style.
/// Note that, although `StyledGrapheme` is the smallest divisible unit of text,
@@ -22,6 +25,11 @@ impl<'a> StyledGrapheme<'a> {
style: style.into(),
}
}
pub(crate) fn is_whitespace(&self) -> bool {
let symbol = self.symbol;
symbol == ZWSP || symbol.chars().all(char::is_whitespace) && symbol != NBSP
}
}
impl<'a> Styled for StyledGrapheme<'a> {

View File

@@ -4,8 +4,7 @@ use std::{borrow::Cow, fmt};
use unicode_truncate::UnicodeTruncateStr;
use super::StyledGrapheme;
use crate::prelude::*;
use crate::{prelude::*, style::Styled, text::StyledGrapheme};
/// A line of text, consisting of one or more [`Span`]s.
///
@@ -13,6 +12,9 @@ use crate::prelude::*;
/// text. When a [`Line`] is rendered, it is rendered as a single line of text, with each [`Span`]
/// being rendered in order (left to right).
///
/// Any newlines in the content are removed when creating a [`Line`] using the constructor or
/// conversion methods.
///
/// # Constructor Methods
///
/// - [`Line::default`] creates a line with empty content and the default style.
@@ -503,13 +505,13 @@ impl<'a> IntoIterator for &'a mut Line<'a> {
impl<'a> From<String> for Line<'a> {
fn from(s: String) -> Self {
Self::from(vec![Span::from(s)])
Self::raw(s)
}
}
impl<'a> From<&'a str> for Line<'a> {
fn from(s: &'a str) -> Self {
Self::from(vec![Span::from(s)])
Self::raw(s)
}
}
@@ -647,6 +649,29 @@ fn spans_after_width<'a>(
})
}
/// A trait for converting a value to a [`Line`].
///
/// This trait is automatically implemented for any type that implements the [`Display`] trait. As
/// such, `ToLine` shouln't be implemented directly: [`Display`] should be implemented instead, and
/// you get the `ToLine` implementation for free.
///
/// [`Display`]: std::fmt::Display
pub trait ToLine {
/// Converts the value to a [`Line`].
fn to_line(&self) -> Line<'_>;
}
/// # Panics
///
/// In this implementation, the `to_line` method panics if the `Display` implementation returns an
/// error. This indicates an incorrect `Display` implementation since `fmt::Write for String` never
/// returns an error itself.
impl<T: fmt::Display> ToLine for T {
fn to_line(&self) -> Line<'_> {
Line::from(self.to_string())
}
}
impl fmt::Display for Line<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for span in &self.spans {
@@ -804,14 +829,28 @@ mod tests {
fn from_string() {
let s = String::from("Hello, world!");
let line = Line::from(s);
assert_eq!(vec![Span::from("Hello, world!")], line.spans);
assert_eq!(line.spans, vec![Span::from("Hello, world!")]);
let s = String::from("Hello\nworld!");
let line = Line::from(s);
assert_eq!(line.spans, vec![Span::from("Hello"), Span::from("world!")]);
}
#[test]
fn from_str() {
let s = "Hello, world!";
let line = Line::from(s);
assert_eq!(vec![Span::from("Hello, world!")], line.spans);
assert_eq!(line.spans, vec![Span::from("Hello, world!")]);
let s = "Hello\nworld!";
let line = Line::from(s);
assert_eq!(line.spans, vec![Span::from("Hello"), Span::from("world!")]);
}
#[test]
fn to_line() {
let line = 42.to_line();
assert_eq!(vec![Span::from("42")], line.spans);
}
#[test]
@@ -821,7 +860,7 @@ mod tests {
Span::styled(" world!", Style::default().fg(Color::Green)),
];
let line = Line::from(spans.clone());
assert_eq!(spans, line.spans);
assert_eq!(line.spans, spans);
}
#[test]
@@ -854,7 +893,7 @@ mod tests {
fn from_span() {
let span = Span::styled("Hello, world!", Style::default().fg(Color::Yellow));
let line = Line::from(span.clone());
assert_eq!(vec![span], line.spans);
assert_eq!(line.spans, vec![span],);
}
#[test]
@@ -864,7 +903,7 @@ mod tests {
Span::styled(" world!", Style::default().fg(Color::Green)),
]);
let s: String = line.into();
assert_eq!("Hello, world!", s);
assert_eq!(s, "Hello, world!");
}
#[test]

View File

@@ -3,8 +3,7 @@ use std::{borrow::Cow, fmt};
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use super::StyledGrapheme;
use crate::prelude::*;
use crate::{prelude::*, style::Styled, text::StyledGrapheme};
/// Represents a part of a line that is contiguous and where all characters share the same style.
///
@@ -363,38 +362,73 @@ impl Widget for Span<'_> {
impl WidgetRef for Span<'_> {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
let area = area.intersection(buf.area);
let Rect {
x: mut current_x,
y,
width,
..
} = area;
let max_x = Ord::min(current_x.saturating_add(width), buf.area.right());
for g in self.styled_graphemes(Style::default()) {
let symbol_width = g.symbol.width();
let next_x = current_x.saturating_add(symbol_width as u16);
if next_x > max_x {
let Rect { mut x, y, .. } = area.intersection(buf.area);
for (i, grapheme) in self.styled_graphemes(Style::default()).enumerate() {
let symbol_width = grapheme.symbol.width();
let next_x = x.saturating_add(symbol_width as u16);
if next_x > area.intersection(buf.area).right() {
break;
}
buf.get_mut(current_x, y)
.set_symbol(g.symbol)
.set_style(g.style);
if i == 0 {
// the first grapheme is always set on the cell
buf.get_mut(x, y)
.set_symbol(grapheme.symbol)
.set_style(grapheme.style);
} else if x == area.x {
// there is one or more zero-width graphemes in the first cell, so the first cell
// must be appended to.
buf.get_mut(x, y)
.append_symbol(grapheme.symbol)
.set_style(grapheme.style);
} else if symbol_width == 0 {
// append zero-width graphemes to the previous cell
buf.get_mut(x - 1, y)
.append_symbol(grapheme.symbol)
.set_style(grapheme.style);
} else {
// just a normal grapheme (not first, not zero-width, not overflowing the area)
buf.get_mut(x, y)
.set_symbol(grapheme.symbol)
.set_style(grapheme.style);
}
// multi-width graphemes must clear the cells of characters that are hidden by the
// grapheme, otherwise the hidden characters will be re-rendered if the grapheme is
// overwritten.
for i in (current_x + 1)..next_x {
buf.get_mut(i, y).reset();
for x_hidden in (x + 1)..next_x {
// it may seem odd that the style of the hidden cells are not set to the style of
// the grapheme, but this is how the existing buffer.set_span() method works.
// buf.get_mut(i, y).set_style(g.style);
buf.get_mut(x_hidden, y).reset();
}
current_x = next_x;
x = next_x;
}
}
}
/// A trait for converting a value to a [`Span`].
///
/// This trait is automatically implemented for any type that implements the [`Display`] trait. As
/// such, `ToSpan` shouln't be implemented directly: [`Display`] should be implemented instead, and
/// you get the `ToSpan` implementation for free.
///
/// [`Display`]: std::fmt::Display
pub trait ToSpan {
/// Converts the value to a [`Span`].
fn to_span(&self) -> Span<'_>;
}
/// # Panics
///
/// In this implementation, the `to_span` method panics if the `Display` implementation returns an
/// error. This indicates an incorrect `Display` implementation since `fmt::Write for String` never
/// returns an error itself.
impl<T: fmt::Display> ToSpan for T {
fn to_span(&self) -> Span<'_> {
Span::raw(self.to_string())
}
}
impl fmt::Display for Span<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.content, f)
@@ -403,6 +437,7 @@ impl fmt::Display for Span<'_> {
#[cfg(test)]
mod tests {
use buffer::Cell;
use rstest::fixture;
use super::*;
@@ -495,6 +530,12 @@ mod tests {
assert_eq!(span.style, Style::default());
}
#[test]
fn to_span() {
assert_eq!(42.to_span(), Span::raw("42"));
assert_eq!("test".to_span(), Span::raw("test"));
}
#[test]
fn reset_style() {
let span = Span::styled("test content", Style::new().green()).reset_style();
@@ -664,5 +705,72 @@ mod tests {
])]);
assert_eq!(buf, expected);
}
#[test]
fn render_first_zero_width() {
let span = Span::raw("\u{200B}abc");
let mut buf = Buffer::empty(Rect::new(0, 0, 3, 1));
span.render(buf.area, &mut buf);
assert_eq!(
buf.content(),
[Cell::new("\u{200B}a"), Cell::new("b"), Cell::new("c"),]
);
}
#[test]
fn render_second_zero_width() {
let span = Span::raw("a\u{200B}bc");
let mut buf = Buffer::empty(Rect::new(0, 0, 3, 1));
span.render(buf.area, &mut buf);
assert_eq!(
buf.content(),
[Cell::new("a\u{200B}"), Cell::new("b"), Cell::new("c")]
);
}
#[test]
fn render_middle_zero_width() {
let span = Span::raw("ab\u{200B}c");
let mut buf = Buffer::empty(Rect::new(0, 0, 3, 1));
span.render(buf.area, &mut buf);
assert_eq!(
buf.content(),
[Cell::new("a"), Cell::new("b\u{200B}"), Cell::new("c")]
);
}
#[test]
fn render_last_zero_width() {
let span = Span::raw("abc\u{200B}");
let mut buf = Buffer::empty(Rect::new(0, 0, 3, 1));
span.render(buf.area, &mut buf);
assert_eq!(
buf.content(),
[Cell::new("a"), Cell::new("b"), Cell::new("c\u{200B}")]
);
}
}
/// Regression test for <https://github.com/ratatui-org/ratatui/issues/1160> One line contains
/// some Unicode Left-Right-Marks (U+200E)
///
/// The issue was that a zero-width character at the end of the buffer causes the buffer bounds
/// to be exceeded (due to a position + 1 calculation that fails to account for the possibility
/// that the next position might not be available).
#[test]
fn issue_1160() {
let span = Span::raw("Hello\u{200E}");
let mut buf = Buffer::empty(Rect::new(0, 0, 5, 1));
span.render(buf.area, &mut buf);
assert_eq!(
buf.content(),
[
Cell::new("H"),
Cell::new("e"),
Cell::new("l"),
Cell::new("l"),
Cell::new("o\u{200E}"),
]
);
}
}

View File

@@ -3,7 +3,7 @@ use std::{borrow::Cow, fmt};
use itertools::{Itertools, Position};
use crate::prelude::*;
use crate::{prelude::*, style::Styled};
/// A string split over one or more lines.
///
@@ -580,6 +580,29 @@ where
}
}
/// A trait for converting a value to a [`Text`].
///
/// This trait is automatically implemented for any type that implements the [`Display`] trait. As
/// such, `ToText` shouldn't be implemented directly: [`Display`] should be implemented instead, and
/// you get the `ToText` implementation for free.
///
/// [`Display`]: std::fmt::Display
pub trait ToText<'a> {
/// Converts the value to a [`Text`].
fn to_text(&self) -> Text<'a>;
}
/// # Panics
///
/// In this implementation, the `to_text` method panics if the `Display` implementation returns an
/// error. This indicates an incorrect `Display` implementation since `fmt::Write for String` never
/// returns an error itself.
impl<'a, T: fmt::Display> ToText<'a> for T {
fn to_text(&self) -> Text<'a> {
Text::raw(self.to_string())
}
}
impl fmt::Display for Text<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (position, line) in self.iter().with_position() {
@@ -747,6 +770,24 @@ mod tests {
assert_eq!(text.lines, vec![Line::from("The first line")]);
}
#[rstest]
#[case(42, Text::from("42"))]
#[case("just\ntesting", Text::from("just\ntesting"))]
#[case(true, Text::from("true"))]
#[case(6.66, Text::from("6.66"))]
#[case('a', Text::from("a"))]
#[case(String::from("hello"), Text::from("hello"))]
#[case(-1, Text::from("-1"))]
#[case("line1\nline2", Text::from("line1\nline2"))]
#[case(
"first line\nsecond line\nthird line",
Text::from("first line\nsecond line\nthird line")
)]
#[case("trailing newline\n", Text::from("trailing newline\n"))]
fn to_text(#[case] value: impl fmt::Display, #[case] expected: Text) {
assert_eq!(value.to_text(), expected);
}
#[test]
fn from_vec_line() {
let text = Text::from(vec![

View File

@@ -52,7 +52,7 @@ pub use self::{
table::{Cell, HighlightSpacing, Row, Table, TableState},
tabs::Tabs,
};
use crate::{buffer::Buffer, layout::Rect};
use crate::{buffer::Buffer, layout::Rect, style::Style};
/// A `Widget` is a type that can be drawn on a [`Buffer`] in a given [`Rect`].
///
@@ -245,11 +245,6 @@ pub trait StatefulWidget {
/// provided. This is a convenience approach to make it easier to attach child widgets to parent
/// widgets. It allows you to render an optional widget by reference.
///
/// A blanket [implementation of `WidgetRef` for `Fn(Rect, &mut
/// Buffer)`](WidgetRef#impl-WidgetRef-for-F) is provided. This allows you to treat any function or
/// closure that takes a `Rect` and a mutable reference to a `Buffer` as a widget in situations
/// where you don't want to create a new type for a widget.
///
/// # Examples
///
/// ```rust
@@ -302,47 +297,20 @@ pub trait StatefulWidget {
/// # }
/// # }
/// ```
#[stability::unstable(feature = "widget-ref")]
#[instability::unstable(feature = "widget-ref")]
pub trait WidgetRef {
/// Draws the current state of the widget in the given buffer. That is the only method required
/// to implement a custom widget.
fn render_ref(&self, area: Rect, buf: &mut Buffer);
}
// /// This allows you to render a widget by reference.
/// This allows you to render a widget by reference.
impl<W: WidgetRef> Widget for &W {
fn render(self, area: Rect, buf: &mut Buffer) {
self.render_ref(area, buf);
}
}
/// A blanket implementation of `WidgetRef` for `Fn(Rect, &mut Buffer)`.
///
/// This allows you to treat any function that takes a `Rect` and a mutable reference to a `Buffer`
/// as a widget in situations where you don't want to create a new type for a widget.
///
/// # Examples
///
/// ```rust
/// use ratatui::{prelude::*, widgets::*};
/// fn hello(area: Rect, buf: &mut Buffer) {
/// Line::raw("Hello").render(area, buf);
/// }
///
/// fn draw(mut frame: Frame) {
/// frame.render_widget(&hello, frame.size());
/// frame.render_widget_ref(hello, frame.size());
/// }
/// ```
impl<F> WidgetRef for F
where
F: Fn(Rect, &mut Buffer),
{
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
self(area, buf);
}
}
/// A blanket implementation of `WidgetExt` for `Option<W>` where `W` implements `WidgetRef`.
///
/// This is a convenience implementation that makes it easy to attach child widgets to parent
@@ -430,7 +398,7 @@ impl<W: WidgetRef> WidgetRef for Option<W> {
/// }
/// # }
/// ```
#[stability::unstable(feature = "widget-ref")]
#[instability::unstable(feature = "widget-ref")]
pub trait StatefulWidgetRef {
/// State associated with the stateful widget.
///
@@ -476,7 +444,7 @@ impl Widget for &str {
/// [`Rect`].
impl WidgetRef for &str {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
buf.set_string(area.x, area.y, self, crate::style::Style::default());
buf.set_stringn(area.x, area.y, self, area.width as usize, Style::new());
}
}
@@ -497,7 +465,7 @@ impl Widget for String {
/// without the need to give up ownership of the underlying text.
impl WidgetRef for String {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
buf.set_string(area.x, area.y, self, crate::style::Style::default());
buf.set_stringn(area.x, area.y, self, area.width as usize, Style::new());
}
}
@@ -506,7 +474,7 @@ mod tests {
use rstest::{fixture, rstest};
use super::*;
use crate::prelude::*;
use crate::text::Line;
#[fixture]
fn buf() -> Buffer {
@@ -682,6 +650,13 @@ mod tests {
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
#[rstest]
fn render_area(mut buf: Buffer) {
let area = Rect::new(buf.area.x, buf.area.y, 11, buf.area.height);
"hello world, just hello".render(area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
#[rstest]
fn render_ref(mut buf: Buffer) {
"hello world".render_ref(buf.area, &mut buf);
@@ -709,6 +684,13 @@ mod tests {
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
#[rstest]
fn render_area(mut buf: Buffer) {
let area = Rect::new(buf.area.x, buf.area.y, 11, buf.area.height);
String::from("hello world, just hello").render(area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
#[rstest]
fn render_ref(mut buf: Buffer) {
String::from("hello world").render_ref(buf.area, &mut buf);
@@ -727,42 +709,4 @@ mod tests {
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
}
mod function_widget {
use super::*;
fn widget_function(area: Rect, buf: &mut Buffer) {
"Hello".render(area, buf);
}
#[rstest]
fn render(mut buf: Buffer) {
widget_function.render(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["Hello "]));
}
#[rstest]
fn render_ref(mut buf: Buffer) {
widget_function.render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["Hello "]));
}
#[rstest]
fn render_closure(mut buf: Buffer) {
let widget = |area: Rect, buf: &mut Buffer| {
"Hello".render(area, buf);
};
widget.render(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["Hello "]));
}
#[rstest]
fn render_closure_ref(mut buf: Buffer) {
let widget = |area: Rect, buf: &mut Buffer| {
"Hello".render(area, buf);
};
widget.render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["Hello "]));
}
}
}

View File

@@ -1,4 +1,4 @@
use crate::{prelude::*, widgets::Block};
use crate::{prelude::*, style::Styled, widgets::Block};
mod bar;
mod bar_group;

View File

@@ -78,7 +78,8 @@ impl<'a> From<&[(&'a str, u64)]> for BarGroup<'a> {
impl<'a, const N: usize> From<&[(&'a str, u64); N]> for BarGroup<'a> {
fn from(value: &[(&'a str, u64); N]) -> Self {
Self::from(value.as_ref())
let value: &[(&'a str, u64)] = value.as_ref();
Self::from(value)
}
}

View File

@@ -8,7 +8,7 @@
use itertools::Itertools;
use strum::{Display, EnumString};
use crate::{prelude::*, symbols::border, widgets::Borders};
use crate::{prelude::*, style::Styled, symbols::border, widgets::Borders};
mod padding;
pub mod title;
@@ -35,6 +35,30 @@ pub use title::{Position, Title};
///
/// Without left border───
/// ```
/// # Constructor methods
///
/// - [`Block::new`] creates a new [`Block`] with no border or paddings.
/// - [`Block::bordered`] Create a new block with all borders shown.
///
/// # Setter methods
///
/// These methods are fluent setters. They return a new [`Block`] with the specified property set.
///
/// - [`Block::borders`] Defines which borders to display.
/// - [`Block::border_style`] Defines the style of the borders.
/// - [`Block::border_type`] Sets the symbols used to display the border (e.g. single line, double
/// line, thick or rounded borders).
/// - [`Block::padding`] Defines the padding inside a [`Block`].
/// - [`Block::style`] Sets the base style of the widget.
/// - [`Block::title`] Adds a title to the block.
/// - [`Block::title_alignment`] Sets the default [`Alignment`] for all block titles.
/// - [`Block::title_style`] Applies the style to all titles.
/// - [`Block::title_top`] Adds a title to the top of the block.
/// - [`Block::title_bottom`] Adds a title to the bottom of the block.
/// - [`Block::title_position`] Adds a title to the block.
///
/// # Other Methods
/// - [`Block::inner`] Compute the inner area of a block based on its border visibility rules.
///
/// # Examples
///
@@ -53,13 +77,30 @@ pub use title::{Position, Title};
/// ```
/// use ratatui::{
/// prelude::*,
/// widgets::{block::*, *},
/// widgets::{
/// block::{Position, Title},
/// Block,
/// },
/// };
///
/// Block::new()
/// .title("Title 1")
/// .title(Title::from("Title 2").position(Position::Bottom));
/// ```
///
/// You can also pass it as parameters of another widget so that the block surrounds them:
/// ```
/// use ratatui::{
/// prelude::*,
/// widgets::{Block, Borders, List},
/// };
///
/// let surrounding_block = Block::default()
/// .borders(Borders::ALL)
/// .title("Here is a list of items");
/// let items = ["Item 1", "Item 2", "Item 3"];
/// let list = List::new(items).block(surrounding_block);
/// ```
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Block<'a> {
/// List of titles
@@ -354,7 +395,10 @@ impl<'a> Block<'a> {
/// ```
/// use ratatui::{
/// prelude::*,
/// widgets::{block::*, *},
/// widgets::{
/// block::{Position, Title},
/// Block,
/// },
/// };
///
/// Block::new()

View File

@@ -36,7 +36,10 @@ use crate::{layout::Alignment, text::Line};
/// ```
/// use ratatui::{
/// prelude::*,
/// widgets::{block::*, *},
/// widgets::{
/// block::{Position, Title},
/// Block,
/// },
/// };
///
/// Title::from("Title")

View File

@@ -30,7 +30,7 @@ pub use self::{
points::Points,
rectangle::Rectangle,
};
use crate::{prelude::*, text::Line as TextLine, widgets::Block};
use crate::{prelude::*, symbols::Marker, text::Line as TextLine, widgets::Block};
/// Something that can be drawn on a [`Canvas`].
///
@@ -464,17 +464,17 @@ impl<'a> Context<'a> {
height: u16,
x_bounds: [f64; 2],
y_bounds: [f64; 2],
marker: symbols::Marker,
marker: Marker,
) -> Self {
let dot = symbols::DOT.chars().next().unwrap();
let block = symbols::block::FULL.chars().next().unwrap();
let bar = symbols::bar::HALF.chars().next().unwrap();
let grid: Box<dyn Grid> = match marker {
symbols::Marker::Dot => Box::new(CharGrid::new(width, height, dot)),
symbols::Marker::Block => Box::new(CharGrid::new(width, height, block)),
symbols::Marker::Bar => Box::new(CharGrid::new(width, height, bar)),
symbols::Marker::Braille => Box::new(BrailleGrid::new(width, height)),
symbols::Marker::HalfBlock => Box::new(HalfBlockGrid::new(width, height)),
Marker::Dot => Box::new(CharGrid::new(width, height, dot)),
Marker::Block => Box::new(CharGrid::new(width, height, block)),
Marker::Bar => Box::new(CharGrid::new(width, height, bar)),
Marker::Braille => Box::new(BrailleGrid::new(width, height)),
Marker::HalfBlock => Box::new(HalfBlockGrid::new(width, height)),
};
Self {
x_bounds,
@@ -604,7 +604,7 @@ where
y_bounds: [f64; 2],
paint_func: Option<F>,
background_color: Color,
marker: symbols::Marker,
marker: Marker,
}
impl<'a, F> Default for Canvas<'a, F>
@@ -618,7 +618,7 @@ where
y_bounds: [0.0, 0.0],
paint_func: None,
background_color: Color::Reset,
marker: symbols::Marker::Braille,
marker: Marker::Braille,
}
}
}
@@ -717,7 +717,7 @@ where
/// .paint(|ctx| {});
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub const fn marker(mut self, marker: symbols::Marker) -> Self {
pub const fn marker(mut self, marker: Marker) -> Self {
self.marker = marker;
self
}

View File

@@ -114,8 +114,14 @@ fn draw_line_high(painter: &mut Painter, x1: usize, y1: usize, x2: usize, y2: us
mod tests {
use rstest::rstest;
use super::{super::*, *};
use crate::{buffer::Buffer, layout::Rect};
use super::*;
use crate::{
buffer::Buffer,
layout::Rect,
style::{Style, Stylize},
symbols::Marker,
widgets::{canvas::Canvas, Widget},
};
#[rstest]
#[case::off_grid(&Line::new(-1.0, -1.0, 10.0, 10.0, Color::Red), [" "; 10])]

View File

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

View File

@@ -66,7 +66,7 @@ impl Shape for Rectangle {
#[cfg(test)]
mod tests {
use super::*;
use crate::{prelude::*, widgets::canvas::Canvas};
use crate::{prelude::*, symbols::Marker, widgets::canvas::Canvas};
#[test]
fn draw_block_lines() {

View File

@@ -6,6 +6,7 @@ use unicode_width::UnicodeWidthStr;
use crate::{
layout::Flex,
prelude::*,
style::Styled,
widgets::{
canvas::{Canvas, Line as CanvasLine, Points},
Block,
@@ -285,7 +286,7 @@ impl LegendPosition {
/// This example draws a red line between two points.
///
/// ```rust
/// use ratatui::{prelude::*, widgets::*};
/// use ratatui::{prelude::*, symbols::Marker, widgets::*};
///
/// let dataset = Dataset::default()
/// .name("dataset 1")

View File

@@ -1,4 +1,4 @@
use crate::{prelude::*, widgets::Block};
use crate::{prelude::*, style::Styled, widgets::Block};
/// A widget to display a progress bar.
///

View File

@@ -3,6 +3,7 @@ use unicode_width::UnicodeWidthStr;
use crate::{
prelude::*,
style::Styled,
widgets::{Block, HighlightSpacing},
};
@@ -156,6 +157,72 @@ impl ListState {
self.offset = 0;
}
}
/// Selects the next item or the first one if no item is selected
///
/// Note: until the list is rendered, the number of items is not known, so the index is set to
/// `0` and will be corrected when the list is rendered
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let mut state = ListState::default();
/// state.select_next();
/// ```
pub fn select_next(&mut self) {
let next = self.selected.map_or(0, |i| i.saturating_add(1));
self.select(Some(next));
}
/// Selects the previous item or the last one if no item is selected
///
/// Note: until the list is rendered, the number of items is not known, so the index is set to
/// `usize::MAX` and will be corrected when the list is rendered
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let mut state = ListState::default();
/// state.select_previous();
/// ```
pub fn select_previous(&mut self) {
let previous = self.selected.map_or(usize::MAX, |i| i.saturating_sub(1));
self.select(Some(previous));
}
/// Selects the first item
///
/// Note: until the list is rendered, the number of items is not known, so the index is set to
/// `0` and will be corrected when the list is rendered
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let mut state = ListState::default();
/// state.select_first();
/// ```
pub fn select_first(&mut self) {
self.select(Some(0));
}
/// Selects the last item
///
/// Note: until the list is rendered, the number of items is not known, so the index is set to
/// `usize::MAX` and will be corrected when the list is rendered
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let mut state = ListState::default();
/// state.select_last();
/// ```
pub fn select_last(&mut self) {
self.select(Some(usize::MAX));
}
}
/// A single item in a [`List`]
@@ -881,10 +948,20 @@ impl StatefulWidgetRef for List<'_> {
self.block.render_ref(area, buf);
let list_area = self.block.inner_if_some(area);
if list_area.is_empty() || self.items.is_empty() {
if list_area.is_empty() {
return;
}
if self.items.is_empty() {
state.select(None);
return;
}
// If the selected index is out of bounds, set it to the last item
if state.selected.is_some_and(|s| s >= self.items.len()) {
state.select(Some(self.items.len().saturating_sub(1)));
}
let list_height = list_area.height as usize;
let (first_visible_index, last_visible_index) =
@@ -1008,6 +1085,11 @@ mod tests {
use super::*;
#[fixture]
fn single_line_buf() -> Buffer {
Buffer::empty(Rect::new(0, 0, 10, 1))
}
#[test]
fn test_list_state_selected() {
let mut state = ListState::default();
@@ -1035,6 +1117,96 @@ mod tests {
assert_eq!(state.offset, 0);
}
#[test]
fn test_list_state_navigation() {
let mut state = ListState::default();
state.select_first();
assert_eq!(state.selected, Some(0));
state.select_previous(); // should not go below 0
assert_eq!(state.selected, Some(0));
state.select_next();
assert_eq!(state.selected, Some(1));
state.select_previous();
assert_eq!(state.selected, Some(0));
state.select_last();
assert_eq!(state.selected, Some(usize::MAX));
state.select_next(); // should not go above usize::MAX
assert_eq!(state.selected, Some(usize::MAX));
state.select_previous();
assert_eq!(state.selected, Some(usize::MAX - 1));
state.select_next();
assert_eq!(state.selected, Some(usize::MAX));
let mut state = ListState::default();
state.select_next();
assert_eq!(state.selected, Some(0));
let mut state = ListState::default();
state.select_previous();
assert_eq!(state.selected, Some(usize::MAX));
}
#[rstest]
fn test_list_state_empty_list(mut single_line_buf: Buffer) {
let mut state = ListState::default();
let items: Vec<ListItem> = Vec::new();
let list = List::new(items);
state.select_first();
StatefulWidget::render(list, single_line_buf.area, &mut single_line_buf, &mut state);
assert_eq!(state.selected, None);
}
#[rstest]
fn test_list_state_single_item(mut single_line_buf: Buffer) {
let mut state = ListState::default();
let items = vec![ListItem::new("Item 1")];
let list = List::new(items);
state.select_first();
StatefulWidget::render(
&list,
single_line_buf.area,
&mut single_line_buf,
&mut state,
);
assert_eq!(state.selected, Some(0));
state.select_last();
StatefulWidget::render(
&list,
single_line_buf.area,
&mut single_line_buf,
&mut state,
);
assert_eq!(state.selected, Some(0));
state.select_previous();
StatefulWidget::render(
&list,
single_line_buf.area,
&mut single_line_buf,
&mut state,
);
assert_eq!(state.selected, Some(0));
state.select_next();
StatefulWidget::render(
&list,
single_line_buf.area,
&mut single_line_buf,
&mut state,
);
assert_eq!(state.selected, Some(0));
}
#[test]
fn test_list_item_new_from_str() {
let item = ListItem::new("Test item");
@@ -1301,7 +1473,7 @@ mod tests {
&single_item,
Some(1),
[
" Item 0 ",
">>Item 0 ",
" ",
" ",
" ",
@@ -1359,7 +1531,7 @@ mod tests {
[
" Item 0 ",
" Item 1 ",
" Item 2 ",
">>Item 2 ",
" ",
" ",
],
@@ -2098,11 +2270,6 @@ mod tests {
]);
}
#[fixture]
fn single_line_buf() -> Buffer {
Buffer::empty(Rect::new(0, 0, 10, 1))
}
/// Regression test for a bug where highlight symbol being greater than width caused a panic due
/// to subtraction with underflow.
///

View File

@@ -2,6 +2,7 @@ use unicode_width::UnicodeWidthStr;
use crate::{
prelude::*,
style::Styled,
text::StyledGrapheme,
widgets::{
reflow::{LineComposer, LineTruncator, WordWrapper, WrappedLine},
@@ -269,7 +270,7 @@ impl<'a> Paragraph<'a> {
/// assert_eq!(paragraph.line_count(20), 1);
/// assert_eq!(paragraph.line_count(10), 2);
/// ```
#[stability::unstable(
#[instability::unstable(
feature = "rendered-line-info",
issue = "https://github.com/ratatui-org/ratatui/issues/293"
)]
@@ -312,7 +313,7 @@ impl<'a> Paragraph<'a> {
/// let paragraph = Paragraph::new("Hello World\nhi\nHello World!!!");
/// assert_eq!(paragraph.line_width(), 14);
/// ```
#[stability::unstable(
#[instability::unstable(
feature = "rendered-line-info",
issue = "https://github.com/ratatui-org/ratatui/issues/293"
)]
@@ -432,7 +433,7 @@ mod test {
#[test]
fn zero_width_char_at_end_of_line() {
let line = "foo\0";
let line = "foo\u{200B}";
for paragraph in [
Paragraph::new(line),
Paragraph::new(line).wrap(Wrap { trim: false }),

View File

@@ -5,9 +5,6 @@ use unicode_width::UnicodeWidthStr;
use crate::{layout::Alignment, text::StyledGrapheme};
const NBSP: &str = "\u{00a0}";
const ZWSP: &str = "\u{200b}";
/// A state machine to pack styled symbols into lines.
/// Cannot implement it as Iterator since it yields slices of the internal buffer (need streaming
/// iterators for that).
@@ -59,6 +56,124 @@ where
trim,
}
}
fn next_cached_line(&mut self) -> Option<Vec<StyledGrapheme<'a>>> {
self.wrapped_lines.as_mut()?.next()
}
/// Split an input line (`line_symbols`) into wrapped lines
/// and cache them to be emitted later
fn process_input(&mut self, line_symbols: impl IntoIterator<Item = StyledGrapheme<'a>>) {
let mut result_lines = vec![];
let mut pending_line = vec![];
let mut line_width = 0;
let mut pending_word = vec![];
let mut word_width = 0;
let mut pending_whitespace: VecDeque<StyledGrapheme> = VecDeque::new();
let mut whitespace_width = 0;
let mut non_whitespace_previous = false;
for grapheme in line_symbols {
let is_whitespace = grapheme.is_whitespace();
let symbol_width = grapheme.symbol.width() as u16;
// ignore symbols wider than line limit
if symbol_width > self.max_line_width {
continue;
}
let word_found = non_whitespace_previous && is_whitespace;
// current word would overflow after removing whitespace
let trimmed_overflow = pending_line.is_empty()
&& self.trim
&& word_width + symbol_width > self.max_line_width;
// separated whitespace would overflow on its own
let whitespace_overflow = pending_line.is_empty()
&& self.trim
&& whitespace_width + symbol_width > self.max_line_width;
// current full word (including whitespace) would overflow
let untrimmed_overflow = pending_line.is_empty()
&& !self.trim
&& word_width + whitespace_width + symbol_width > self.max_line_width;
// append finished segment to current line
if word_found || trimmed_overflow || whitespace_overflow || untrimmed_overflow {
if !pending_line.is_empty() || !self.trim {
pending_line.extend(pending_whitespace.drain(..));
line_width += whitespace_width;
}
pending_line.append(&mut pending_word);
line_width += word_width;
pending_whitespace.clear();
whitespace_width = 0;
word_width = 0;
}
// pending line fills up limit
let line_full = line_width >= self.max_line_width;
// pending word would overflow line limit
let pending_word_overflow = symbol_width > 0
&& line_width + whitespace_width + word_width >= self.max_line_width;
// add finished wrapped line to remaining lines
if line_full || pending_word_overflow {
let mut remaining_width = u16::saturating_sub(self.max_line_width, line_width);
result_lines.push(std::mem::take(&mut pending_line));
line_width = 0;
// remove whitespace up to the end of line
while let Some(grapheme) = pending_whitespace.front() {
let width = grapheme.symbol.width() as u16;
if width > remaining_width {
break;
}
whitespace_width -= width;
remaining_width -= width;
pending_whitespace.pop_front();
}
// don't count first whitespace toward next word
if is_whitespace && pending_whitespace.is_empty() {
continue;
}
}
// append symbol to a pending buffer
if is_whitespace {
whitespace_width += symbol_width;
pending_whitespace.push_back(grapheme);
} else {
word_width += symbol_width;
pending_word.push(grapheme);
}
non_whitespace_previous = !is_whitespace;
}
// append remaining text parts
if pending_line.is_empty() && pending_word.is_empty() && !pending_whitespace.is_empty() {
result_lines.push(vec![]);
}
if !pending_line.is_empty() || !self.trim {
pending_line.extend(pending_whitespace);
}
pending_line.extend(pending_word);
if !pending_line.is_empty() {
result_lines.push(pending_line);
}
if result_lines.is_empty() {
result_lines.push(vec![]);
}
// save processed lines for emitting later
self.wrapped_lines = Some(result_lines.into_iter());
}
}
impl<'a, O, I> LineComposer<'a> for WordWrapper<'a, O, I>
@@ -72,155 +187,26 @@ where
return None;
}
let mut current_line: Option<Vec<StyledGrapheme<'a>>> = None;
let mut line_width: u16 = 0;
loop {
// emit next cached line if present
if let Some(line) = self.next_cached_line() {
let line_width = line
.iter()
.map(|grapheme| grapheme.symbol.width() as u16)
.sum();
// Try to repeatedly retrieve next line
while current_line.is_none() {
// Retrieve next preprocessed wrapped line
if let Some(line_iterator) = &mut self.wrapped_lines {
if let Some(line) = line_iterator.next() {
line_width = line
.iter()
.map(|grapheme| grapheme.symbol.width())
.sum::<usize>() as u16;
current_line = Some(line);
}
self.current_line = line;
return Some(WrappedLine {
line: &self.current_line,
width: line_width,
alignment: self.current_alignment,
});
}
// When no more preprocessed wrapped lines
if current_line.is_none() {
// Try to calculate next wrapped lines based on current whole line
if let Some((line_symbols, line_alignment)) = &mut self.input_lines.next() {
// Save the whole line's alignment
self.current_alignment = *line_alignment;
let mut wrapped_lines = vec![]; // Saves the wrapped lines
// Saves the unfinished wrapped line
let (mut current_line, mut current_line_width) = (vec![], 0);
// Saves the partially processed word
let (mut unfinished_word, mut word_width) = (vec![], 0);
// Saves the whitespaces of the partially unfinished word
let (mut unfinished_whitespaces, mut whitespace_width) =
(VecDeque::<StyledGrapheme>::new(), 0);
let mut has_seen_non_whitespace = false;
for StyledGrapheme { symbol, style } in line_symbols {
let symbol_whitespace = symbol == ZWSP
|| (symbol.chars().all(&char::is_whitespace) && symbol != NBSP);
let symbol_width = symbol.width() as u16;
// Ignore characters wider than the total max width
if symbol_width > self.max_line_width {
continue;
}
// Append finished word to current line
if has_seen_non_whitespace && symbol_whitespace
// Append if trimmed (whitespaces removed) word would overflow
|| word_width + symbol_width > self.max_line_width && current_line.is_empty() && self.trim
// Append if removed whitespace would overflow -> reset whitespace counting to prevent overflow
|| whitespace_width + symbol_width > self.max_line_width && current_line.is_empty() && self.trim
// Append if complete word would overflow
|| word_width + whitespace_width + symbol_width > self.max_line_width && current_line.is_empty() && !self.trim
{
if !current_line.is_empty() || !self.trim {
// Also append whitespaces if not trimming or current line is not
// empty
current_line.extend(
std::mem::take(&mut unfinished_whitespaces).into_iter(),
);
current_line_width += whitespace_width;
}
// Append trimmed word
current_line.append(&mut unfinished_word);
current_line_width += word_width;
// Clear whitespace buffer
unfinished_whitespaces.clear();
whitespace_width = 0;
word_width = 0;
}
// Append the unfinished wrapped line to wrapped lines if it is as wide as
// max line width
if current_line_width >= self.max_line_width
// or if it would be too long with the current partially processed word added
|| current_line_width + whitespace_width + word_width >= self.max_line_width && symbol_width > 0
{
let mut remaining_width = (i32::from(self.max_line_width)
- i32::from(current_line_width))
.max(0) as u16;
wrapped_lines.push(std::mem::take(&mut current_line));
current_line_width = 0;
// Remove all whitespaces till end of just appended wrapped line + next
// whitespace
let mut first_whitespace = unfinished_whitespaces.pop_front();
while let Some(grapheme) = first_whitespace.as_ref() {
let symbol_width = grapheme.symbol.width() as u16;
whitespace_width -= symbol_width;
if symbol_width > remaining_width {
break;
}
remaining_width -= symbol_width;
first_whitespace = unfinished_whitespaces.pop_front();
}
// In case all whitespaces have been exhausted
if symbol_whitespace && first_whitespace.is_none() {
// Prevent first whitespace to count towards next word
continue;
}
}
// Append symbol to unfinished, partially processed word
if symbol_whitespace {
whitespace_width += symbol_width;
unfinished_whitespaces.push_back(StyledGrapheme { symbol, style });
} else {
word_width += symbol_width;
unfinished_word.push(StyledGrapheme { symbol, style });
}
has_seen_non_whitespace = !symbol_whitespace;
}
// Append remaining text parts
if !unfinished_word.is_empty() || !unfinished_whitespaces.is_empty() {
if current_line.is_empty() && unfinished_word.is_empty() {
wrapped_lines.push(vec![]);
} else if !self.trim || !current_line.is_empty() {
current_line.extend(unfinished_whitespaces.into_iter());
} else {
// TODO: explain why this else branch is ok.
// See clippy::else_if_without_else
}
current_line.append(&mut unfinished_word);
}
if !current_line.is_empty() {
wrapped_lines.push(current_line);
}
if wrapped_lines.is_empty() {
// Append empty line if there was nothing to wrap in the first place
wrapped_lines.push(vec![]);
}
self.wrapped_lines = Some(wrapped_lines.into_iter());
} else {
// No more whole lines available -> stop repeatedly retrieving next wrapped line
break;
}
}
}
if let Some(line) = current_line {
self.current_line = line;
Some(WrappedLine {
line: &self.current_line,
width: line_width,
alignment: self.current_alignment,
})
} else {
None
// otherwise, process pending wrapped lines from input
let (line_symbols, line_alignment) = self.input_lines.next()?;
self.current_alignment = line_alignment;
self.process_input(line_symbols);
}
}
}
@@ -673,11 +659,11 @@ mod test {
#[test]
fn line_composer_zero_width_at_end() {
let width = 3;
let line = "foo\0";
let line = "foo\u{200B}";
let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: true }, line, width);
let (line_truncator, _, _) = run_composer(Composer::LineTruncator, line, width);
assert_eq!(word_wrapper, vec!["foo\0"]);
assert_eq!(line_truncator, vec!["foo\0"]);
assert_eq!(word_wrapper, vec!["foo"]);
assert_eq!(line_truncator, vec!["foo\u{200B}"]);
}
#[test]

View File

@@ -2,7 +2,7 @@ use std::cmp::min;
use strum::{Display, EnumString};
use crate::{prelude::*, widgets::Block};
use crate::{prelude::*, style::Styled, widgets::Block};
/// Widget to render a sparkline over one or more lines.
///

View File

@@ -1,4 +1,4 @@
use crate::prelude::*;
use crate::{prelude::*, style::Styled};
/// A [`Cell`] contains the [`Text`] to be displayed in a [`Row`] of a [`Table`].
///

View File

@@ -1,5 +1,5 @@
use super::Cell;
use crate::prelude::*;
use crate::{prelude::*, style::Styled};
/// A single row of data to be displayed in a [`Table`] widget.
///

View File

@@ -3,7 +3,7 @@ use itertools::Itertools;
#[allow(unused_imports)] // `Cell` is used in the doc comment but not the code
use super::Cell;
use super::{HighlightSpacing, Row, TableState};
use crate::{layout::Flex, prelude::*, widgets::Block};
use crate::{layout::Flex, prelude::*, style::Styled, widgets::Block};
/// A widget to display data in formatted columns.
///
@@ -20,7 +20,9 @@ use crate::{layout::Flex, prelude::*, widgets::Block};
/// [`Table`] implements [`Widget`] and so it can be drawn using [`Frame::render_widget`].
///
/// [`Table`] is also a [`StatefulWidget`], which means you can use it with [`TableState`] to allow
/// the user to scroll through the rows and select one of them.
/// the user to scroll through the rows and select one of them. When rendering a [`Table`] with a
/// [`TableState`], the selected row will be highlighted. If the selected row is not visible (based
/// on the offset), the table will be scrolled to make the selected row visible.
///
/// Note: if the `widths` field is empty, the table will be rendered with equal widths.
///
@@ -611,6 +613,14 @@ impl StatefulWidgetRef for Table<'_> {
return;
}
if state.selected.is_some_and(|s| s >= self.rows.len()) {
state.select(Some(self.rows.len().saturating_sub(1)));
}
if self.rows.is_empty() {
state.select(None);
}
let selection_width = self.selection_width(state);
let columns_widths = self.get_columns_widths(table_area.width, selection_width);
let (header_area, rows_area, footer_area) = self.layout(table_area);
@@ -775,7 +785,14 @@ impl Table<'_> {
end += 1;
}
let selected = selected.unwrap_or(0).min(self.rows.len() - 1);
let Some(selected) = selected else {
return (start, end);
};
// clamp the selected row to the last row
let selected = selected.min(self.rows.len() - 1);
// scroll down until the selected row is visible
while selected >= end {
height = height.saturating_add(self.rows[end].height_with_margin());
end += 1;
@@ -784,6 +801,8 @@ impl Table<'_> {
start += 1;
}
}
// scroll up until the selected row is visible
while selected < start {
start -= 1;
height = height.saturating_add(self.rows[start].height_with_margin());
@@ -1005,8 +1024,64 @@ mod tests {
assert_eq!(table.widths, vec![Constraint::Percentage(100)], "vec ref");
}
#[cfg(test)]
mod state {
use rstest::{fixture, rstest};
use super::TableState;
use crate::{
buffer::Buffer,
layout::{Constraint, Rect},
widgets::{Row, StatefulWidget, Table},
};
#[fixture]
fn table_buf() -> Buffer {
Buffer::empty(Rect::new(0, 0, 10, 10))
}
#[rstest]
fn test_list_state_empty_list(mut table_buf: Buffer) {
let mut state = TableState::default();
let rows: Vec<Row> = Vec::new();
let widths = vec![Constraint::Percentage(100)];
let table = Table::new(rows, widths);
state.select_first();
StatefulWidget::render(table, table_buf.area, &mut table_buf, &mut state);
assert_eq!(state.selected, None);
}
#[rstest]
fn test_list_state_single_item(mut table_buf: Buffer) {
let mut state = TableState::default();
let widths = vec![Constraint::Percentage(100)];
let items = vec![Row::new(vec!["Item 1"])];
let table = Table::new(items, widths);
state.select_first();
StatefulWidget::render(&table, table_buf.area, &mut table_buf, &mut state);
assert_eq!(state.selected, Some(0));
state.select_last();
StatefulWidget::render(&table, table_buf.area, &mut table_buf, &mut state);
assert_eq!(state.selected, Some(0));
state.select_previous();
StatefulWidget::render(&table, table_buf.area, &mut table_buf, &mut state);
assert_eq!(state.selected, Some(0));
state.select_next();
StatefulWidget::render(&table, table_buf.area, &mut table_buf, &mut state);
assert_eq!(state.selected, Some(0));
}
}
#[cfg(test)]
mod render {
use rstest::rstest;
use super::*;
#[test]
@@ -1197,6 +1272,36 @@ mod tests {
]);
assert_eq!(buf, expected);
}
/// Note that this includes a regression test for a bug where the table would not render the
/// correct rows when there is no selection.
/// <https://github.com/ratatui-org/ratatui/issues/1179>
#[rstest]
#[case::no_selection(None, 50, ["50", "51", "52", "53", "54"])]
#[case::selection_before_offset(20, 20, ["20", "21", "22", "23", "24"])]
#[case::selection_immediately_before_offset(49, 49, ["49", "50", "51", "52", "53"])]
#[case::selection_at_start_of_offset(50, 50, ["50", "51", "52", "53", "54"])]
#[case::selection_at_end_of_offset(54, 50, ["50", "51", "52", "53", "54"])]
#[case::selection_immediately_after_offset(55, 51, ["51", "52", "53", "54", "55"])]
#[case::selection_after_offset(80, 76, ["76", "77", "78", "79", "80"])]
fn render_with_selection_and_offset<T: Into<Option<usize>>>(
#[case] selected_row: T,
#[case] expected_offset: usize,
#[case] expected_items: [&str; 5],
) {
// render 100 rows offset at 50, with a selected row
let rows = (0..100).map(|i| Row::new([i.to_string()]));
let table = Table::new(rows, [Constraint::Length(2)]);
let mut buf = Buffer::empty(Rect::new(0, 0, 2, 5));
let mut state = TableState::new()
.with_offset(50)
.with_selected(selected_row);
StatefulWidget::render(table.clone(), Rect::new(0, 0, 5, 5), &mut buf, &mut state);
assert_eq!(buf, Buffer::with_lines(expected_items));
assert_eq!(state.offset, expected_offset);
}
}
// test how constraints interact with table column width allocation

View File

@@ -175,6 +175,72 @@ impl TableState {
self.offset = 0;
}
}
/// 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
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let mut state = TableState::default();
/// state.select_next();
/// ```
pub fn select_next(&mut self) {
let next = self.selected.map_or(0, |i| i.saturating_add(1));
self.select(Some(next));
}
/// Selects the previous item or the last 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
/// `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();
/// ```
pub fn select_previous(&mut self) {
let previous = self.selected.map_or(usize::MAX, |i| i.saturating_sub(1));
self.select(Some(previous));
}
/// Selects the first item
///
/// 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
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let mut state = TableState::default();
/// state.select_first();
/// ```
pub fn select_first(&mut self) {
self.select(Some(0));
}
/// Selects the last item
///
/// 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
///
/// # 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));
}
}
#[cfg(test)]
@@ -239,4 +305,40 @@ mod tests {
state.select(None);
assert_eq!(state.selected, None);
}
#[test]
fn test_table_state_navigation() {
let mut state = TableState::default();
state.select_first();
assert_eq!(state.selected, Some(0));
state.select_previous(); // should not go below 0
assert_eq!(state.selected, Some(0));
state.select_next();
assert_eq!(state.selected, Some(1));
state.select_previous();
assert_eq!(state.selected, Some(0));
state.select_last();
assert_eq!(state.selected, Some(usize::MAX));
state.select_next(); // should not go above usize::MAX
assert_eq!(state.selected, Some(usize::MAX));
state.select_previous();
assert_eq!(state.selected, Some(usize::MAX - 1));
state.select_next();
assert_eq!(state.selected, Some(usize::MAX));
let mut state = TableState::default();
state.select_next();
assert_eq!(state.selected, Some(0));
let mut state = TableState::default();
state.select_previous();
assert_eq!(state.selected, Some(usize::MAX));
}
}

View File

@@ -1,4 +1,4 @@
use crate::{prelude::*, widgets::Block};
use crate::{prelude::*, style::Styled, widgets::Block};
const DEFAULT_HIGHLIGHT_STYLE: Style = Style::new().add_modifier(Modifier::REVERSED);

View File

@@ -186,7 +186,7 @@ fn widgets_list_should_clamp_offset_if_items_are_removed() {
})
.unwrap();
terminal.backend().assert_buffer_lines([
" Item 3 ",
">> Item 3 ",
" ",
" ",
" ",