Compare commits

...

61 Commits

Author SHA1 Message Date
Dheepak Krishnamurthy
ccf9d92f10 test: Add more tests for selection and marks behavior 2024-05-13 23:26:47 -04:00
Dheepak Krishnamurthy
b712034644 feat: Show highlight_column only if there are marks 2024-05-13 23:23:57 -04:00
Dheepak Krishnamurthy
38fca62fa9 chore: Use assert_eq instead of assert_buffer_eq 2024-05-13 22:51:31 -04:00
Dheepak Krishnamurthy
3a51a027a6 Merge branch 'main' into kd/multi-select-table 2024-05-13 22:49:22 -04:00
Dheepak Krishnamurthy
5b30f2275c docs: Update docstring for field 2024-05-13 22:45:35 -04:00
EdJoPaTo
9bd89c218a refactor(clippy): enable breaking lint checks (#988)
We need to make sure to not change existing methods without a notice.
But at the same time this also finds public additions with mistakes
before they are even released which is what I would like to have.

This renames a method and deprecated the old name hinting to a new name.
Should this be mentioned somewhere, so it's added to the release notes?
It's not breaking because the old method is still there.
2024-05-13 18:16:09 -07:00
EdJoPaTo
2cfe82a47e refactor(buffer): deprecate assert_buffer_eq! in favor of assert_eq! (#1007)
- Simplify `assert_buffer_eq!` logic.
- Deprecate `assert_buffer_eq!`.
- Introduce `TestBackend::assert_buffer_lines`.

Also simplify many tests involving buffer comparisons.

For the deprecation, just use `assert_eq` instead of `assert_buffer_eq`:

```diff
-assert_buffer_eq!(actual, expected);
+assert_eq!(actual, expected);
```

---

I noticed `assert_buffer_eq!` creating no test coverage reports and
looked into this macro. First I simplified it. Then I noticed a bunch of
`assert_eq!(buffer, …)` and other indirect usages of this macro (like
`TestBackend::assert_buffer`).

The good thing here is that it's mainly used in tests so not many
changes to the library code.
2024-05-13 18:13:46 -07:00
Dheepak Krishnamurthy
b4c27c744c chore: Move pretty assert to top of file 2024-05-13 20:05:20 -04:00
Dheepak Krishnamurthy
977a4899c8 chore: Update serialized data representation 2024-05-13 20:03:59 -04:00
Dheepak Krishnamurthy
cd27b4829a docs: use pretty assertion 2024-05-13 20:00:56 -04:00
Dheepak Krishnamurthy
feee871519 docs: fix broken test 2024-05-13 19:57:56 -04:00
tranzystorekk
1a4bb1cbb8 perf(layout): avoid allocating memory when using split ergonomic utils (#1105)
Don't create intermediate vec in `Layout::areas` and
`Layout::spacers` when there's no need for one.
2024-05-13 16:54:34 -07:00
Dheepak Krishnamurthy
839cca20bf docs(table): Fix typo in docs for highlight_symbol (#1108) 2024-05-13 16:38:34 -04:00
Orhun Parmaksız
f945a0bcff docs(test): fix typo in TestBackend documentation (#1107) 2024-05-13 23:15:09 +03:00
EdJoPaTo
eb281df974 feat: use inner Display implementation (#1097) 2024-05-13 01:36:39 -07:00
Josh McKinney
28e81c0714 build: add underline-color to all features flag in makefile (#1100) 2024-05-13 01:34:52 -07:00
Dheepak Krishnamurthy
0051bb2037 build(test): Add more tests 2024-05-13 03:59:28 -04:00
Dheepak Krishnamurthy
477217c77a feat: Better spacing 2024-05-13 03:45:22 -04:00
Dheepak Krishnamurthy
31de3586f7 feat: Add mark, unmark and mark_highlight symbols 2024-05-13 03:27:22 -04:00
Dheepak Krishnamurthy
f702025b75 feat: Add multi-selection for table 2024-05-13 01:20:12 -04:00
Dheepak Krishnamurthy
76e5fe5a9a chore: Revert "Make Stylize's .bg(color) generic" (#1102)
This reverts commit ec763af851 from #1099
2024-05-12 19:05:00 -04:00
EdJoPaTo
366cbae09f fix(buffer): fix Debug panic and fix formatting of overridden parts (#1098)
Fix panic in `Debug for Buffer` when `width == 0`.
Also corrects the output when symbols are overridden.
2024-05-12 14:57:40 -07:00
Dheepak Krishnamurthy
ec763af851 feat: Make Stylize's .bg(color) generic (#1099)
This PR makes `.bg(color)` generic accepting anything that can be
converted into `Color`; similar to the `.fg(color)` method on the same
trait
2024-05-12 13:23:08 -04:00
Josh McKinney
699c2d7c8d fix: unicode truncation bug (#1089)
- Rewrote the line / span rendering code to take into account how
multi-byte / wide emoji characters are truncated when rendering into
areas that cannot accommodate them in the available space
- Added comprehensive coverage over the edge cases
- Adds a benchmark to ensure perf

Fixes: https://github.com/ratatui-org/ratatui/issues/1032
Co-authored-by: EdJoPaTo <rfc-conform-git-commit-email@funny-long-domain-label-everyone-hates-as-it-is-too-long.edjopato.de>
Co-authored-by: EdJoPaTo <github@edjopato.de>
2024-05-11 19:28:38 -07:00
EdJoPaTo
3cc29bdada test(block): use rstest to simplify test cases (#1095) 2024-05-11 11:03:02 -07:00
Josh McKinney
8de3d52469 chore(changelog): put commit id on the same line as the message (#1093) 2024-05-11 15:30:08 +03:00
Josh McKinney
b30411d1c7 fix: termwiz underline color test (#1094)
Fixes code that doesn't compile in the termwiz tests when
underline-color feature is enabled.
2024-05-10 20:04:58 -07:00
Josh McKinney
aa4260f92c style: use std::fmt instead of importing Debug and Display (#1087)
This is a small universal style change to avoid making this change a
part of other PRs.

[rationale](https://github.com/ratatui-org/ratatui/pull/1083#discussion_r1588466060)
2024-05-04 23:51:24 -07:00
Josh McKinney
5f1e119563 fix: correct feature flag typo for termwiz (#1088)
underline-color was incorrectly spelt as underline_color
2024-05-04 23:43:24 -07:00
Josh McKinney
4d1784f2de feat: re-export ParseColorError as style::ParseColorError (#1086)
Fixes: https://github.com/ratatui-org/ratatui/issues/1085
2024-05-04 14:22:51 -07:00
EdJoPaTo
baedc39494 refactor(buffer): simplify set_stringn logic (#1083) 2024-05-02 16:32:52 -07:00
EdJoPaTo
366c2a0e6d perf(block): use Block::bordered (#1041)
`Block::bordered()` is shorter than
`Block::new().borders(Borders::ALL)`, requires one less import
(`Borders`) and in case `Block::default()` was used before can even be
`const`.
2024-05-02 03:09:48 -07:00
Josh McKinney
bef2bc1e7c chore(cargo): add homepage to Cargo.toml (#1080) 2024-05-02 12:47:10 +03:00
Josh McKinney
64eb3913a4 chore: fixup cargo lint for windows targets (#1071)
Crossterm brings in multiple versions of the same dep
2024-05-01 05:59:09 -07:00
May
e95230beda docs: add note about scrollbar state content length (#1077) 2024-04-30 23:37:43 -07:00
Levi Zim
f4637d40c3 fix(reflow): allow wrapping at zero width whitespace (#1074) 2024-04-28 01:58:28 -07:00
Orhun Parmaksız
da1ade7b2e docs(github): update code owners about past maintainers (#1073)
As per suggestion in
https://github.com/ratatui-org/ratatui/pull/1067#issuecomment-2079766990

It's good for historical purposes!
2024-04-27 16:06:29 -07:00
Josh McKinney
1706b0a3e4 perf(crossterm): Speed up combined fg and bg color changes by up to 20% (#1072) 2024-04-27 14:34:52 -07:00
Eiko Thomas
4392759501 fix(examples): changed user_input example to work with multi-byte unicode chars (#1069)
This is the proposed solution for issue #1068. It solves the bug in the
user_input example with multi-byte UTF-8 characters as input.

Fixes: #1068

---------

Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2024-04-26 22:36:36 -07:00
Paul Sobolik
20fc0ddfca fix(examples): fix key handling in constraints (#1066)
Add check for `KeyEventKind::Press` to constraints example's event
handler to eliminate double keys
on Windows.

Fixes: #1062

---------

Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2024-04-26 21:20:30 -07:00
Orhun Parmaksız
3687f78f6a docs(github): update code owners (#1067)
Removes the team members that are not able to review PRs recently (with
their approval ofc)
2024-04-26 20:51:06 -07:00
Tadeas Uradnik
5fbb77ad20 chore(README): use terminal theme for badges (#1026)
The badges in the readme were all the default theme. Giving them
prettier colors that match the terminal gif is better. I've used the
colors from the VHS repo.
2024-04-25 20:27:28 -07:00
EdJoPaTo
97ee102f17 feat(buffer): track_caller for index_of (#1046)
The caller put in the wrong x/y -> the caller is the cause.
2024-04-25 16:23:09 -07:00
EdJoPaTo
c442dfd1ad perf(canvas): change map data to const instead of static (#1037) 2024-04-24 01:49:57 -07:00
Ibby
0a164965ea fix: use to_string to serialize Color (#934)
Since deserialize now uses `FromStr` to deserialize color, serializing
`Color` RGB values, as well as index values, would produce an output
that would no longer be able to be deserialized without causing an
error.


Color::Rgb will now be serialized as the hex representation of their
value.
For example, with serde_json, `Color::Rgb(255, 0, 255)` would be
serialized as `"#FF00FF"` rather than `{"Rgb": [255, 0, 255]}`.

Color::Indexed will now be serialized as just the string of the index.
For example, with serde_json, `Color::Indexed(10)` would be serialized
as `"10"` rather than `{"Indexed": 10}`.

Other color variants remain the same.
2024-04-23 23:30:00 -07:00
EdJoPaTo
bf0923473c feat(table): make TableState::new const (#1040) 2024-04-21 23:23:56 -06:00
EdJoPaTo
81b96338ea perf(calendar): use const fn (#1039)
Also, do the comparison without `as u8`. Stays the same at runtime and
is cleaner code.
2024-04-21 11:34:17 -06:00
EdJoPaTo
2e71c1874e perf(buffer): simplify Buffer::filled with macro (#1036)
The `vec![]` macro is highly optimized by the Rust team and shorter.
Don't do it manually.

This change is mainly cleaner code. The only production code that uses
this is `Terminal::with_options` and `Terminal::insert_before` so it's
not performance relevant on every render.
2024-04-21 11:29:37 -06:00
Josh McKinney
c75aa1990f build: add clippy::cargo lint (#1053)
Followup to https://github.com/ratatui-org/ratatui/pull/1035 and
https://github.com/ratatui-org/ratatui/discussions/1034

It's reasonable to enable this and deal with breakage by fixing any
specific issues that arise.
2024-04-21 10:43:46 -06:00
mcskware
326a461f9a chore: Add package categories field (#1035)
Add the package categories field in Cargo.toml, with value
`["command-line-interface"]`. This fixes the (currently non-default)
clippy cargo group lint
[`clippy::cargo_common_metadata`](https://rust-lang.github.io/rust-clippy/master/index.html#/cargo_common_metadata).

As per discussion in [Cargo package categories
suggestions](https://github.com/ratatui-org/ratatui/discussions/1034),
this lint is not suggested to be run by default in CI, but rather as an
occasional one-off as part of the larger
[`clippy::cargo`](https://doc.rust-lang.org/stable/clippy/lints.html#cargo)
lint group.
2024-04-19 12:55:12 -07:00
EdJoPaTo
f3172c59d4 refactor(gauge): fix internal typo (#1048) 2024-04-18 13:47:52 +03:00
EdJoPaTo
bef5bcf750 refactor(example): remove pointless new method (#1038)
Use `App::default()` directly.
2024-04-16 21:02:39 +03:00
Marcin Puc
11264787d0 chore(changelog): fix changelog release links (#1033)
<!-- Please read CONTRIBUTING.md before submitting any pull request. -->

Minor oopsie
2024-04-15 23:11:27 +03:00
dependabot[bot]
9b3c260b76 chore(deps): update rstest requirement from 0.18.2 to 0.19.0 (#1031)
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>
<p>Introduce MSRV and minor fixes</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.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>
<h3>Fixed</h3>
<ul>
<li>Wrong doc test</li>
<li>Docs</li>
</ul>
<h2>[0.18.0] 2023/7/4</h2>
<h3>Add</h3>
<ul>
<li>Add support for <code>RSTEST_TIMEOUT</code> environment variable to
define a max timeout
for each function (see <a
href="https://redirect.github.com/la10736/rstest/issues/190">#190</a>
for details).
Thanks to <a
href="https://github.com/aviramha"><code>@​aviramha</code></a> for idea
and PR</li>
<li><code>#[files(&quot;glob path&quot;)]</code> attribute to generate
tests based on files that
satisfy the given glob path (see <a
href="https://redirect.github.com/la10736/rstest/issues/163">#163</a>
for details).</li>
</ul>
<h3>Changed</h3>
<ul>
<li>Switch to <code>syn</code> 2.0 and edition 2021 : minimal Rust
version now is 1.56.0
both for <code>rstest</code> and <code>rstest_reuse</code> (see <a
href="https://redirect.github.com/la10736/rstest/issues/187">#187</a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li>Fixed wired behavior on extraction <code>#[awt]</code> function
attrs (See
<a
href="https://redirect.github.com/la10736/rstest/issues/189">#189</a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="3ffd682568"><code>3ffd682</code></a>
Fix license links</li>
<li><a
href="36ab06d68e"><code>36ab06d</code></a>
Fix license link</li>
<li><a
href="941d8ac7ae"><code>941d8ac</code></a>
Update changelog</li>
<li><a
href="cdff674d16"><code>cdff674</code></a>
Bump version</li>
<li><a
href="e0624fe198"><code>e0624fe</code></a>
Fix clippy warning</li>
<li><a
href="f7b4b57922"><code>f7b4b57</code></a>
Shutup warning on nightly (tests)</li>
<li><a
href="49a7d3816e"><code>49a7d38</code></a>
Shutup warning in night</li>
<li><a
href="b58ce22ef1"><code>b58ce22</code></a>
Set resolver in virtual manifest</li>
<li><a
href="3c2fb9c33c"><code>3c2fb9c</code></a>
Properly handle mutability for awaited futures (<a
href="https://redirect.github.com/la10736/rstest/issues/239">#239</a>)</li>
<li><a
href="61a7007f66"><code>61a7007</code></a>
We're not interested about msrv for tests</li>
<li>Additional commits viewable in <a
href="https://github.com/la10736/rstest/compare/v0.18.2...v0.19.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-04-15 18:55:45 +03:00
Orhun Parmaksız
363c4c54e8 chore(release): prepare for 0.26.2 (#1029) 2024-04-15 13:38:55 +03:00
Josh McKinney
b7778e5cd1 fix(paragraph): unit test typo (#1022) 2024-04-08 17:33:05 -07:00
dependabot[bot]
b5061c5250 chore(deps): update stability requirement from 0.1.1 to 0.2.0 (#1021)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2024-04-08 22:43:04 +03:00
EdJoPaTo
359204c929 refactor: simplify to io::Result (#1016)
Simplifies the code, logic stays exactly the same.
2024-04-03 02:08:42 -07:00
EdJoPaTo
14461c3a35 docs(breaking-changes): typos and markdownlint (#1009) 2024-04-03 02:07:39 -07:00
Josh McKinney
3b002fdcab docs: update incompatible code warning in examples readme (#1013)
Co-authored-by: EdJoPaTo <rfc-conform-git-commit-email@funny-long-domain-label-everyone-hates-as-it-is-too-long.edjopato.de>
2024-04-01 13:34:03 -07:00
Tadeas Uradnik
0207160784 fix(line): line truncation respects alignment (#987)
When rendering a `Line`, the line will be truncated:
- on the right for left aligned lines
- on the left for right aligned lines
- on bot sides for centered lines

E.g. "Hello World" will be rendered as "Hello", "World", "lo wo" for
left, right, centered lines respectively.

Fixes: https://github.com/ratatui-org/ratatui/issues/932
2024-03-30 18:10:33 -07:00
91 changed files with 5235 additions and 4769 deletions

5
.github/CODEOWNERS vendored
View File

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

View File

@@ -2,7 +2,7 @@
This document contains a list of breaking changes in each version and some notes to help migrate
between versions. It is compiled manually from the commit history and changelog. We also tag PRs on
github with a [breaking change] label.
GitHub with a [breaking change] label.
[breaking change]: (https://github.com/ratatui-org/ratatui/issues?q=label%3A%22breaking+change%22)
@@ -37,7 +37,7 @@ This is a quick summary of the sections below:
- `Scrollbar`: symbols moved to `symbols` module
- MSRV is now 1.67.0
- [v0.22.0](#v0220)
- serde representation of `Borders` and `Modifiers` has changed
- `serde` representation of `Borders` and `Modifiers` has changed
- [v0.21.0](#v0210)
- MSRV is now 1.65.0
- `terminal::ViewPort` is now an enum
@@ -49,14 +49,14 @@ This is a quick summary of the sections below:
## [v0.26.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.26.0)
### `Flex::Start` is the new default flex mode for `Layout`
### `Flex::Start` is the new default flex mode for `Layout` ([#881])
[#881]: https://github.com/ratatui-org/ratatui/pull/881
Previously, constraints would stretch to fill all available space, violating constraints if
necessary.
With v0.26.0, `Flex` modes are introduced and the default is `Flex::Start`, which will align
With v0.26.0, `Flex` modes are introduced, and the default is `Flex::Start`, which will align
areas associated with constraints to be beginning of the area. With v0.26.0, additionally,
`Min` constraints grow to fill excess space. These changes will allow users to build layouts
more easily.
@@ -108,7 +108,7 @@ by removing the call to `.collect()`.
[#751]: https://github.com/ratatui-org/ratatui/pull/751
The default() implementation of Table now sets the column_spacing field to 1 and the segment_size
field to SegmentSize::None. This will affect the rendering of a small amount of apps.
field to `SegmentSize::None`. This will affect the rendering of a small amount of apps.
To use the previous default values, call `table.segment_size(Default::default())` and
`table.column_spacing(0)`.
@@ -222,8 +222,8 @@ widget in the default configuration would not show any indication of the selecte
[#664]: https://github.com/ratatui-org/ratatui/pull/664
Previously `Table`s could be constructed without widths. In almost all cases this is an error.
A new widths parameter is now mandatory on `Table::new()`. Existing code of the form:
Previously `Table`s could be constructed without `widths`. In almost all cases this is an error.
A new `widths` parameter is now mandatory on `Table::new()`. Existing code of the form:
```diff
- Table::new(rows).widths(widths)
@@ -281,7 +281,7 @@ let layout = layout::new(Direction::Vertical, [Constraint::Min(1), Constraint::M
## [v0.24.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.24.0)
### ScrollbarState field type changed from `u16` to `usize` ([#456])
### `ScrollbarState` field type changed from `u16` to `usize` ([#456])
[#456]: https://github.com/ratatui-org/ratatui/pull/456
@@ -385,12 +385,12 @@ The MSRV of ratatui is now 1.67 due to an MSRV update in a dependency (`time`).
## [v0.22.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.22.0)
### bitflags updated to 2.3 ([#205])
### `bitflags` updated to 2.3 ([#205])
[#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)
@@ -422,9 +422,9 @@ let terminal = Terminal::with_options(backend, TerminalOptions {
[#168]: https://github.com/ratatui-org/ratatui/issues/168
A new type `Masked` was introduced that implements `From<Text<'a>>`. This causes any code that did
A new type `Masked` was introduced that implements `From<Text<'a>>`. This causes any code that
previously did not need to use type annotations to fail to compile. To fix this, annotate or call
to_string() / to_owned() / as_str() on the value. E.g.:
`to_string()` / `to_owned()` / `as_str()` on the value. E.g.:
```diff
- let paragraph = Paragraph::new("".as_ref());

View File

@@ -2,7 +2,457 @@
All notable changes to this project will be documented in this file.
## [0.26.1](https://github.com/ratatui-org/ratatui/releases/tag/0.26.1) - 2024-02-12
## [0.26.2](https://github.com/ratatui-org/ratatui/releases/tag/v0.26.2) - 2024-04-15
This is a patch release that fixes bugs and adds enhancements, including new iterator constructors, List scroll padding, and various rendering improvements. ✨
**Release highlights**: <https://ratatui.rs/highlights/v0262/>
### Features
- [11b452d](https://github.com/ratatui-org/ratatui/commit/11b452d56fe590188ee7a53fa2dde95513b1a4c7)
*(layout)* Mark various functions as const by @EdJoPaTo in [#951](https://github.com/ratatui-org/ratatui/pull/951)
- [1cff511](https://github.com/ratatui-org/ratatui/commit/1cff51193466f5a94d202b6233d56889eccf6d7b)
*(line)* Impl Styled for Line by @joshka in [#968](https://github.com/ratatui-org/ratatui/pull/968)
````text
This adds `FromIterator` impls for `Line` and `Text` that allow creating
`Line` and `Text` instances from iterators of `Span` and `Line`
instances, respectively.
```rust
let line = Line::from_iter(vec!["Hello".blue(), " world!".green()]);
let line: Line = iter::once("Hello".blue())
.chain(iter::once(" world!".green()))
.collect();
let text = Text::from_iter(vec!["The first line", "The second line"]);
let text: Text = iter::once("The first line")
.chain(iter::once("The second line"))
.collect();
```
````
- [654949b](https://github.com/ratatui-org/ratatui/commit/654949bb00b4522130642f9ad50ab4d9095d921b)
*(list)* Add Scroll Padding to Lists by @CameronBarnes in [#958](https://github.com/ratatui-org/ratatui/pull/958)
````text
Introduces scroll padding, which allows the api user to request that a certain number of ListItems be kept visible above and below the currently selected item while scrolling.
```rust
let list = List::new(items).scroll_padding(1);
```
````
Fixes:https://github.com/ratatui-org/ratatui/pull/955
- [26af650](https://github.com/ratatui-org/ratatui/commit/26af65043ee9f165459dec228d12eaeed9997d92)
*(text)* Add push methods for text and line by @joshka in [#998](https://github.com/ratatui-org/ratatui/pull/998)
````text
Adds the following methods to the `Text` and `Line` structs:
- Text::push_line
- Text::push_span
- Line::push_span
This allows for adding lines and spans to a text object without having
to call methods on the fields directly, which is useful for incremental
construction of text objects.
````
- [b5bdde0](https://github.com/ratatui-org/ratatui/commit/b5bdde079e0e1eda98b9b1bbbba011b770e5b167)
*(text)* Add `FromIterator` impls for `Line` and `Text` by @joshka in [#967](https://github.com/ratatui-org/ratatui/pull/967)
````text
This adds `FromIterator` impls for `Line` and `Text` that allow creating
`Line` and `Text` instances from iterators of `Span` and `Line`
instances, respectively.
```rust
let line = Line::from_iter(vec!["Hello".blue(), " world!".green()]);
let line: Line = iter::once("Hello".blue())
.chain(iter::once(" world!".green()))
.collect();
let text = Text::from_iter(vec!["The first line", "The second line"]);
let text: Text = iter::once("The first line")
.chain(iter::once("The second line"))
.collect();
```
````
- [12f67e8](https://github.com/ratatui-org/ratatui/commit/12f67e810fad0f907546408192a2380b590ff7bd)
*(uncategorized)* Impl Widget for `&str` and `String` by @kdheepak in [#952](https://github.com/ratatui-org/ratatui/pull/952)
````text
Currently, `f.render_widget("hello world".bold(), area)` works but
`f.render_widget("hello world", area)` doesn't. This PR changes that my
implementing `Widget` for `&str` and `String`. This makes it easier to
render strings with no styles as widgets.
Example usage:
```rust
terminal.draw(|f| f.render_widget("Hello World!", f.size()))?;
```
---------
````
### Bug Fixes
- [0207160](https://github.com/ratatui-org/ratatui/commit/02071607848c51250b4663722c52e19c8ce1c5e2)
*(line)* Line truncation respects alignment by @TadoTheMiner in [#987](https://github.com/ratatui-org/ratatui/pull/987)
````text
When rendering a `Line`, the line will be truncated:
- on the right for left aligned lines
- on the left for right aligned lines
- on bot sides for centered lines
E.g. "Hello World" will be rendered as "Hello", "World", "lo wo" for
left, right, centered lines respectively.
````
Fixes:https://github.com/ratatui-org/ratatui/issues/932
- [c56f49b](https://github.com/ratatui-org/ratatui/commit/c56f49b9fb1c7f1c8c97749119e85f81882ca9a9)
*(list)* Saturating_sub to fix highlight_symbol overflow by @mrjackwills in [#949](https://github.com/ratatui-org/ratatui/pull/949)
````text
An overflow (pedantically an underflow) can occur if the
highlight_symbol is a multi-byte char, and area is reduced to a size
less than that char length.
````
- [b7778e5](https://github.com/ratatui-org/ratatui/commit/b7778e5cd15d0d4b28f7bbb8b3c62950748e333a)
*(paragraph)* Unit test typo by @joshka in [#1022](https://github.com/ratatui-org/ratatui/pull/1022)
- [943c043](https://github.com/ratatui-org/ratatui/commit/943c0431d968a82b23a2f31527f32e57f86f8a7c)
*(scrollbar)* Dont render on 0 length track by @EdJoPaTo in [#964](https://github.com/ratatui-org/ratatui/pull/964)
````text
Fixes a panic when `track_length - 1` is used. (clamp panics on `-1.0`
being smaller than `0.0`)
````
- [742a5ea](https://github.com/ratatui-org/ratatui/commit/742a5ead066bec14047f6ab7ffa3ac8307eea715)
*(text)* Fix panic when rendering out of bounds by @joshka in [#997](https://github.com/ratatui-org/ratatui/pull/997)
````text
Previously it was possible to cause a panic when rendering to an area
outside of the buffer bounds. Instead this now correctly renders nothing
to the buffer.
````
- [f6c4e44](https://github.com/ratatui-org/ratatui/commit/f6c4e447e65fe10f4fc7fcc9e9c4312acad41096)
*(uncategorized)* Ensure that paragraph correctly renders styled text by @joshka in [#992](https://github.com/ratatui-org/ratatui/pull/992)
````text
Paragraph was ignoring the new `Text::style` field added in 0.26.0
````
Fixes:https://github.com/ratatui-org/ratatui/issues/990
- [35e971f](https://github.com/ratatui-org/ratatui/commit/35e971f7ebb0deadc613b561b15511abd48bdb54)
*(uncategorized)* Scrollbar thumb not visible on long lists by @ThomasMiz in [#959](https://github.com/ratatui-org/ratatui/pull/959)
````text
When displaying somewhat-long lists, the `Scrollbar` widget sometimes did not display a thumb character, and only the track will be visible.
````
### Refactor
- [6fd5f63](https://github.com/ratatui-org/ratatui/commit/6fd5f631bbd58156d9fcae196040bb0248097819)
*(lint)* Prefer idiomatic for loops by @EdJoPaTo
- [37b957c](https://github.com/ratatui-org/ratatui/commit/37b957c7e167a7ecda07b8a60cee5de71efcc55e)
*(lints)* Add lints to scrollbar by @EdJoPaTo
- [c12bcfe](https://github.com/ratatui-org/ratatui/commit/c12bcfefa26529610886040bd96f2b6762436b15)
*(non-src)* Apply pedantic lints by @EdJoPaTo in [#976](https://github.com/ratatui-org/ratatui/pull/976)
````text
Fixes many not yet enabled lints (mostly pedantic) on everything that is
not the lib (examples, benches, tests). Therefore, this is not containing
anything that can be a breaking change.
Lints are not enabled as that should be the job of #974. I created this
as a separate PR as its mostly independent and would only clutter up the
diff of #974 even more.
Also see
https://github.com/ratatui-org/ratatui/pull/974#discussion_r1506458743
---------
````
- [8719608](https://github.com/ratatui-org/ratatui/commit/8719608bdaf32ba92bdfdd60569cf73f7070a618)
*(span)* Rename to_aligned_line into into_aligned_line by @EdJoPaTo in [#993](https://github.com/ratatui-org/ratatui/pull/993)
````text
With the Rust method naming conventions these methods are into methods
consuming the Span. Therefore, it's more consistent to use `into_`
instead of `to_`.
```rust
Span::to_centered_line
Span::to_left_aligned_line
Span::to_right_aligned_line
```
Are marked deprecated and replaced with the following
```rust
Span::into_centered_line
Span::into_left_aligned_line
Span::into_right_aligned_line
```
````
- [b831c56](https://github.com/ratatui-org/ratatui/commit/b831c5688c6f1fbfa6ae2bcd70d803a54fcf0196)
*(widget-ref)* Clippy::needless_pass_by_value by @EdJoPaTo
- [359204c](https://github.com/ratatui-org/ratatui/commit/359204c9298cc26ea21807d886d596de0329bacc)
*(uncategorized)* Simplify to io::Result by @EdJoPaTo in [#1016](https://github.com/ratatui-org/ratatui/pull/1016)
````text
Simplifies the code, logic stays exactly the same.
````
- [8e68db9](https://github.com/ratatui-org/ratatui/commit/8e68db9e2f57fcbf7cb5140006bbbd4dd80bf907)
*(uncategorized)* Remove pointless default on internal structs by @EdJoPaTo in [#980](https://github.com/ratatui-org/ratatui/pull/980)
See #978
Also remove other derives. They are unused and just slow down
compilation.
- [3be189e](https://github.com/ratatui-org/ratatui/commit/3be189e3c6ebd418d13138ff32bc4a749dc840cf)
*(uncategorized)* Clippy::thread_local_initializer_can_be_made_const by @EdJoPaTo
````text
enabled by default on nightly
````
- [5c4efac](https://github.com/ratatui-org/ratatui/commit/5c4efacd1d70bb295d90ffaa73853dc206c187fb)
*(uncategorized)* Clippy::map_err_ignore by @EdJoPaTo
- [bbb6d65](https://github.com/ratatui-org/ratatui/commit/bbb6d65e063df9a74ab6487b2216183c1fdd7230)
*(uncategorized)* Clippy::else_if_without_else by @EdJoPaTo
- [fdb14dc](https://github.com/ratatui-org/ratatui/commit/fdb14dc7cd69788e2ed20709e767f7631b11ffa2)
*(uncategorized)* Clippy::redundant_type_annotations by @EdJoPaTo
- [9b3b23a](https://github.com/ratatui-org/ratatui/commit/9b3b23ac14518a1ef23065d4a5da0fb047b18213)
*(uncategorized)* Remove literal suffix by @EdJoPaTo
````text
its not needed and can just be assumed
````
related:clippy::(un)separated_literal_suffix
- [58b6e0b](https://github.com/ratatui-org/ratatui/commit/58b6e0be0f4db3d90005e130e4b84cd865179785)
*(uncategorized)* Clippy::should_panic_without_expect by @EdJoPaTo
- [c870a41](https://github.com/ratatui-org/ratatui/commit/c870a41057ac0c14c2e72e762b37689dc32e7b23)
*(uncategorized)* Clippy::many_single_char_names by @EdJoPaTo
- [a6036ad](https://github.com/ratatui-org/ratatui/commit/a6036ad78911653407f607f5efa556a055d3dce9)
*(uncategorized)* Clippy::similar_names by @EdJoPaTo
- [060d26b](https://github.com/ratatui-org/ratatui/commit/060d26b6dc6e1027dbf46ae98b0ebba83701f941)
*(uncategorized)* Clippy::match_same_arms by @EdJoPaTo
- [fcbea9e](https://github.com/ratatui-org/ratatui/commit/fcbea9ee68591344a29a7b2e83f1c8c878857aeb)
*(uncategorized)* Clippy::uninlined_format_args by @EdJoPaTo
- [14b24e7](https://github.com/ratatui-org/ratatui/commit/14b24e75858af48f39d5880e7f6c9adeac1b1da9)
*(uncategorized)* Clippy::if_not_else by @EdJoPaTo
- [5ed1f43](https://github.com/ratatui-org/ratatui/commit/5ed1f43c627053f25d9ee711677ebec6cb8fcd85)
*(uncategorized)* Clippy::redundant_closure_for_method_calls by @EdJoPaTo
- [c8c7924](https://github.com/ratatui-org/ratatui/commit/c8c7924e0ca84351f5ed5c54e79611ce16d4dc37)
*(uncategorized)* Clippy::too_many_lines by @EdJoPaTo
- [e3afe7c](https://github.com/ratatui-org/ratatui/commit/e3afe7c8a14c1cffd7de50782a7acf0f95f41673)
*(uncategorized)* Clippy::unreadable_literal by @EdJoPaTo
- [a1f54de](https://github.com/ratatui-org/ratatui/commit/a1f54de7d60fa6c57be29bf8f02a675e58b7b9c2)
*(uncategorized)* Clippy::bool_to_int_with_if by @EdJoPaTo
- [b8ea190](https://github.com/ratatui-org/ratatui/commit/b8ea190bf2cde8c18e2ac8276d2eb57d219db263)
*(uncategorized)* Clippy::cast_lossless by @EdJoPaTo
- [0de5238](https://github.com/ratatui-org/ratatui/commit/0de5238ed3613f2d663f5e9628ca7b2aa205ed02)
*(uncategorized)* Dead_code by @EdJoPaTo
````text
enabled by default, only detected by nightly yet
````
- [df5dddf](https://github.com/ratatui-org/ratatui/commit/df5dddfbc9c679d15a5a90ea79bb1f8946d5cb9c)
*(uncategorized)* Unused_imports by @EdJoPaTo
````text
enabled by default, only detected on nightly yet
````
- [f1398ae](https://github.com/ratatui-org/ratatui/commit/f1398ae6cb1abd32106923d64844b482c7ba6f82)
*(uncategorized)* Clippy::useless_vec by @EdJoPaTo
````text
Lint enabled by default but only nightly finds this yet
````
- [525848f](https://github.com/ratatui-org/ratatui/commit/525848ff4e066526d402fecf1d5b9c63cff1f22a)
*(uncategorized)* Manually apply clippy::use_self for impl with lifetimes by @EdJoPaTo
- [660c718](https://github.com/ratatui-org/ratatui/commit/660c7183c7a10dc453d80dfb651d9534536960b9)
*(uncategorized)* Clippy::empty_line_after_doc_comments by @EdJoPaTo
- [ab951fa](https://github.com/ratatui-org/ratatui/commit/ab951fae8166c9321728ba942b48552dfe4d9c55)
*(uncategorized)* Clippy::return_self_not_must_use by @EdJoPaTo
- [3cd4369](https://github.com/ratatui-org/ratatui/commit/3cd436917649a93b4b80d0c4a0343284e0585522)
*(uncategorized)* Clippy::doc_markdown by @EdJoPaTo
- [9bc014d](https://github.com/ratatui-org/ratatui/commit/9bc014d7f16efdb70fcd6b6b786fe74eac7b9bdf)
*(uncategorized)* Clippy::items_after_statements by @EdJoPaTo
- [36a0cd5](https://github.com/ratatui-org/ratatui/commit/36a0cd56e5645533a1d6c2720536fa10a56b0d40)
*(uncategorized)* Clippy::deref_by_slicing by @EdJoPaTo
- [f7f6692](https://github.com/ratatui-org/ratatui/commit/f7f66928a8833532a3bc97292665640285e7aafa)
*(uncategorized)* Clippy::equatable_if_let by @EdJoPaTo
- [01418eb](https://github.com/ratatui-org/ratatui/commit/01418eb7c2e1874cb4070828c485d81ea171b18d)
*(uncategorized)* Clippy::default_trait_access by @EdJoPaTo
- [8536760](https://github.com/ratatui-org/ratatui/commit/8536760e7802a498f7c6d9fe8fb4c7920a1c6e71)
*(uncategorized)* Clippy::inefficient_to_string by @EdJoPaTo
- [a558b19](https://github.com/ratatui-org/ratatui/commit/a558b19c9a7b90a1ed3f309301f49f0b483e02ec)
*(uncategorized)* Clippy::implicit_clone by @EdJoPaTo
- [5b00e3a](https://github.com/ratatui-org/ratatui/commit/5b00e3aae98cb5c20c10bec944948a75ac83f956)
*(uncategorized)* Clippy::use_self by @EdJoPaTo
- [27680c0](https://github.com/ratatui-org/ratatui/commit/27680c05ce1670f026ad23c446ada321c1c755f0)
*(uncategorized)* Clippy::semicolon_if_nothing_returned by @EdJoPaTo
### Documentation
- [14461c3](https://github.com/ratatui-org/ratatui/commit/14461c3a3554c95905ebca433fc3d4dae1e1acda)
*(breaking-changes)* Typos and markdownlint by @EdJoPaTo in [#1009](https://github.com/ratatui-org/ratatui/pull/1009)
- [d0067c8](https://github.com/ratatui-org/ratatui/commit/d0067c8815d5244d319934d58a9366c8ad36b3e5)
*(license)* Update copyright years by @orhun in [#962](https://github.com/ratatui-org/ratatui/pull/962)
- [88bfb5a](https://github.com/ratatui-org/ratatui/commit/88bfb5a43027cf3410ad560772c5bfdbaa3d58b7)
*(text)* Update Text and Line docs by @joshka in [#969](https://github.com/ratatui-org/ratatui/pull/969)
- [3b002fd](https://github.com/ratatui-org/ratatui/commit/3b002fdcab964ce3f65f55dc8053d9678ae247a3)
*(uncategorized)* Update incompatible code warning in examples readme by @joshka in [#1013](https://github.com/ratatui-org/ratatui/pull/1013)
### Performance
- [e02f476](https://github.com/ratatui-org/ratatui/commit/e02f4768ce2ee30473200fe98e2687e42acb9c33)
*(borders)* Allow border!() in const by @EdJoPaTo in [#977](https://github.com/ratatui-org/ratatui/pull/977)
````text
This allows more compiler optimizations when the macro is used.
````
- [541f0f9](https://github.com/ratatui-org/ratatui/commit/541f0f99538762a07d68a71b2989ecc6ff6f71ef)
*(cell)* Use const CompactString::new_inline by @EdJoPaTo in [#979](https://github.com/ratatui-org/ratatui/pull/979)
````text
Some minor find when messing around trying to `const` all the things.
While `reset()` and `default()` can not be `const` it's still a benefit
when their contents are.
````
- [65e7923](https://github.com/ratatui-org/ratatui/commit/65e792375396c3160d76964ef0dfc4fb1e53be41)
*(scrollbar)* Const creation by @EdJoPaTo in [#963](https://github.com/ratatui-org/ratatui/pull/963)
````text
A bunch of `const fn` allow for more performance and `Default` now uses the `const` new implementations.
````
- [8195f52](https://github.com/ratatui-org/ratatui/commit/8195f526cb4b321f337dcbe9e689cc7f6eb84065)
*(uncategorized)* Clippy::needless_pass_by_value by @EdJoPaTo
- [183c07e](https://github.com/ratatui-org/ratatui/commit/183c07ef436cbb8fb0bec418042b44b4fedd836f)
*(uncategorized)* Clippy::trivially_copy_pass_by_ref by @EdJoPaTo
- [a13867f](https://github.com/ratatui-org/ratatui/commit/a13867ffceb2f8f57f4540049754c2f916fd3efc)
*(uncategorized)* Clippy::cloned_instead_of_copied by @EdJoPaTo
- [3834374](https://github.com/ratatui-org/ratatui/commit/3834374652b46c5ddbfedcf8dea2086fd762f884)
*(uncategorized)* Clippy::missing_const_for_fn by @EdJoPaTo
### Miscellaneous Tasks
- [125ee92](https://github.com/ratatui-org/ratatui/commit/125ee929ee9009b97a270e2e105a3f1167ab13d7)
*(docs)* Fix: fix typos in crate documentation by @orhun in [#1002](https://github.com/ratatui-org/ratatui/pull/1002)
- [38c17e0](https://github.com/ratatui-org/ratatui/commit/38c17e091cf3f4de2d196ecdd6a40129019eafc4)
*(editorconfig)* Set and apply some defaults by @EdJoPaTo
- [07da90a](https://github.com/ratatui-org/ratatui/commit/07da90a7182035b24f870bcbf0a0ffaad75eb48b)
*(funding)* Add eth address for receiving funds from drips.network by @BenJam in [#994](https://github.com/ratatui-org/ratatui/pull/994)
- [078e97e](https://github.com/ratatui-org/ratatui/commit/078e97e4ff65c02afa7c884914ecd38a6e959b58)
*(github)* Add EdJoPaTo as a maintainer by @orhun in [#986](https://github.com/ratatui-org/ratatui/pull/986)
- [b0314c5](https://github.com/ratatui-org/ratatui/commit/b0314c5731b32f51f5b6ca71a5194c6d7f265972)
*(uncategorized)* Remove conventional commit check for PR by @Valentin271 in [#950](https://github.com/ratatui-org/ratatui/pull/950)
````text
This removes conventional commit check for PRs.
Since we use the PR title and description this is useless. It fails a
lot of time and we ignore it.
IMPORTANT NOTE: This does **not** mean Ratatui abandons conventional
commits. This only relates to commits in PRs.
````
### Build
- [6e6ba27](https://github.com/ratatui-org/ratatui/commit/6e6ba27a122560bcf47b0efd20b7095f1bfd8714)
*(lint)* Warn on pedantic and allow the rest by @EdJoPaTo
- [c4ce7e8](https://github.com/ratatui-org/ratatui/commit/c4ce7e8ff6f00875e1ead5b68052f0db737bd44d)
*(uncategorized)* Enable more satisfied lints by @EdJoPaTo
````text
These lints dont generate warnings and therefore dont need refactoring.
I think they are useful in the future.
````
- [a4e84a6](https://github.com/ratatui-org/ratatui/commit/a4e84a6a7f6f5b80903799028f30e2a4438f2807)
*(uncategorized)* Increase msrv to 1.74.0 by @EdJoPaTo [**breaking**]
````text
configure lints in Cargo.toml requires 1.74.0
````
BREAKING CHANGE:rust 1.74 is required now
### New Contributors
* @TadoTheMiner made their first contribution in [#987](https://github.com/ratatui-org/ratatui/pull/987)
* @BenJam made their first contribution in [#994](https://github.com/ratatui-org/ratatui/pull/994)
* @CameronBarnes made their first contribution in [#958](https://github.com/ratatui-org/ratatui/pull/958)
* @ThomasMiz made their first contribution in [#959](https://github.com/ratatui-org/ratatui/pull/959)
**Full Changelog**: https://github.com/ratatui-org/ratatui/compare/v0.26.1...0.26.2
## [0.26.1](https://github.com/ratatui-org/ratatui/releases/tag/v0.26.1) - 2024-02-12
This is a patch release that fixes bugs and adds enhancements, including new iterators, title options for blocks, and various rendering improvements. ✨
@@ -161,7 +611,7 @@ Here is the list of contributors who have contributed to `ratatui` for the first
* @mo8it
* @m4rch3n1ng
## [0.26.0](https://github.com/ratatui-org/ratatui/releases/tag/0.26.0) - 2024-02-02
## [0.26.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.26.0) - 2024-02-02
We are excited to announce the new version of `ratatui` - a Rust library that's all about cooking up TUIs 🐭
@@ -1818,7 +2268,7 @@ Shout out to our new sponsors!
* @atuinsh
* @JeftavanderHorst!
## [0.25.0](https://github.com/ratatui-org/ratatui/releases/tag/0.25.0) - 2023-12-18
## [0.25.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.25.0) - 2023-12-18
We are thrilled to announce the new version of `ratatui` - a Rust library that's all about cooking up TUIs 🐭
@@ -2303,7 +2753,7 @@ Here is the list of contributors who have contributed to `ratatui` for the first
* @YeungKC
* @lyuha
## [0.24.0](https://github.com/ratatui-org/ratatui/releases/tag/0.24.0) - 2023-10-23
## [0.24.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.24.0) - 2023-10-23
We are excited to announce the new version of `ratatui` - a Rust library that's all about cooking up TUIs 🐭

View File

@@ -1,11 +1,13 @@
[package]
name = "ratatui"
version = "0.26.1" # crate version
version = "0.26.2" # 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/"
keywords = ["tui", "terminal", "dashboard"]
repository = "https://github.com/ratatui-org/ratatui"
homepage = "https://ratatui.rs"
keywords = ["tui", "terminal", "dashboard"]
categories = ["command-line-interface"]
readme = "README.md"
license = "MIT"
exclude = [
@@ -23,24 +25,23 @@ rust-version = "1.74.0"
[badges]
[dependencies]
crossterm = { version = "0.27", optional = true }
termion = { version = "3.0", optional = true }
termwiz = { version = "0.22.0", optional = true }
serde = { version = "1", optional = true, features = ["derive"] }
bitflags = "2.3"
cassowary = "0.3"
indoc = "2.0"
compact_str = "0.7.1"
crossterm = { version = "0.27", optional = true }
document-features = { version = "0.2.7", optional = true }
itertools = "0.12"
lru = "0.12.0"
paste = "1.0.2"
serde = { version = "1", optional = true, features = ["derive"] }
stability = "0.2.0"
strum = { version = "0.26", features = ["derive"] }
termion = { version = "3.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"
document-features = { version = "0.2.7", optional = true }
lru = "0.12.0"
stability = "0.1.1"
compact_str = "0.7.1"
[dev-dependencies]
anyhow = "1.0.71"
@@ -54,16 +55,19 @@ criterion = { version = "0.5.1", features = ["html_reports"] }
derive_builder = "0.20.0"
fakeit = "1.1"
font8x8 = "0.3.1"
indoc = "2"
palette = "0.7.3"
pretty_assertions = "1.4.0"
rand = "0.8.5"
rand_chacha = "0.3.1"
rstest = "0.18.2"
rstest = "0.19.0"
serde_json = "1.0.109"
[lints.rust]
unsafe_code = "forbid"
[lints.clippy]
cargo = { level = "warn", priority = -1 }
pedantic = { level = "warn", priority = -1 }
cast_possible_truncation = "allow"
cast_possible_wrap = "allow"
@@ -159,6 +163,10 @@ harness = false
name = "block"
harness = false
[[bench]]
name = "line"
harness = false
[[bench]]
name = "list"
harness = false

View File

@@ -7,11 +7,12 @@ skip_core_tasks = true
# all features except the backend ones
ALL_FEATURES = "all-widgets,macros,serde"
[env.ALL_FEATURES_FLAG]
# Windows does not support building termion, so this avoids the build failure by providing two
# sets of flags, one for Windows and one for other platforms.
# Windows: --features=all-widgets,macros,serde,crossterm,termwiz,underline-color
# Other: --features=all-widgets,macros,serde,crossterm,termion,termwiz,underline-color
ALL_FEATURES_FLAG = { source = "${CARGO_MAKE_RUST_TARGET_OS}", default_value = "--features=all-widgets,macros,serde,crossterm,termion,termwiz,unstable", mapping = { "windows" = "--features=all-widgets,macros,serde,crossterm,termwiz,unstable" } }
source = "${CARGO_MAKE_RUST_TARGET_OS}"
default_value = "--features=all-widgets,macros,serde,crossterm,termion,termwiz,underline-color,unstable"
mapping = { "windows" = "--features=all-widgets,macros,serde,crossterm,termwiz,underline-color,unstable" }
[tasks.default]
alias = "ci"

View File

@@ -162,7 +162,7 @@ fn handle_events() -> io::Result<bool> {
fn ui(frame: &mut Frame) {
frame.render_widget(
Paragraph::new("Hello World!")
.block(Block::default().title("Greeting").borders(Borders::ALL)),
.block(Block::bordered().title("Greeting")),
frame.size(),
);
}
@@ -207,11 +207,11 @@ fn ui(frame: &mut Frame) {
)
.split(main_layout[1]);
frame.render_widget(
Block::default().borders(Borders::ALL).title("Left"),
Block::bordered().title("Left"),
inner_layout[0],
);
frame.render_widget(
Block::default().borders(Borders::ALL).title("Right"),
Block::bordered().title("Right"),
inner_layout[1],
);
}
@@ -327,24 +327,23 @@ Running this example produces the following output:
[Termwiz]: https://crates.io/crates/termwiz
[tui-rs]: https://crates.io/crates/tui
[GitHub Sponsors]: https://github.com/sponsors/ratatui-org
[Crate Badge]: https://img.shields.io/crates/v/ratatui?logo=rust&style=flat-square
[License Badge]: https://img.shields.io/crates/l/ratatui?style=flat-square
[CI Badge]:
https://img.shields.io/github/actions/workflow/status/ratatui-org/ratatui/ci.yml?style=flat-square&logo=github
[Crate Badge]: https://img.shields.io/crates/v/ratatui?logo=rust&style=flat-square&logoColor=E05D44&color=E05D44
[License Badge]: https://img.shields.io/crates/l/ratatui?style=flat-square&color=1370D3
[CI Badge]: https://img.shields.io/github/actions/workflow/status/ratatui-org/ratatui/ci.yml?style=flat-square&logo=github
[CI Workflow]: https://github.com/ratatui-org/ratatui/actions/workflows/ci.yml
[Codecov Badge]:
https://img.shields.io/codecov/c/github/ratatui-org/ratatui?logo=codecov&style=flat-square&token=BAQ8SOKEST
https://img.shields.io/codecov/c/github/ratatui-org/ratatui?logo=codecov&style=flat-square&token=BAQ8SOKEST&color=C43AC3&logoColor=C43AC3
[Codecov]: https://app.codecov.io/gh/ratatui-org/ratatui
[Deps.rs Badge]: https://deps.rs/repo/github/ratatui-org/ratatui/status.svg?style=flat-square
[Deps.rs]: https://deps.rs/repo/github/ratatui-org/ratatui
[Discord Badge]:
https://img.shields.io/discord/1070692720437383208?label=discord&logo=discord&style=flat-square
https://img.shields.io/discord/1070692720437383208?label=discord&logo=discord&style=flat-square&color=1370D3&logoColor=1370D3
[Discord Server]: https://discord.gg/pMCEU9hNEj
[Docs Badge]: https://img.shields.io/docsrs/ratatui?logo=rust&style=flat-square
[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
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
[Sponsors Badge]: https://img.shields.io/github/sponsors/ratatui-org?logo=github&style=flat-square
[Sponsors Badge]: https://img.shields.io/github/sponsors/ratatui-org?logo=github&style=flat-square&color=1370D3
<!-- cargo-rdme end -->

View File

@@ -1,11 +1,10 @@
use criterion::{criterion_group, criterion_main, BatchSize, Bencher, BenchmarkId, Criterion};
use criterion::{criterion_group, criterion_main, BatchSize, Bencher, Criterion};
use ratatui::{
buffer::Buffer,
layout::Rect,
prelude::Alignment,
layout::{Alignment, Rect},
widgets::{
block::{Position, Title},
Block, Borders, Padding, Widget,
Block, Padding, Widget,
},
};
@@ -13,32 +12,31 @@ use ratatui::{
fn block(c: &mut Criterion) {
let mut group = c.benchmark_group("block");
for buffer_size in [
Rect::new(0, 0, 100, 50), // vertically split screen
Rect::new(0, 0, 200, 50), // 1080p fullscreen with medium font
Rect::new(0, 0, 256, 256), // Max sized area
for (width, height) in [
(100, 50), // vertically split screen
(200, 50), // 1080p fullscreen with medium font
(256, 256), // Max sized area
] {
let buffer_area = buffer_size.area();
let buffer_size = Rect::new(0, 0, width, height);
// Render an empty block
group.bench_with_input(
BenchmarkId::new("render_empty", buffer_area),
format!("render_empty/{width}x{height}"),
&Block::new(),
|b, block| render(b, block, buffer_size),
);
// Render with all features
group.bench_with_input(
BenchmarkId::new("render_all_feature", buffer_area),
&Block::new()
.borders(Borders::ALL)
format!("render_all_feature/{width}x{height}"),
&Block::bordered()
.padding(Padding::new(5, 5, 2, 2))
.title("test title")
.title(
Title::from("bottom left title")
.alignment(Alignment::Right)
.position(Position::Bottom),
)
.padding(Padding::new(5, 5, 2, 2)),
),
|b, block| render(b, block, buffer_size),
);
}

39
benches/line.rs Normal file
View File

@@ -0,0 +1,39 @@
use std::hint::black_box;
use criterion::{criterion_group, criterion_main, Criterion};
use ratatui::{
buffer::Buffer,
layout::{Alignment, Rect},
style::Stylize,
text::Line,
widgets::Widget,
};
fn line_render(criterion: &mut Criterion) {
for alignment in [Alignment::Left, Alignment::Center, Alignment::Right] {
let mut group = criterion.benchmark_group(format!("line_render/{alignment}"));
group.sample_size(1000);
let line = &Line::from(vec![
"This".red(),
" ".green(),
"is".italic(),
" ".blue(),
"SPARTA!!".bold(),
])
.alignment(alignment);
for width in [0, 3, 4, 6, 7, 10, 42] {
let area = Rect::new(0, 0, width, 1);
group.bench_function(width.to_string(), |bencher| {
let mut buffer = Buffer::empty(area);
bencher.iter(|| black_box(line).render(area, &mut buffer));
});
}
group.finish();
}
}
criterion_group!(benches, line_render);
criterion_main!(benches);

View File

@@ -1,4 +1,9 @@
# configuration for https://github.com/orhun/git-cliff
# git-cliff ~ configuration file
# https://git-cliff.org/docs/configuration
[remote.github]
owner = "ratatui-org"
repo = "ratatui"
[changelog]
# changelog header
@@ -21,8 +26,10 @@ body = """
{% endif -%}
{% macro commit(commit) -%}
- [{{ commit.id | truncate(length=7, end="") }}]({{ "https://github.com/ratatui-org/ratatui/commit/" ~ commit.id }})
*({{commit.scope | default(value = "uncategorized") | lower }})* {{ commit.message | upper_first }}
- [{{ commit.id | truncate(length=7, end="") }}]({{ "https://github.com/ratatui-org/ratatui/commit/" ~ commit.id }}) \
*({{commit.scope | default(value = "uncategorized") | lower }})* {{ commit.message | upper_first | trim }}\
{% if commit.github.username %} by @{{ commit.github.username }}{%- endif -%}\
{% if commit.github.pr_number %} in [#{{ commit.github.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.github.pr_number }}){%- endif %}\
{%- if commit.breaking %} [**breaking**]{% endif %}
{%- if commit.body %}
@@ -49,6 +56,28 @@ body = """
{%- endif -%}
{%- endfor -%}
{%- endfor %}
{% if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}
### New Contributors
{%- endif %}\
{% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}
* @{{ contributor.username }} made their first contribution
{%- if contributor.pr_number %} in \
[#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \
{%- endif %}
{%- endfor -%}
{% if version %}
{% if previous.version %}
**Full Changelog**: {{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }}
{% endif %}
{% else -%}
{% raw %}\n{% endraw %}
{% endif %}
{%- macro remote_url() -%}
https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}\
{% endmacro %}
"""
@@ -68,7 +97,7 @@ filter_unconventional = true
split_commits = false
# regex for preprocessing the commit messages
commit_preprocessors = [
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/ratatui-org/ratatui/issues/${2}))" },
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "" },
{ pattern = '(better safe shared layout cache)', replace = "perf(layout): ${1}" },
{ pattern = '(Clarify README.md)', replace = "docs(readme): ${1}" },
{ pattern = '(Update README.md)', replace = "docs(readme): ${1}" },

17
clippy.toml Normal file
View File

@@ -0,0 +1,17 @@
avoid-breaking-exported-api = false
# https://rust-lang.github.io/rust-clippy/master/index.html#/multiple_crate_versions
# ratatui -> bitflags v2.3
# termwiz -> wezterm-blob-leases -> mac_address -> nix -> bitflags v1.3.2
# crossterm -> all the windows- deps https://github.com/ratatui-org/ratatui/pull/1064#issuecomment-2078848980
allowed-duplicate-crates = [
"bitflags",
"windows-targets",
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]

View File

@@ -1,20 +1,20 @@
# Examples
This folder contains unreleased code. View the [examples for the latest release
(0.25.0)](https://github.com/ratatui-org/ratatui/tree/v0.25.0/examples) instead.
This folder might use unreleased code. View the examples for the latest release instead.
> [!WARNING]
>
> There are backwards incompatible changes in these examples, as they are designed to compile
> There may be backwards incompatible changes in these examples, as they are designed to compile
> against the `main` branch.
>
> There are a few workaround for this problem:
>
> - View the examples as they were when the latest version was release by selecting the tag that
> matches that version. E.g. <https://github.com/ratatui-org/ratatui/tree/v0.25.0/examples>. There
> is a combo box at the top of this page which allows you to select any previous tagged version.
> - To view the code locally, checkout the tag using `git switch --detach v0.25.0`.
> - Use the latest [alpha version of Ratatui]. These are released weekly on Saturdays.
> matches that version. E.g. <https://github.com/ratatui-org/ratatui/tree/v0.26.1/examples>.
> - If you're viewing this file on GitHub, there is a combo box at the top of this page which
> allows you to select any previous tagged version.
> - To view the code locally, checkout the tag. E.g. `git switch --detach v0.26.1`.
> - Use the latest [alpha version of Ratatui] in your app. These are released weekly on Saturdays.
> - Compile your code against the main branch either locally by adding e.g. `path = "../ratatui"` to
> the dependency, or remotely by adding `git = "https://github.com/ratatui-org/ratatui"`
>

View File

@@ -26,7 +26,7 @@ use crossterm::{
};
use ratatui::{
prelude::*,
widgets::{Bar, BarChart, BarGroup, Block, Borders, Paragraph},
widgets::{Bar, BarChart, BarGroup, Block, Paragraph},
};
struct Company<'a> {
@@ -159,7 +159,7 @@ fn ui(frame: &mut Frame, app: &App) {
let [left, right] = horizontal.areas(bottom);
let barchart = BarChart::default()
.block(Block::default().title("Data1").borders(Borders::ALL))
.block(Block::bordered().title("Data1"))
.data(&app.data)
.bar_width(9)
.bar_style(Style::default().fg(Color::Yellow))
@@ -217,7 +217,7 @@ fn draw_bar_with_group_labels(f: &mut Frame, app: &App, area: Rect) {
let groups = create_groups(app, false);
let mut barchart = BarChart::default()
.block(Block::default().title("Data1").borders(Borders::ALL))
.block(Block::bordered().title("Data1"))
.bar_width(7)
.group_gap(3);
@@ -246,7 +246,7 @@ fn draw_horizontal_bars(f: &mut Frame, app: &App, area: Rect) {
let groups = create_groups(app, true);
let mut barchart = BarChart::default()
.block(Block::default().title("Data1").borders(Borders::ALL))
.block(Block::bordered().title("Data1"))
.bar_width(1)
.group_gap(1)
.bar_gap(0)
@@ -286,15 +286,13 @@ fn draw_legend(f: &mut Frame, area: Rect) {
"- Company B",
Style::default().fg(Color::Yellow),
)),
Line::from(vec![Span::styled(
Line::from(Span::styled(
"- Company C",
Style::default().fg(Color::White),
)]),
)),
];
let block = Block::default()
.borders(Borders::ALL)
.style(Style::default().fg(Color::White));
let block = Block::bordered().style(Style::default().fg(Color::White));
let paragraph = Paragraph::new(text).block(block);
f.render_widget(paragraph, area);
}

View File

@@ -160,23 +160,20 @@ fn render_border_type(
frame: &mut Frame,
area: Rect,
) {
let block = Block::new()
.borders(Borders::ALL)
let block = Block::bordered()
.border_type(border_type)
.title(format!("BorderType::{border_type:#?}"));
frame.render_widget(paragraph.clone().block(block), area);
}
fn render_styled_borders(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
let block = Block::new()
.borders(Borders::ALL)
let block = Block::bordered()
.border_style(Style::new().blue().on_white().bold().italic())
.title("Styled borders");
frame.render_widget(paragraph.clone().block(block), area);
}
fn render_styled_block(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
let block = Block::new()
.borders(Borders::ALL)
let block = Block::bordered()
.style(Style::new().blue().on_white().bold().italic())
.title("Styled block");
frame.render_widget(paragraph.clone().block(block), area);
@@ -184,8 +181,7 @@ fn render_styled_block(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
// Note: this currently renders incorrectly, see https://github.com/ratatui-org/ratatui/issues/349
fn render_styled_title(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
let block = Block::new()
.borders(Borders::ALL)
let block = Block::bordered()
.title("Styled title")
.title_style(Style::new().blue().on_white().bold().italic());
frame.render_widget(paragraph.clone().block(block), area);
@@ -196,21 +192,19 @@ fn render_styled_title_content(paragraph: &Paragraph, frame: &mut Frame, area: R
"Styled ".blue().on_white().bold().italic(),
"title content".red().on_white().bold().italic(),
]);
let block = Block::new().borders(Borders::ALL).title(title);
let block = Block::bordered().title(title);
frame.render_widget(paragraph.clone().block(block), area);
}
fn render_multiple_titles(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
let block = Block::new()
.borders(Borders::ALL)
let block = Block::bordered()
.title("Multiple".blue().on_white().bold().italic())
.title("Titles".red().on_white().bold().italic());
frame.render_widget(paragraph.clone().block(block), area);
}
fn render_multiple_title_positions(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
let block = Block::new()
.borders(Borders::ALL)
let block = Block::bordered()
.title(
Title::from("top left")
.position(Position::Top)
@@ -245,16 +239,15 @@ fn render_multiple_title_positions(paragraph: &Paragraph, frame: &mut Frame, are
}
fn render_padding(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
let block = Block::new()
.borders(Borders::ALL)
.title("Padding")
.padding(Padding::new(5, 10, 1, 2));
let block = Block::bordered()
.padding(Padding::new(5, 10, 1, 2))
.title("Padding");
frame.render_widget(paragraph.clone().block(block), area);
}
fn render_nested_blocks(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
let outer_block = Block::new().borders(Borders::ALL).title("Outer block");
let inner_block = Block::new().borders(Borders::ALL).title("Inner block");
let outer_block = Block::bordered().title("Outer block");
let inner_block = Block::bordered().title("Inner block");
let inner = outer_block.inner(area);
frame.render_widget(outer_block, area);
frame.render_widget(paragraph.clone().block(inner_block), inner);

View File

@@ -137,7 +137,7 @@ impl App {
fn map_canvas(&self) -> impl Widget + '_ {
Canvas::default()
.block(Block::default().borders(Borders::ALL).title("World"))
.block(Block::bordered().title("World"))
.marker(self.marker)
.paint(|ctx| {
ctx.draw(&Map {
@@ -152,7 +152,7 @@ impl App {
fn pong_canvas(&self) -> impl Widget + '_ {
Canvas::default()
.block(Block::default().borders(Borders::ALL).title("Pong"))
.block(Block::bordered().title("Pong"))
.marker(self.marker)
.paint(|ctx| {
ctx.draw(&self.ball);
@@ -167,7 +167,7 @@ impl App {
let bottom = 0.0;
let top = f64::from(area.height).mul_add(2.0, -4.0);
Canvas::default()
.block(Block::default().borders(Borders::ALL).title("Rects"))
.block(Block::bordered().title("Rects"))
.marker(self.marker)
.x_bounds([left, right])
.y_bounds([bottom, top])

View File

@@ -26,7 +26,7 @@ use crossterm::{
};
use ratatui::{
prelude::*,
widgets::{block::Title, Axis, Block, Borders, Chart, Dataset, GraphType, LegendPosition},
widgets::{block::Title, Axis, Block, Chart, Dataset, GraphType, LegendPosition},
};
#[derive(Clone)]
@@ -184,11 +184,7 @@ fn render_chart1(f: &mut Frame, area: Rect, app: &App) {
];
let chart = Chart::new(datasets)
.block(
Block::default()
.title("Chart 1".cyan().bold())
.borders(Borders::ALL),
)
.block(Block::bordered().title("Chart 1".cyan().bold()))
.x_axis(
Axis::default()
.title("X Axis")
@@ -217,13 +213,11 @@ fn render_line_chart(f: &mut Frame, area: Rect) {
let chart = Chart::new(datasets)
.block(
Block::default()
.title(
Title::default()
.content("Line chart".cyan().bold())
.alignment(Alignment::Center),
)
.borders(Borders::ALL),
Block::bordered().title(
Title::default()
.content("Line chart".cyan().bold())
.alignment(Alignment::Center),
),
)
.x_axis(
Axis::default()
@@ -269,7 +263,7 @@ fn render_scatter(f: &mut Frame, area: Rect) {
let chart = Chart::new(datasets)
.block(
Block::new().borders(Borders::all()).title(
Block::bordered().title(
Title::default()
.content("Scatter chart".cyan().bold())
.alignment(Alignment::Center),

View File

@@ -230,12 +230,12 @@ fn render_indexed_colors(frame: &mut Frame, area: Rect) {
}
fn title_block(title: String) -> Block<'static> {
Block::default()
Block::new()
.borders(Borders::TOP)
.border_style(Style::new().dark_gray())
.title(title)
.title_alignment(Alignment::Center)
.border_style(Style::new().dark_gray())
.title_style(Style::new().reset())
.title(title)
}
fn render_indexed_grayscale(frame: &mut Frame, area: Rect) {

View File

@@ -436,7 +436,7 @@ impl ConstraintBlock {
} else {
main_color
};
Block::default()
Block::new()
.fg(Self::TEXT_COLOR)
.bg(selected_color)
.render(area, buf);

View File

@@ -19,7 +19,7 @@ use std::io::{self, stdout};
use color_eyre::{config::HookBuilder, Result};
use crossterm::{
event::{self, Event, KeyCode},
event::{self, Event, KeyCode, KeyEventKind},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
};
@@ -109,6 +109,9 @@ impl App {
fn handle_events(&mut self) -> Result<()> {
if let Event::Key(key) = event::read()? {
use KeyCode::*;
if key.kind != KeyEventKind::Press {
return Ok(());
}
match key.code {
Char('q') | Esc => self.quit(),
Char('l') | Right => self.next(),
@@ -194,7 +197,7 @@ impl App {
);
Paragraph::new(width_bar.dark_gray())
.centered()
.block(Block::default().padding(Padding {
.block(Block::new().padding(Padding {
left: 0,
right: 0,
top: 1,

View File

@@ -14,7 +14,7 @@ pub fn draw(f: &mut Frame, app: &mut App) {
.iter()
.map(|t| text::Line::from(Span::styled(*t, Style::default().fg(Color::Green))))
.collect::<Tabs>()
.block(Block::default().borders(Borders::ALL).title(app.title))
.block(Block::bordered().title(app.title))
.highlight_style(Style::default().fg(Color::Yellow))
.select(app.tabs.index);
f.render_widget(tabs, chunks[0]);
@@ -46,12 +46,12 @@ fn draw_gauges(f: &mut Frame, app: &mut App, area: Rect) {
])
.margin(1)
.split(area);
let block = Block::default().borders(Borders::ALL).title("Graphs");
let block = Block::bordered().title("Graphs");
f.render_widget(block, area);
let label = format!("{:.2}%", app.progress * 100.0);
let gauge = Gauge::default()
.block(Block::default().title("Gauge:"))
.block(Block::new().title("Gauge:"))
.gauge_style(
Style::default()
.fg(Color::Magenta)
@@ -64,7 +64,7 @@ fn draw_gauges(f: &mut Frame, app: &mut App, area: Rect) {
f.render_widget(gauge, chunks[0]);
let sparkline = Sparkline::default()
.block(Block::default().title("Sparkline:"))
.block(Block::new().title("Sparkline:"))
.style(Style::default().fg(Color::Green))
.data(&app.sparkline.points)
.bar_set(if app.enhanced_graphics {
@@ -75,7 +75,7 @@ fn draw_gauges(f: &mut Frame, app: &mut App, area: Rect) {
f.render_widget(sparkline, chunks[1]);
let line_gauge = LineGauge::default()
.block(Block::default().title("LineGauge:"))
.block(Block::new().title("LineGauge:"))
.gauge_style(Style::default().fg(Color::Magenta))
.line_set(if app.enhanced_graphics {
symbols::line::THICK
@@ -110,7 +110,7 @@ fn draw_charts(f: &mut Frame, app: &mut App, area: Rect) {
.map(|i| ListItem::new(vec![text::Line::from(Span::raw(*i))]))
.collect();
let tasks = List::new(tasks)
.block(Block::default().borders(Borders::ALL).title("List"))
.block(Block::bordered().title("List"))
.highlight_style(Style::default().add_modifier(Modifier::BOLD))
.highlight_symbol("> ");
f.render_stateful_widget(tasks, chunks[0], &mut app.tasks.state);
@@ -138,12 +138,12 @@ fn draw_charts(f: &mut Frame, app: &mut App, area: Rect) {
ListItem::new(content)
})
.collect();
let logs = List::new(logs).block(Block::default().borders(Borders::ALL).title("List"));
let logs = List::new(logs).block(Block::bordered().title("List"));
f.render_stateful_widget(logs, chunks[1], &mut app.logs.state);
}
let barchart = BarChart::default()
.block(Block::default().borders(Borders::ALL).title("Bar chart"))
.block(Block::bordered().title("Bar chart"))
.data(&app.barchart)
.bar_width(3)
.bar_gap(2)
@@ -195,14 +195,12 @@ fn draw_charts(f: &mut Frame, app: &mut App, area: Rect) {
];
let chart = Chart::new(datasets)
.block(
Block::default()
.title(Span::styled(
"Chart",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
))
.borders(Borders::ALL),
Block::bordered().title(Span::styled(
"Chart",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
)),
)
.x_axis(
Axis::default()
@@ -254,7 +252,7 @@ fn draw_text(f: &mut Frame, area: Rect) {
"One more thing is that it should display unicode characters: 10€"
),
];
let block = Block::default().borders(Borders::ALL).title(Span::styled(
let block = Block::bordered().title(Span::styled(
"Footer",
Style::default()
.fg(Color::Magenta)
@@ -292,11 +290,11 @@ fn draw_second_tab(f: &mut Frame, app: &mut App, area: Rect) {
.style(Style::default().fg(Color::Yellow))
.bottom_margin(1),
)
.block(Block::default().title("Servers").borders(Borders::ALL));
.block(Block::bordered().title("Servers"));
f.render_widget(table, chunks[0]);
let map = Canvas::default()
.block(Block::default().title("World").borders(Borders::ALL))
.block(Block::bordered().title("World"))
.paint(|ctx| {
ctx.draw(&Map {
color: Color::White,
@@ -390,6 +388,6 @@ fn draw_third_tab(f: &mut Frame, _app: &mut App, area: Rect) {
Constraint::Ratio(1, 3),
],
)
.block(Block::default().title("Colors").borders(Borders::ALL));
.block(Block::bordered().title("Colors"));
f.render_widget(table, chunks[0]);
}

View File

@@ -38,14 +38,10 @@ enum Tab {
}
pub fn run(terminal: &mut Terminal<impl Backend>) -> Result<()> {
App::new().run(terminal)
App::default().run(terminal)
}
impl App {
pub fn new() -> Self {
Self::default()
}
/// Run the app until the user quits.
pub fn run(&mut self, terminal: &mut Terminal<impl Backend>) -> Result<()> {
while self.is_running() {

View File

@@ -276,8 +276,6 @@ fn render_glyph(glyph: [u8; 8], area: Rect, buf: &mut Buffer, pixel_size: PixelS
#[cfg(test)]
mod tests {
use ratatui::assert_buffer_eq;
use super::*;
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
@@ -308,7 +306,7 @@ mod tests {
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 80, 8));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines(vec![
let expected = Buffer::with_lines([
" ████ ██ ███ ████ ██ ",
"██ ██ ██ ██ ",
"███ ███ █████ ███ ██ ██ ████ ██ ███ █████ ████ ",
@@ -318,7 +316,7 @@ mod tests {
" ████ ████ ██ ██ ██ ████ ████ ███████ ████ ██ ██ ████ ",
" █████ ",
]);
assert_buffer_eq!(buf, expected);
assert_eq!(buf, expected);
Ok(())
}
@@ -329,7 +327,7 @@ mod tests {
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 70, 6));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines(vec![
let expected = Buffer::with_lines([
"██████ █ ███",
"█ ██ █ ██ ██",
" ██ ██ ███ ██ ██ █████ ████ ████ █████ ████ ██",
@@ -337,7 +335,7 @@ mod tests {
" ██ ██ ██ ██ ██ ██ ██ ██ █████ ██ ██████ ██ ██",
" ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ █ ██ ██ ██",
]);
assert_buffer_eq!(buf, expected);
assert_eq!(buf, expected);
Ok(())
}
@@ -348,7 +346,7 @@ mod tests {
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 40, 16));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines(vec![
let expected = Buffer::with_lines([
"██ ██ ███ █ ██ ",
"███ ███ ██ ██ ",
"███████ ██ ██ ██ █████ ███ ",
@@ -366,7 +364,7 @@ mod tests {
"███████ ████ ██ ██ ████ █████ ",
" ",
]);
assert_buffer_eq!(buf, expected);
assert_eq!(buf, expected);
Ok(())
}
@@ -378,18 +376,17 @@ mod tests {
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 48, 8));
big_text.render(buf.area, &mut buf);
let mut expected = Buffer::with_lines(vec![
" ████ █ ███ ███ ",
"██ ██ ██ ██ ██ ",
"███ █████ ██ ██ ██ ████ ██ ",
" ███ ██ ██ ██ ██ ██ ██ █████ ",
" ███ ██ ██ ██ ██ ██████ ██ ██ ",
"██ ██ ██ █ █████ ██ ██ ██ ██ ",
" ████ ██ ██ ████ ████ ███ ██ ",
" █████ ",
let expected = Buffer::with_lines([
" ████ █ ███ ███ ".bold(),
"██ ██ ██ ██ ██ ".bold(),
"███ █████ ██ ██ ██ ████ ██ ".bold(),
" ███ ██ ██ ██ ██ ██ ██ █████ ".bold(),
" ███ ██ ██ ██ ██ ██████ ██ ██ ".bold(),
"██ ██ ██ █ █████ ██ ██ ██ ██ ".bold(),
" ████ ██ ██ ████ ████ ███ ██ ".bold(),
" █████ ".bold(),
]);
expected.set_style(Rect::new(0, 0, 48, 8), Style::new().bold());
assert_buffer_eq!(buf, expected);
assert_eq!(buf, expected);
Ok(())
}
@@ -404,7 +401,7 @@ mod tests {
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 40, 24));
big_text.render(buf.area, &mut buf);
let mut expected = Buffer::with_lines(vec![
let mut expected = Buffer::with_lines([
"██████ ███ ",
" ██ ██ ██ ",
" ██ ██ ████ ██ ",
@@ -433,7 +430,7 @@ mod tests {
expected.set_style(Rect::new(0, 0, 24, 8), Style::new().red());
expected.set_style(Rect::new(0, 8, 40, 8), Style::new().green());
expected.set_style(Rect::new(0, 16, 32, 8), Style::new().blue());
assert_buffer_eq!(buf, expected);
assert_eq!(buf, expected);
Ok(())
}
@@ -445,13 +442,13 @@ mod tests {
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 80, 4));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines(vec![
let expected = Buffer::with_lines([
"▄█▀▀█▄ ▀▀ ▀██ ▀██▀ ▀▀ ",
"▀██▄ ▀██ ██▀▀█▄ ▄█▀▀▄█▀ ██ ▄█▀▀█▄ ██ ▀██ ██▀▀█▄ ▄█▀▀█▄ ",
"▄▄ ▀██ ██ ██ ██ ▀█▄▄██ ██ ██▀▀▀▀ ██ ▄█ ██ ██ ██ ██▀▀▀▀ ",
" ▀▀▀▀ ▀▀▀▀ ▀▀ ▀▀ ▄▄▄▄█▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀▀▀▀ ▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀ ",
]);
assert_buffer_eq!(buf, expected);
assert_eq!(buf, expected);
Ok(())
}
@@ -463,12 +460,12 @@ mod tests {
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 70, 3));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines(vec![
let expected = Buffer::with_lines([
"█▀██▀█ ▄█ ▀██",
" ██ ▀█▄█▀█▄ ██ ██ ██▀▀█▄ ▄█▀▀█▄ ▀▀▀█▄ ▀██▀▀ ▄█▀▀█▄ ▄▄▄██",
" ██ ██ ▀▀ ██ ██ ██ ██ ██ ▄▄ ▄█▀▀██ ██ ▄ ██▀▀▀▀ ██ ██",
]);
assert_buffer_eq!(buf, expected);
assert_eq!(buf, expected);
Ok(())
}
@@ -480,7 +477,7 @@ mod tests {
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 40, 8));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines(vec![
let expected = Buffer::with_lines([
"██▄ ▄██ ▀██ ▄█ ▀▀ ",
"███████ ██ ██ ██ ▀██▀▀ ▀██ ",
"██ ▀ ██ ██ ██ ██ ██ ▄ ██ ",
@@ -490,7 +487,7 @@ mod tests {
" ██ ▄█ ██ ██ ██ ██▀▀▀▀ ▀▀▀█▄ ",
"▀▀▀▀▀▀▀ ▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀ ▀▀▀▀▀ ",
]);
assert_buffer_eq!(buf, expected);
assert_eq!(buf, expected);
Ok(())
}
@@ -503,14 +500,13 @@ mod tests {
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 48, 4));
big_text.render(buf.area, &mut buf);
let mut expected = Buffer::with_lines(vec![
"▄█▀▀█▄ ▄█ ▀██ ▀██ ",
"▀██▄ ▀██▀▀ ██ ██ ██ ▄█▀▀█▄ ▄▄▄██ ",
"▄▄ ▀██ ██ ▄ ▀█▄▄██ ██ ██▀▀▀▀ ██ ██ ",
" ▀▀▀▀ ▀▀ ▄▄▄▄█▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀ ▀▀ ",
let expected = Buffer::with_lines([
"▄█▀▀█▄ ▄█ ▀██ ▀██ ".bold(),
"▀██▄ ▀██▀▀ ██ ██ ██ ▄█▀▀█▄ ▄▄▄██ ".bold(),
"▄▄ ▀██ ██ ▄ ▀█▄▄██ ██ ██▀▀▀▀ ██ ██ ".bold(),
" ▀▀▀▀ ▀▀ ▄▄▄▄█▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀ ▀▀ ".bold(),
]);
expected.set_style(Rect::new(0, 0, 48, 4), Style::new().bold());
assert_buffer_eq!(buf, expected);
assert_eq!(buf, expected);
Ok(())
}
@@ -526,7 +522,7 @@ mod tests {
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 40, 12));
big_text.render(buf.area, &mut buf);
let mut expected = Buffer::with_lines(vec![
let mut expected = Buffer::with_lines([
"▀██▀▀█▄ ▀██ ",
" ██▄▄█▀ ▄█▀▀█▄ ▄▄▄██ ",
" ██ ▀█▄ ██▀▀▀▀ ██ ██ ",
@@ -543,7 +539,7 @@ mod tests {
expected.set_style(Rect::new(0, 0, 24, 4), Style::new().red());
expected.set_style(Rect::new(0, 4, 40, 4), Style::new().green());
expected.set_style(Rect::new(0, 8, 32, 4), Style::new().blue());
assert_buffer_eq!(buf, expected);
assert_eq!(buf, expected);
Ok(())
}
@@ -555,7 +551,7 @@ mod tests {
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 40, 8));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines(vec![
let expected = Buffer::with_lines([
"▐█▌ █ ▐█ ██ █ ",
"█ █ █ ▐▌ ",
"█▌ ▐█ ██▌ ▐█▐▌ █ ▐█▌ ▐▌ ▐█ ██▌ ▐█▌ ",
@@ -565,7 +561,7 @@ mod tests {
"▐█▌ ▐█▌ █ █ █ ▐█▌ ▐█▌ ███▌▐█▌ █ █ ▐█▌ ",
" ██▌ ",
]);
assert_buffer_eq!(buf, expected);
assert_eq!(buf, expected);
Ok(())
}
@@ -577,7 +573,7 @@ mod tests {
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 35, 6));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines(vec![
let expected = Buffer::with_lines([
"███ ▐ ▐█",
"▌█▐ █ █",
" █ █▐█ █ █ ██▌ ▐█▌ ▐█▌ ▐██ ▐█▌ █",
@@ -585,7 +581,7 @@ mod tests {
" █ ▐▌▐▌█ █ █ █ █ ▐██ █ ███ █ █",
" █ ▐▌ █ █ █ █ █ █ █ █ █▐ █ █ █",
]);
assert_buffer_eq!(buf, expected);
assert_eq!(buf, expected);
Ok(())
}
@@ -597,7 +593,7 @@ mod tests {
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 16));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines(vec![
let expected = Buffer::with_lines([
"█ ▐▌ ▐█ ▐ █ ",
"█▌█▌ █ █ ",
"███▌█ █ █ ▐██ ▐█ ",
@@ -615,7 +611,7 @@ mod tests {
"███▌▐█▌ █ █ ▐█▌ ██▌ ",
" ",
]);
assert_buffer_eq!(buf, expected);
assert_eq!(buf, expected);
Ok(())
}
@@ -628,18 +624,17 @@ mod tests {
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 24, 8));
big_text.render(buf.area, &mut buf);
let mut expected = Buffer::with_lines(vec![
"▐█▌ ▐ ▐█ ▐█ ",
"█ █ █ █ █ ",
"█▌ ▐██ █ █ █ ▐█▌ █ ",
"▐█ █ █ █ █ █ █ ▐██ ",
" ▐█ █ █ █ █ ███ █ █ ",
"█ █ █▐ ▐██ █ █ █ █ ",
"▐█▌ ▐▌ █ ▐█▌ ▐█▌ ▐█▐▌",
" ██▌ ",
let expected = Buffer::with_lines([
"▐█▌ ▐ ▐█ ▐█ ".bold(),
"█ █ █ █ █ ".bold(),
"█▌ ▐██ █ █ █ ▐█▌ █ ".bold(),
"▐█ █ █ █ █ █ █ ▐██ ".bold(),
" ▐█ █ █ █ █ ███ █ █ ".bold(),
"█ █ █▐ ▐██ █ █ █ █ ".bold(),
"▐█▌ ▐▌ █ ▐█▌ ▐█▌ ▐█▐▌".bold(),
" ██▌ ".bold(),
]);
expected.set_style(Rect::new(0, 0, 24, 8), Style::new().bold());
assert_buffer_eq!(buf, expected);
assert_eq!(buf, expected);
Ok(())
}
@@ -655,7 +650,7 @@ mod tests {
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 24));
big_text.render(buf.area, &mut buf);
let mut expected = Buffer::with_lines(vec![
let mut expected = Buffer::with_lines([
"███ ▐█ ",
"▐▌▐▌ █ ",
"▐▌▐▌▐█▌ █ ",
@@ -684,7 +679,7 @@ mod tests {
expected.set_style(Rect::new(0, 0, 12, 8), Style::new().red());
expected.set_style(Rect::new(0, 8, 20, 8), Style::new().green());
expected.set_style(Rect::new(0, 16, 16, 8), Style::new().blue());
assert_buffer_eq!(buf, expected);
assert_eq!(buf, expected);
Ok(())
}
@@ -717,13 +712,13 @@ mod tests {
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 40, 4));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines(vec![
let expected = Buffer::with_lines([
"▟▀▙ ▀ ▝█ ▜▛ ▀ ",
"▜▙ ▝█ █▀▙ ▟▀▟▘ █ ▟▀▙ ▐▌ ▝█ █▀▙ ▟▀▙ ",
"▄▝█ █ █ █ ▜▄█ █ █▀▀ ▐▌▗▌ █ █ █ █▀▀ ",
"▝▀▘ ▝▀▘ ▀ ▀ ▄▄▛ ▝▀▘ ▝▀▘ ▀▀▀▘▝▀▘ ▀ ▀ ▝▀▘ ",
]);
assert_buffer_eq!(buf, expected);
assert_eq!(buf, expected);
Ok(())
}
@@ -735,12 +730,12 @@ mod tests {
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 35, 3));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines(vec![
let expected = Buffer::with_lines([
"▛█▜ ▟ ▝█",
" █ ▜▟▜▖█ █ █▀▙ ▟▀▙ ▝▀▙ ▝█▀ ▟▀▙ ▗▄█",
" █ ▐▌▝▘█ █ █ █ █ ▄ ▟▀█ █▗ █▀▀ █ █",
]);
assert_buffer_eq!(buf, expected);
assert_eq!(buf, expected);
Ok(())
}
@@ -752,7 +747,7 @@ mod tests {
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 8));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines(vec![
let expected = Buffer::with_lines([
"█▖▟▌ ▝█ ▟ ▀ ",
"███▌█ █ █ ▝█▀ ▝█ ",
"█▝▐▌█ █ █ █▗ █ ",
@@ -762,7 +757,7 @@ mod tests {
"▐▌▗▌ █ █ █ █▀▀ ▝▀▙ ",
"▀▀▀▘▝▀▘ ▀ ▀ ▝▀▘ ▀▀▘ ",
]);
assert_buffer_eq!(buf, expected);
assert_eq!(buf, expected);
Ok(())
}
@@ -775,14 +770,13 @@ mod tests {
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 24, 4));
big_text.render(buf.area, &mut buf);
let mut expected = Buffer::with_lines(vec![
"▟▀▙ ▟ ▝█ ▝█ ",
"▜▙ ▝█▀ █ █ █ ▟▀▙ ▗▄█ ",
"▄▝█ █▗ ▜▄█ █ █▀▀ █ █ ",
"▝▀▘ ▝▘ ▄▄▛ ▝▀▘ ▝▀▘ ▝▀▝▘",
let expected = Buffer::with_lines([
"▟▀▙ ▟ ▝█ ▝█ ".bold(),
"▜▙ ▝█▀ █ █ █ ▟▀▙ ▗▄█ ".bold(),
"▄▝█ █▗ ▜▄█ █ █▀▀ █ █ ".bold(),
"▝▀▘ ▝▘ ▄▄▛ ▝▀▘ ▝▀▘ ▝▀▝▘".bold(),
]);
expected.set_style(Rect::new(0, 0, 24, 4), Style::new().bold());
assert_buffer_eq!(buf, expected);
assert_eq!(buf, expected);
Ok(())
}
@@ -798,7 +792,7 @@ mod tests {
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 12));
big_text.render(buf.area, &mut buf);
let mut expected = Buffer::with_lines(vec![
let mut expected = Buffer::with_lines([
"▜▛▜▖ ▝█ ",
"▐▙▟▘▟▀▙ ▗▄█ ",
"▐▌▜▖█▀▀ █ █ ",
@@ -815,7 +809,7 @@ mod tests {
expected.set_style(Rect::new(0, 0, 12, 4), Style::new().red());
expected.set_style(Rect::new(0, 4, 20, 4), Style::new().green());
expected.set_style(Rect::new(0, 8, 16, 4), Style::new().blue());
assert_buffer_eq!(buf, expected);
assert_eq!(buf, expected);
Ok(())
}
}

View File

@@ -39,7 +39,7 @@ pub use theme::*;
fn main() -> Result<()> {
errors::init_hooks()?;
let terminal = &mut term::init()?;
App::new().run(terminal)?;
App::default().run(terminal)?;
term::restore()?;
Ok(())
}

View File

@@ -49,10 +49,10 @@ fn render_hops(selected_row: usize, area: Rect, buf: &mut Buffer) {
.iter()
.map(|hop| Row::new(vec![hop.host, hop.address]))
.collect_vec();
let block = Block::default()
.title("Traceroute bad.horse".bold().white())
let block = Block::new()
.padding(Padding::new(1, 1, 1, 1))
.title_alignment(Alignment::Center)
.padding(Padding::new(1, 1, 1, 1));
.title("Traceroute bad.horse".bold().white());
StatefulWidget::render(
Table::new(rows, [Constraint::Max(100), Constraint::Length(15)])
.header(Row::new(vec!["Host", "Address"]).set_style(THEME.traceroute.header))

View File

@@ -51,8 +51,7 @@ fn main() -> io::Result<()> {
fn hello_world(frame: &mut Frame) {
frame.render_widget(
Paragraph::new("Hello World!")
.block(Block::default().title("Greeting").borders(Borders::ALL)),
Paragraph::new("Hello World!").block(Block::bordered().title("Greeting")),
frame.size(),
);
}
@@ -87,8 +86,8 @@ fn layout(frame: &mut Frame) {
Block::new().borders(Borders::TOP).title("Status Bar"),
status_bar,
);
frame.render_widget(Block::default().borders(Borders::ALL).title("Left"), left);
frame.render_widget(Block::default().borders(Borders::ALL).title("Right"), right);
frame.render_widget(Block::bordered().title("Left"), left);
frame.render_widget(Block::bordered().title("Right"), right);
}
fn styling(frame: &mut Frame) {

View File

@@ -206,11 +206,11 @@ impl App {
fn title_block(title: &str) -> Block {
let title = Title::from(title).alignment(Alignment::Center);
Block::default()
.title(title)
Block::new()
.borders(Borders::NONE)
.fg(CUSTOM_LABEL_COLOR)
.padding(Padding::vertical(1))
.title(title)
.fg(CUSTOM_LABEL_COLOR)
}
fn init_error_hooks() -> color_eyre::Result<()> {

View File

@@ -236,7 +236,7 @@ fn run_app<B: Backend>(
fn ui(f: &mut Frame, downloads: &Downloads) {
let area = f.size();
let block = Block::default().title(block::Title::from("Progress").alignment(Alignment::Center));
let block = Block::new().title(block::Title::from("Progress").alignment(Alignment::Center));
f.render_widget(block, area);
let vertical = Layout::vertical([Constraint::Length(2), Constraint::Length(4)]).margin(1);

View File

@@ -26,7 +26,7 @@ use itertools::Itertools;
use ratatui::{
layout::Constraint::*,
prelude::*,
widgets::{Block, Borders, Paragraph},
widgets::{Block, Paragraph},
};
fn main() -> Result<(), Box<dyn Error>> {
@@ -190,10 +190,9 @@ fn render_example_combination(
title: &str,
constraints: Vec<(Constraint, Constraint)>,
) {
let block = Block::default()
let block = Block::bordered()
.title(title.gray())
.style(Style::reset())
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::DarkGray));
let inner = block.inner(area);
frame.render_widget(block, area);

View File

@@ -189,13 +189,13 @@ impl Widget for &mut 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::default()
let outer_block = Block::new()
.borders(Borders::NONE)
.fg(TEXT_COLOR)
.bg(TODO_HEADER_BG)
.title_alignment(Alignment::Center)
.title("TODO List")
.title_alignment(Alignment::Center);
let inner_block = Block::default()
.fg(TEXT_COLOR)
.bg(TODO_HEADER_BG);
let inner_block = Block::new()
.borders(Borders::NONE)
.fg(TEXT_COLOR)
.bg(NORMAL_ROW_COLOR);
@@ -246,16 +246,16 @@ impl App<'_> {
};
// We show the list item's info under the list in this paragraph
let outer_info_block = Block::default()
let outer_info_block = Block::new()
.borders(Borders::NONE)
.fg(TEXT_COLOR)
.bg(TODO_HEADER_BG)
.title_alignment(Alignment::Center)
.title("TODO Info")
.title_alignment(Alignment::Center);
let inner_info_block = Block::default()
.fg(TEXT_COLOR)
.bg(TODO_HEADER_BG);
let inner_info_block = Block::new()
.borders(Borders::NONE)
.bg(NORMAL_ROW_COLOR)
.padding(Padding::horizontal(1));
.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.

View File

@@ -37,7 +37,7 @@ use crossterm::{
};
use ratatui::{
prelude::*,
widgets::{Block, Borders, Paragraph},
widgets::{Block, Paragraph},
};
type Result<T> = std::result::Result<T, Box<dyn Error>>;
@@ -142,11 +142,9 @@ fn ui(f: &mut Frame, app: &App) {
Line::from("try first without the panic handler to see the difference"),
];
let b = Block::default()
.title("Panic Handler Demo")
.borders(Borders::ALL);
let paragraph = Paragraph::new(text)
.block(Block::bordered().title("Panic Handler Demo"))
.centered();
let p = Paragraph::new(text).block(b).centered();
f.render_widget(p, f.size());
f.render_widget(paragraph, f.size());
}

View File

@@ -26,7 +26,7 @@ use crossterm::{
};
use ratatui::{
prelude::*,
widgets::{Block, Borders, Paragraph, Wrap},
widgets::{Block, Paragraph, Wrap},
};
struct App {
@@ -105,7 +105,7 @@ fn ui(f: &mut Frame, app: &App) {
let mut long_line = s.repeat(usize::from(size.width) / s.len() + 4);
long_line.push('\n');
let block = Block::default().black();
let block = Block::new().black();
f.render_widget(block, size);
let layout = Layout::vertical([Constraint::Ratio(1, 4); 4]).split(size);
@@ -127,8 +127,7 @@ fn ui(f: &mut Frame, app: &App) {
];
let create_block = |title| {
Block::default()
.borders(Borders::ALL)
Block::bordered()
.style(Style::default().fg(Color::Gray))
.title(Span::styled(
title,

View File

@@ -25,7 +25,7 @@ use crossterm::{
};
use ratatui::{
prelude::*,
widgets::{Block, Borders, Clear, Paragraph, Wrap},
widgets::{Block, Clear, Paragraph, Wrap},
};
struct App {
@@ -98,14 +98,11 @@ fn ui(f: &mut Frame, app: &App) {
.wrap(Wrap { trim: true });
f.render_widget(paragraph, instructions);
let block = Block::default()
.title("Content")
.borders(Borders::ALL)
.on_blue();
let block = Block::bordered().title("Content").on_blue();
f.render_widget(block, content);
if app.show_popup {
let block = Block::default().title("Popup").borders(Borders::ALL);
let block = Block::bordered().title("Popup");
let area = centered_rect(60, 20, area);
f.render_widget(Clear, area); //this clears out the background
f.render_widget(block, area);

View File

@@ -151,18 +151,18 @@ fn ui(f: &mut Frame, app: &App) {
.split(f.size());
let sparkline = Sparkline::default()
.block(
Block::default()
.title("Data1")
.borders(Borders::LEFT | Borders::RIGHT),
Block::new()
.borders(Borders::LEFT | Borders::RIGHT)
.title("Data1"),
)
.data(&app.data1)
.style(Style::default().fg(Color::Yellow));
f.render_widget(sparkline, chunks[0]);
let sparkline = Sparkline::default()
.block(
Block::default()
.title("Data2")
.borders(Borders::LEFT | Borders::RIGHT),
Block::new()
.borders(Borders::LEFT | Borders::RIGHT)
.title("Data2"),
)
.data(&app.data2)
.style(Style::default().bg(Color::Green));
@@ -170,9 +170,9 @@ fn ui(f: &mut Frame, app: &App) {
// Multiline
let sparkline = Sparkline::default()
.block(
Block::default()
.title("Data3")
.borders(Borders::LEFT | Borders::RIGHT),
Block::new()
.borders(Borders::LEFT | Borders::RIGHT)
.title("Data3"),
)
.data(&app.data3)
.style(Style::default().fg(Color::Red));

View File

@@ -331,10 +331,9 @@ fn render_footer(f: &mut Frame, app: &App, area: Rect) {
.style(Style::new().fg(app.colors.row_fg).bg(app.colors.buffer_bg))
.centered()
.block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::new().fg(app.colors.footer_border_color))
.border_type(BorderType::Double),
Block::bordered()
.border_type(BorderType::Double)
.border_style(Style::new().fg(app.colors.footer_border_color)),
);
f.render_widget(info_footer, area);
}

View File

@@ -205,8 +205,7 @@ impl SelectedTab {
/// A block surrounding the tab's content
fn block(self) -> Block<'static> {
Block::default()
.borders(Borders::ALL)
Block::bordered()
.border_set(symbols::border::PROPORTIONAL_TALL)
.padding(Padding::horizontal(1))
.border_style(self.palette().c700)

View File

@@ -36,7 +36,7 @@ use crossterm::{
};
use ratatui::{
prelude::*,
widgets::{Block, Borders, List, ListItem, Paragraph},
widgets::{Block, List, ListItem, Paragraph},
};
enum InputMode {
@@ -49,7 +49,7 @@ struct App {
/// Current value of the input box
input: String,
/// Position of cursor in the editor area.
cursor_position: usize,
character_index: usize,
/// Current input mode
input_mode: InputMode,
/// History of recorded messages
@@ -62,34 +62,46 @@ impl App {
input: String::new(),
input_mode: InputMode::Normal,
messages: Vec::new(),
cursor_position: 0,
character_index: 0,
}
}
fn move_cursor_left(&mut self) {
let cursor_moved_left = self.cursor_position.saturating_sub(1);
self.cursor_position = self.clamp_cursor(cursor_moved_left);
let cursor_moved_left = self.character_index.saturating_sub(1);
self.character_index = self.clamp_cursor(cursor_moved_left);
}
fn move_cursor_right(&mut self) {
let cursor_moved_right = self.cursor_position.saturating_add(1);
self.cursor_position = self.clamp_cursor(cursor_moved_right);
let cursor_moved_right = self.character_index.saturating_add(1);
self.character_index = self.clamp_cursor(cursor_moved_right);
}
fn enter_char(&mut self, new_char: char) {
self.input.insert(self.cursor_position, new_char);
let index = self.byte_index();
self.input.insert(index, new_char);
self.move_cursor_right();
}
/// Returns the byte index based on the character position.
///
/// Since each character in a string can be contain multiple bytes, it's necessary to calculate
/// the byte index based on the index of the character.
fn byte_index(&mut self) -> usize {
self.input
.char_indices()
.map(|(i, _)| i)
.nth(self.character_index)
.unwrap_or(self.input.len())
}
fn delete_char(&mut self) {
let is_not_cursor_leftmost = self.cursor_position != 0;
let is_not_cursor_leftmost = self.character_index != 0;
if is_not_cursor_leftmost {
// Method "remove" is not used on the saved text for deleting the selected char.
// Reason: Using remove on String works on bytes instead of the chars.
// Using remove would require special care because of char boundaries.
let current_index = self.cursor_position;
let current_index = self.character_index;
let from_left_to_current_index = current_index - 1;
// Getting all characters before the selected character.
@@ -105,11 +117,11 @@ impl App {
}
fn clamp_cursor(&self, new_cursor_pos: usize) -> usize {
new_cursor_pos.clamp(0, self.input.len())
new_cursor_pos.clamp(0, self.input.chars().count())
}
fn reset_cursor(&mut self) {
self.cursor_position = 0;
self.character_index = 0;
}
fn submit_message(&mut self) {
@@ -226,7 +238,7 @@ fn ui(f: &mut Frame, app: &App) {
InputMode::Normal => Style::default(),
InputMode::Editing => Style::default().fg(Color::Yellow),
})
.block(Block::default().borders(Borders::ALL).title("Input"));
.block(Block::bordered().title("Input"));
f.render_widget(input, input_area);
match app.input_mode {
InputMode::Normal =>
@@ -240,7 +252,7 @@ fn ui(f: &mut Frame, app: &App) {
f.set_cursor(
// Draw the cursor at the current position in the input field.
// This position is can be controlled via the left and right arrow key
input_area.x + app.cursor_position as u16 + 1,
input_area.x + app.character_index as u16 + 1,
// Move one line down, from the border to the input line
input_area.y + 1,
);
@@ -256,7 +268,6 @@ fn ui(f: &mut Frame, app: &App) {
ListItem::new(content)
})
.collect();
let messages =
List::new(messages).block(Block::default().borders(Borders::ALL).title("Messages"));
let messages = List::new(messages).block(Block::bordered().title("Messages"));
f.render_widget(messages, messages_area);
}

View File

@@ -10,8 +10,8 @@ use crossterm::{
cursor::{Hide, MoveTo, Show},
execute, queue,
style::{
Attribute as CAttribute, Attributes as CAttributes, Color as CColor, ContentStyle, Print,
SetAttribute, SetBackgroundColor, SetForegroundColor,
Attribute as CAttribute, Attributes as CAttributes, Color as CColor, Colors, ContentStyle,
Print, SetAttribute, SetBackgroundColor, SetColors, SetForegroundColor,
},
terminal::{self, Clear},
};
@@ -145,14 +145,12 @@ where
diff.queue(&mut self.writer)?;
modifier = cell.modifier;
}
if cell.fg != fg {
let color = CColor::from(cell.fg);
queue!(self.writer, SetForegroundColor(color))?;
if cell.fg != fg || cell.bg != bg {
queue!(
self.writer,
SetColors(Colors::new(cell.fg.into(), cell.bg.into()))
)?;
fg = cell.fg;
}
if cell.bg != bg {
let color = CColor::from(cell.bg);
queue!(self.writer, SetBackgroundColor(color))?;
bg = cell.bg;
}
#[cfg(feature = "underline-color")]
@@ -228,7 +226,7 @@ where
Ok(Rect::new(0, 0, width, height))
}
fn window_size(&mut self) -> Result<WindowSize, io::Error> {
fn window_size(&mut self) -> io::Result<WindowSize> {
let crossterm::terminal::WindowSize {
columns,
rows,

View File

@@ -198,7 +198,7 @@ where
Ok(Rect::new(0, 0, terminal.0, terminal.1))
}
fn window_size(&mut self) -> Result<WindowSize, io::Error> {
fn window_size(&mut self) -> io::Result<WindowSize> {
Ok(WindowSize {
columns_rows: termion::terminal_size()?.into(),
pixels: termion::terminal_size_pixels()?.into(),

View File

@@ -111,7 +111,7 @@ impl TermwizBackend {
}
impl Backend for TermwizBackend {
fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error>
fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
where
I: Iterator<Item = (u16, u16, &'a Cell)>,
{
@@ -181,13 +181,13 @@ impl Backend for TermwizBackend {
Ok(())
}
fn hide_cursor(&mut self) -> Result<(), io::Error> {
fn hide_cursor(&mut self) -> io::Result<()> {
self.buffered_terminal
.add_change(Change::CursorVisibility(CursorVisibility::Hidden));
Ok(())
}
fn show_cursor(&mut self) -> Result<(), io::Error> {
fn show_cursor(&mut self) -> io::Result<()> {
self.buffered_terminal
.add_change(Change::CursorVisibility(CursorVisibility::Visible));
Ok(())
@@ -207,18 +207,18 @@ impl Backend for TermwizBackend {
Ok(())
}
fn clear(&mut self) -> Result<(), io::Error> {
fn clear(&mut self) -> io::Result<()> {
self.buffered_terminal
.add_change(Change::ClearScreen(termwiz::color::ColorAttribute::Default));
Ok(())
}
fn size(&self) -> Result<Rect, io::Error> {
fn size(&self) -> io::Result<Rect> {
let (cols, rows) = self.buffered_terminal.dimensions();
Ok(Rect::new(0, 0, u16_max(cols), u16_max(rows)))
}
fn window_size(&mut self) -> Result<WindowSize, io::Error> {
fn window_size(&mut self) -> io::Result<WindowSize> {
let ScreenSize {
cols,
rows,
@@ -241,7 +241,7 @@ impl Backend for TermwizBackend {
})
}
fn flush(&mut self) -> Result<(), io::Error> {
fn flush(&mut self) -> io::Result<()> {
self.buffered_terminal
.flush()
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
@@ -271,7 +271,7 @@ impl From<CellAttributes> for Style {
style.fg = Some(value.foreground().into());
style.bg = Some(value.background().into());
#[cfg(feature = "underline_color")]
#[cfg(feature = "underline-color")]
{
style.underline_color = Some(value.underline_color().into());
}
@@ -407,7 +407,6 @@ fn u16_max(i: usize) -> u16 {
#[cfg(test)]
mod tests {
use super::*;
use crate::style::Stylize;
mod into_color {
use Color as C;
@@ -576,11 +575,19 @@ mod tests {
#[test]
fn from_cell_attribute_for_style() {
use crate::style::Stylize;
#[cfg(feature = "underline-color")]
const STYLE: Style = Style::new()
.underline_color(Color::Reset)
.fg(Color::Reset)
.bg(Color::Reset);
#[cfg(not(feature = "underline-color"))]
const STYLE: Style = Style::new().fg(Color::Reset).bg(Color::Reset);
// default
assert_eq!(
Style::from(CellAttributes::default()),
Style::new().fg(Color::Reset).bg(Color::Reset)
);
assert_eq!(Style::from(CellAttributes::default()), STYLE);
// foreground color
assert_eq!(
Style::from(
@@ -588,7 +595,7 @@ mod tests {
.set_foreground(ColorAttribute::PaletteIndex(31))
.to_owned()
),
Style::new().fg(Color::Indexed(31)).bg(Color::Reset)
STYLE.fg(Color::Indexed(31))
);
// background color
assert_eq!(
@@ -597,21 +604,7 @@ mod tests {
.set_background(ColorAttribute::PaletteIndex(31))
.to_owned()
),
Style::new().fg(Color::Reset).bg(Color::Indexed(31))
);
// underline color
#[cfg(feature = "underline_color")]
assert_eq!(
Style::from(
CellAttributes::default()
.set_underline_color(AnsiColor::Red)
.set
.to_owned()
),
Style::new()
.fg(Color::Reset)
.bg(Color::Reset)
.underline_color(Color::Red)
STYLE.bg(Color::Indexed(31))
);
// underlined
assert_eq!(
@@ -620,12 +613,12 @@ mod tests {
.set_underline(Underline::Single)
.to_owned()
),
Style::new().fg(Color::Reset).bg(Color::Reset).underlined()
STYLE.underlined()
);
// blink
assert_eq!(
Style::from(CellAttributes::default().set_blink(Blink::Slow).to_owned()),
Style::new().fg(Color::Reset).bg(Color::Reset).slow_blink()
STYLE.slow_blink()
);
// intensity
assert_eq!(
@@ -634,27 +627,38 @@ mod tests {
.set_intensity(Intensity::Bold)
.to_owned()
),
Style::new().fg(Color::Reset).bg(Color::Reset).bold()
STYLE.bold()
);
// italic
assert_eq!(
Style::from(CellAttributes::default().set_italic(true).to_owned()),
Style::new().fg(Color::Reset).bg(Color::Reset).italic()
STYLE.italic()
);
// reversed
assert_eq!(
Style::from(CellAttributes::default().set_reverse(true).to_owned()),
Style::new().fg(Color::Reset).bg(Color::Reset).reversed()
STYLE.reversed()
);
// strikethrough
assert_eq!(
Style::from(CellAttributes::default().set_strikethrough(true).to_owned()),
Style::new().fg(Color::Reset).bg(Color::Reset).crossed_out()
STYLE.crossed_out()
);
// hidden
assert_eq!(
Style::from(CellAttributes::default().set_invisible(true).to_owned()),
Style::new().fg(Color::Reset).bg(Color::Reset).hidden()
STYLE.hidden()
);
// underline color
#[cfg(feature = "underline-color")]
assert_eq!(
Style::from(
CellAttributes::default()
.set_underline_color(AnsiColor::Red)
.to_owned()
),
STYLE.underline_color(Color::Indexed(9))
);
}
}

View File

@@ -2,21 +2,19 @@
//! It is used in the integration tests to verify the correctness of the library.
use std::{
fmt::{Display, Write},
fmt::{self, Write},
io,
};
use unicode_width::UnicodeWidthStr;
use crate::{
assert_buffer_eq,
backend::{Backend, ClearType, WindowSize},
buffer::{Buffer, Cell},
layout::{Rect, Size},
};
/// A [`Backend`] implementation used for integration testing that that renders to an in memory
/// buffer.
/// A [`Backend`] implementation used for integration testing that renders to an memory buffer.
///
/// Note: that although many of the integration and unit tests in ratatui are written using this
/// backend, it is preferable to write unit tests for widgets directly against the buffer rather
@@ -30,7 +28,7 @@ use crate::{
///
/// let mut backend = TestBackend::new(10, 2);
/// backend.clear()?;
/// backend.assert_buffer(&Buffer::with_lines(vec![" "; 2]));
/// backend.assert_buffer_lines([" "; 2]);
/// # std::io::Result::Ok(())
/// ```
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
@@ -97,24 +95,46 @@ impl TestBackend {
}
/// Asserts that the `TestBackend`'s buffer is equal to the expected buffer.
/// If the buffers are not equal, a panic occurs with a detailed error message
/// showing the differences between the expected and actual buffers.
///
/// This is a shortcut for `assert_eq!(self.buffer(), &expected)`.
///
/// # Panics
/// When they are not equal, a panic occurs with a detailed error message showing the
/// differences between the expected and actual buffers.
#[allow(deprecated)]
#[track_caller]
pub fn assert_buffer(&self, expected: &Buffer) {
assert_buffer_eq!(&self.buffer, expected);
// TODO: use assert_eq!()
crate::assert_buffer_eq!(&self.buffer, expected);
}
/// Asserts that the `TestBackend`'s buffer is equal to the expected lines.
///
/// This is a shortcut for `assert_eq!(self.buffer(), &Buffer::with_lines(expected))`.
///
/// # Panics
/// When they are not equal, a panic occurs with a detailed error message showing the
/// differences between the expected and actual buffers.
#[track_caller]
pub fn assert_buffer_lines<'line, Lines>(&self, expected: Lines)
where
Lines: IntoIterator,
Lines::Item: Into<crate::text::Line<'line>>,
{
self.assert_buffer(&Buffer::with_lines(expected));
}
}
impl Display for TestBackend {
impl fmt::Display for TestBackend {
/// Formats the `TestBackend` for display by calling the `buffer_view` function
/// on its internal buffer.
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", buffer_view(&self.buffer))
}
}
impl Backend for TestBackend {
fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error>
fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
where
I: Iterator<Item = (u16, u16, &'a Cell)>,
{
@@ -125,26 +145,26 @@ impl Backend for TestBackend {
Ok(())
}
fn hide_cursor(&mut self) -> Result<(), io::Error> {
fn hide_cursor(&mut self) -> io::Result<()> {
self.cursor = false;
Ok(())
}
fn show_cursor(&mut self) -> Result<(), io::Error> {
fn show_cursor(&mut self) -> io::Result<()> {
self.cursor = true;
Ok(())
}
fn get_cursor(&mut self) -> Result<(u16, u16), io::Error> {
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
Ok(self.pos)
}
fn set_cursor(&mut self, x: u16, y: u16) -> Result<(), io::Error> {
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
self.pos = (x, y);
Ok(())
}
fn clear(&mut self) -> Result<(), io::Error> {
fn clear(&mut self) -> io::Result<()> {
self.buffer.reset();
Ok(())
}
@@ -214,11 +234,11 @@ impl Backend for TestBackend {
Ok(())
}
fn size(&self) -> Result<Rect, io::Error> {
fn size(&self) -> io::Result<Rect> {
Ok(Rect::new(0, 0, self.width, self.height))
}
fn window_size(&mut self) -> Result<WindowSize, io::Error> {
fn window_size(&mut self) -> io::Result<WindowSize> {
// Some arbitrary window pixel size, probably doesn't need much testing.
static WINDOW_PIXEL_SIZE: Size = Size {
width: 640,
@@ -230,7 +250,7 @@ impl Backend for TestBackend {
})
}
fn flush(&mut self) -> Result<(), io::Error> {
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
@@ -246,7 +266,7 @@ mod tests {
TestBackend {
width: 10,
height: 2,
buffer: Buffer::with_lines(vec![" "; 2]),
buffer: Buffer::with_lines([" "; 2]),
cursor: false,
pos: (0, 0),
}
@@ -254,14 +274,14 @@ mod tests {
}
#[test]
fn test_buffer_view() {
let buffer = Buffer::with_lines(vec!["aaaa"; 2]);
let buffer = Buffer::with_lines(["aaaa"; 2]);
assert_eq!(buffer_view(&buffer), "\"aaaa\"\n\"aaaa\"\n");
}
#[test]
fn buffer_view_with_overwrites() {
let multi_byte_char = "👨‍👩‍👧‍👦"; // renders 8 wide
let buffer = Buffer::with_lines(vec![multi_byte_char]);
let buffer = Buffer::with_lines([multi_byte_char]);
assert_eq!(
buffer_view(&buffer),
format!(
@@ -274,29 +294,27 @@ mod tests {
#[test]
fn buffer() {
let backend = TestBackend::new(10, 2);
assert_eq!(backend.buffer(), &Buffer::with_lines(vec![" "; 2]));
backend.assert_buffer_lines([" "; 2]);
}
#[test]
fn resize() {
let mut backend = TestBackend::new(10, 2);
backend.resize(5, 5);
assert_eq!(backend.buffer(), &Buffer::with_lines(vec![" "; 5]));
backend.assert_buffer_lines([" "; 5]);
}
#[test]
fn assert_buffer() {
let backend = TestBackend::new(10, 2);
let buffer = Buffer::with_lines(vec![" "; 2]);
backend.assert_buffer(&buffer);
backend.assert_buffer_lines([" "; 2]);
}
#[test]
#[should_panic = "buffer contents not equal"]
fn assert_buffer_panics() {
let backend = TestBackend::new(10, 2);
let buffer = Buffer::with_lines(vec!["aaaaaaaaaa"; 2]);
backend.assert_buffer(&buffer);
backend.assert_buffer_lines(["aaaaaaaaaa"; 2]);
}
#[test]
@@ -312,7 +330,7 @@ mod tests {
cell.set_symbol("a");
backend.draw([(0, 0, &cell)].into_iter()).unwrap();
backend.draw([(0, 1, &cell)].into_iter()).unwrap();
backend.assert_buffer(&Buffer::with_lines(vec!["a "; 2]));
backend.assert_buffer_lines(["a "; 2]);
}
#[test]
@@ -344,24 +362,19 @@ mod tests {
#[test]
fn clear() {
let mut backend = TestBackend::new(10, 4);
let mut backend = TestBackend::new(4, 2);
let mut cell = Cell::default();
cell.set_symbol("a");
backend.draw([(0, 0, &cell)].into_iter()).unwrap();
backend.draw([(0, 1, &cell)].into_iter()).unwrap();
backend.clear().unwrap();
backend.assert_buffer(&Buffer::with_lines(vec![
" ",
" ",
" ",
" ",
]));
backend.assert_buffer_lines([" ", " "]);
}
#[test]
fn clear_region_all() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines(vec![
backend.buffer = Buffer::with_lines([
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
@@ -370,19 +383,19 @@ mod tests {
]);
backend.clear_region(ClearType::All).unwrap();
backend.assert_buffer(&Buffer::with_lines(vec![
backend.assert_buffer_lines([
" ",
" ",
" ",
" ",
" ",
]));
]);
}
#[test]
fn clear_region_after_cursor() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines(vec![
backend.buffer = Buffer::with_lines([
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
@@ -392,19 +405,19 @@ mod tests {
backend.set_cursor(3, 2).unwrap();
backend.clear_region(ClearType::AfterCursor).unwrap();
backend.assert_buffer(&Buffer::with_lines(vec![
backend.assert_buffer_lines([
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaa ",
" ",
" ",
]));
]);
}
#[test]
fn clear_region_before_cursor() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines(vec![
backend.buffer = Buffer::with_lines([
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
@@ -414,19 +427,19 @@ mod tests {
backend.set_cursor(5, 3).unwrap();
backend.clear_region(ClearType::BeforeCursor).unwrap();
backend.assert_buffer(&Buffer::with_lines(vec![
backend.assert_buffer_lines([
" ",
" ",
" ",
" aaaaa",
"aaaaaaaaaa",
]));
]);
}
#[test]
fn clear_region_current_line() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines(vec![
backend.buffer = Buffer::with_lines([
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
@@ -436,19 +449,19 @@ mod tests {
backend.set_cursor(3, 1).unwrap();
backend.clear_region(ClearType::CurrentLine).unwrap();
backend.assert_buffer(&Buffer::with_lines(vec![
backend.assert_buffer_lines([
"aaaaaaaaaa",
" ",
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
]));
]);
}
#[test]
fn clear_region_until_new_line() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines(vec![
backend.buffer = Buffer::with_lines([
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
@@ -458,19 +471,19 @@ mod tests {
backend.set_cursor(3, 0).unwrap();
backend.clear_region(ClearType::UntilNewLine).unwrap();
backend.assert_buffer(&Buffer::with_lines(vec![
backend.assert_buffer_lines([
"aaa ",
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
]));
]);
}
#[test]
fn append_lines_not_at_last_line() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines(vec![
backend.buffer = Buffer::with_lines([
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
@@ -496,19 +509,19 @@ mod tests {
assert_eq!(backend.get_cursor().unwrap(), (4, 4));
// As such the buffer should remain unchanged
backend.assert_buffer(&Buffer::with_lines(vec![
backend.assert_buffer_lines([
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
"dddddddddd",
"eeeeeeeeee",
]));
]);
}
#[test]
fn append_lines_at_last_line() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines(vec![
backend.buffer = Buffer::with_lines([
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
@@ -522,7 +535,7 @@ mod tests {
backend.append_lines(1).unwrap();
backend.buffer = Buffer::with_lines(vec![
backend.buffer = Buffer::with_lines([
"bbbbbbbbbb",
"cccccccccc",
"dddddddddd",
@@ -538,7 +551,7 @@ mod tests {
#[test]
fn append_multiple_lines_not_at_last_line() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines(vec![
backend.buffer = Buffer::with_lines([
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
@@ -555,19 +568,19 @@ mod tests {
assert_eq!(backend.get_cursor().unwrap(), (1, 4));
// As such the buffer should remain unchanged
backend.assert_buffer(&Buffer::with_lines(vec![
backend.assert_buffer_lines([
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
"dddddddddd",
"eeeeeeeeee",
]));
]);
}
#[test]
fn append_multiple_lines_past_last_line() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines(vec![
backend.buffer = Buffer::with_lines([
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
@@ -580,19 +593,19 @@ mod tests {
backend.append_lines(3).unwrap();
assert_eq!(backend.get_cursor().unwrap(), (1, 4));
backend.assert_buffer(&Buffer::with_lines(vec![
backend.assert_buffer_lines([
"cccccccccc",
"dddddddddd",
"eeeeeeeeee",
" ",
" ",
]));
]);
}
#[test]
fn append_multiple_lines_where_cursor_at_end_appends_height_lines() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines(vec![
backend.buffer = Buffer::with_lines([
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
@@ -605,19 +618,19 @@ mod tests {
backend.append_lines(5).unwrap();
assert_eq!(backend.get_cursor().unwrap(), (1, 4));
backend.assert_buffer(&Buffer::with_lines(vec![
backend.assert_buffer_lines([
" ",
" ",
" ",
" ",
" ",
]));
]);
}
#[test]
fn append_multiple_lines_where_cursor_appends_height_lines() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines(vec![
backend.buffer = Buffer::with_lines([
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
@@ -630,13 +643,13 @@ mod tests {
backend.append_lines(5).unwrap();
assert_eq!(backend.get_cursor().unwrap(), (1, 4));
backend.assert_buffer(&Buffer::with_lines(vec![
backend.assert_buffer_lines([
"bbbbbbbbbb",
"cccccccccc",
"dddddddddd",
"eeeeeeeeee",
" ",
]));
]);
}
#[test]

View File

@@ -1,57 +1,44 @@
/// Assert that two buffers are equal by comparing their areas and content.
///
/// On panic, displays the areas or the content and a diff of the contents.
/// # Panics
/// When the buffers differ this method panics and displays the differences similar to
/// `assert_eq!()`.
#[deprecated = "use assert_eq!(&actual, &expected)"]
#[macro_export]
macro_rules! assert_buffer_eq {
($actual_expr:expr, $expected_expr:expr) => {
match (&$actual_expr, &$expected_expr) {
(actual, expected) => {
if actual.area != expected.area {
panic!(
indoc::indoc!(
"
buffer areas not equal
expected: {:?}
actual: {:?}"
),
expected, actual
);
}
let diff = expected.diff(&actual);
if !diff.is_empty() {
let nice_diff = diff
.iter()
.enumerate()
.map(|(i, (x, y, cell))| {
let expected_cell = expected.get(*x, *y);
indoc::formatdoc! {"
{i}: at ({x}, {y})
expected: {expected_cell:?}
actual: {cell:?}
"}
})
.collect::<Vec<String>>()
.join("\n");
panic!(
indoc::indoc!(
"
buffer contents not equal
expected: {:?}
actual: {:?}
diff:
{}"
),
expected, actual, nice_diff
);
}
assert!(
actual.area == expected.area,
"buffer areas not equal\nexpected: {expected:?}\nactual: {actual:?}",
);
let nice_diff = expected
.diff(actual)
.into_iter()
.enumerate()
.map(|(i, (x, y, cell))| {
let expected_cell = expected.get(x, y);
format!("{i}: at ({x}, {y})\n expected: {expected_cell:?}\n actual: {cell:?}")
})
.collect::<Vec<String>>()
.join("\n");
assert!(
nice_diff.is_empty(),
"buffer contents not equal\nexpected: {expected:?}\nactual: {actual:?}\ndiff:\n{nice_diff}",
);
// shouldn't get here, but this guards against future behavior
// that changes equality but not area or content
assert_eq!(actual, expected, "buffers not equal");
assert_eq!(
actual, expected,
"buffers are not equal in an unexpected way. Please open an issue about this."
);
}
}
};
}
#[allow(deprecated)]
#[cfg(test)]
mod tests {
use crate::prelude::*;

View File

@@ -1,7 +1,4 @@
use std::{
cmp::min,
fmt::{Debug, Formatter, Result},
};
use std::fmt;
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
@@ -55,22 +52,21 @@ pub struct Buffer {
impl Buffer {
/// Returns a Buffer with all cells set to the default one
#[must_use]
pub fn empty(area: Rect) -> Self {
let cell = Cell::default();
Self::filled(area, &cell)
Self::filled(area, &Cell::default())
}
/// Returns a Buffer with all cells initialized with the attributes of the given Cell
#[must_use]
pub fn filled(area: Rect, cell: &Cell) -> Self {
let size = area.area() as usize;
let mut content = Vec::with_capacity(size);
for _ in 0..size {
content.push(cell.clone());
}
let content = vec![cell.clone(); size];
Self { area, content }
}
/// Returns a Buffer containing the given lines
#[must_use]
pub fn with_lines<'a, Iter>(lines: Iter) -> Self
where
Iter: IntoIterator,
@@ -97,12 +93,14 @@ impl Buffer {
}
/// Returns a reference to Cell at the given coordinates
#[track_caller]
pub fn get(&self, x: u16, y: u16) -> &Cell {
let i = self.index_of(x, y);
&self.content[i]
}
/// Returns a mutable reference to Cell at the given coordinates
#[track_caller]
pub fn get_mut(&mut self, x: u16, y: u16) -> &mut Cell {
let i = self.index_of(x, y);
&mut self.content[i]
@@ -134,6 +132,7 @@ impl Buffer {
/// // starts at (200, 100).
/// buffer.index_of(0, 0); // Panics
/// ```
#[track_caller]
pub fn index_of(&self, x: u16, y: u16) -> usize {
debug_assert!(
x >= self.area.left()
@@ -184,65 +183,56 @@ impl Buffer {
}
/// Print a string, starting at the position (x, y)
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
pub fn set_string<T, S>(&mut self, x: u16, y: u16, string: T, style: S)
where
T: AsRef<str>,
S: Into<Style>,
{
self.set_stringn(x, y, string, usize::MAX, style.into());
self.set_stringn(x, y, string, usize::MAX, style);
}
/// Print at most the first n characters of a string if enough space is available
/// until the end of the line
/// until the end of the line.
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
/// Use [`Buffer::set_string`] when the maximum amount of characters can be printed.
pub fn set_stringn<T, S>(
&mut self,
x: u16,
mut x: u16,
y: u16,
string: T,
width: usize,
max_width: usize,
style: S,
) -> (u16, u16)
where
T: AsRef<str>,
S: Into<Style>,
{
let max_width = max_width.try_into().unwrap_or(u16::MAX);
let mut remaining_width = self.area.right().saturating_sub(x).min(max_width);
let graphemes = UnicodeSegmentation::graphemes(string.as_ref(), true)
.map(|symbol| (symbol, symbol.width() as u16))
.filter(|(_symbol, width)| *width > 0)
.map_while(|(symbol, width)| {
remaining_width = remaining_width.checked_sub(width)?;
Some((symbol, width))
});
let style = style.into();
let mut index = self.index_of(x, y);
let mut x_offset = x as usize;
let graphemes = UnicodeSegmentation::graphemes(string.as_ref(), true);
let max_offset = min(self.area.right() as usize, width.saturating_add(x as usize));
for s in graphemes {
let width = s.width();
if width == 0 {
continue;
}
// `x_offset + width > max_offset` could be integer overflow on 32-bit machines if we
// change dimensions to usize or u32 and someone resizes the terminal to 1x2^32.
if width > max_offset.saturating_sub(x_offset) {
break;
}
self.content[index].set_symbol(s);
self.content[index].set_style(style);
for (symbol, width) in graphemes {
self.get_mut(x, y).set_symbol(symbol).set_style(style);
let next_symbol = x + width;
x += 1;
// Reset following cells if multi-width (they would be hidden by the grapheme),
for i in index + 1..index + width {
self.content[i].reset();
while x < next_symbol {
self.get_mut(x, y).reset();
x += 1;
}
index += width;
x_offset += width;
}
(x_offset as u16, y)
(x, y)
}
/// Print a line, starting at the position (x, y)
pub fn set_line(&mut self, x: u16, y: u16, line: &Line<'_>, width: u16) -> (u16, u16) {
let mut remaining_width = width;
pub fn set_line(&mut self, x: u16, y: u16, line: &Line<'_>, max_width: u16) -> (u16, u16) {
let mut remaining_width = max_width;
let mut x = x;
for span in line {
if remaining_width == 0 {
@@ -263,8 +253,8 @@ impl Buffer {
}
/// Print a span, starting at the position (x, y)
pub fn set_span(&mut self, x: u16, y: u16, span: &Span<'_>, width: u16) -> (u16, u16) {
self.set_stringn(x, y, span.content.as_ref(), width as usize, span.style)
pub fn set_span(&mut self, x: u16, y: u16, span: &Span<'_>, max_width: u16) -> (u16, u16) {
self.set_stringn(x, y, &span.content, max_width as usize, span.style)
}
/// Set the style of all cells in the given area.
@@ -303,8 +293,7 @@ impl Buffer {
/// Merge an other buffer into this one
pub fn merge(&mut self, other: &Self) {
let area = self.area.union(other.area);
let cell = Cell::default();
self.content.resize(area.area() as usize, cell.clone());
self.content.resize(area.area() as usize, Cell::default());
// Move original content to the appropriate space
let size = self.area.area() as usize;
@@ -314,7 +303,7 @@ impl Buffer {
let k = ((y - area.y) * area.width + x - area.x) as usize;
if i != k {
self.content[k] = self.content[i].clone();
self.content[i] = cell.clone();
self.content[i] = Cell::default();
}
}
@@ -383,7 +372,7 @@ impl Buffer {
}
}
impl Debug for Buffer {
impl fmt::Debug for Buffer {
/// Writes a debug representation of the buffer to the given formatter.
///
/// The format is like a pretty printed struct, with the following fields:
@@ -391,11 +380,14 @@ impl Debug for Buffer {
/// * `content`: displayed as a list of strings representing the content of the buffer
/// * `styles`: displayed as a list of: `{ x: 1, y: 2, fg: Color::Red, bg: Color::Blue,
/// modifier: Modifier::BOLD }` only showing a value when there is a change in style.
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
f.write_fmt(format_args!(
"Buffer {{\n area: {:?},\n content: [\n",
&self.area
))?;
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_fmt(format_args!("Buffer {{\n area: {:?}", &self.area))?;
if self.area.is_empty() {
return f.write_str("\n}");
}
f.write_str(",\n content: [\n")?;
let mut last_style = None;
let mut styles = vec![];
for (y, line) in self.content.chunks(self.area.width as usize).enumerate() {
@@ -426,12 +418,13 @@ impl Debug for Buffer {
}
}
}
f.write_str("\",")?;
if !overwritten.is_empty() {
f.write_fmt(format_args!(
"// hidden by multi-width symbols: {overwritten:?}"
" // hidden by multi-width symbols: {overwritten:?}"
))?;
}
f.write_str("\",\n")?;
f.write_str("\n")?;
}
f.write_str(" ],\n styles: [\n")?;
for s in styles {
@@ -459,7 +452,6 @@ mod tests {
use rstest::{fixture, rstest};
use super::*;
use crate::assert_buffer_eq;
fn cell(s: &str) -> Cell {
let mut cell = Cell::default();
@@ -468,10 +460,40 @@ mod tests {
}
#[test]
fn debug() {
let mut buf = Buffer::empty(Rect::new(0, 0, 12, 2));
buf.set_string(0, 0, "Hello World!", Style::default());
buf.set_string(
fn debug_empty_buffer() {
let buffer = Buffer::empty(Rect::ZERO);
let result = format!("{buffer:?}");
println!("{result}");
let expected = "Buffer {\n area: Rect { x: 0, y: 0, width: 0, height: 0 }\n}";
assert_eq!(result, expected);
}
#[cfg(feature = "underline-color")]
#[test]
fn debug_grapheme_override() {
let buffer = Buffer::with_lines(["a🦀b"]);
let result = format!("{buffer:?}");
println!("{result}");
let expected = indoc::indoc!(
r#"
Buffer {
area: Rect { x: 0, y: 0, width: 4, height: 1 },
content: [
"a🦀b", // hidden by multi-width symbols: [(2, " ")]
],
styles: [
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
]
}"#
);
assert_eq!(result, expected);
}
#[test]
fn debug_some_example() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 12, 2));
buffer.set_string(0, 0, "Hello World!", Style::default());
buffer.set_string(
0,
1,
"G'day World!",
@@ -480,42 +502,40 @@ mod tests {
.bg(Color::Yellow)
.add_modifier(Modifier::BOLD),
);
let result = format!("{buffer:?}");
println!("{result}");
#[cfg(feature = "underline-color")]
assert_eq!(
format!("{buf:?}"),
indoc::indoc!(
"
Buffer {
area: Rect { x: 0, y: 0, width: 12, height: 2 },
content: [
\"Hello World!\",
\"G'day World!\",
],
styles: [
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
x: 0, y: 1, fg: Green, bg: Yellow, underline: Reset, modifier: BOLD,
]
}"
)
let expected = indoc::indoc!(
r#"
Buffer {
area: Rect { x: 0, y: 0, width: 12, height: 2 },
content: [
"Hello World!",
"G'day World!",
],
styles: [
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
x: 0, y: 1, fg: Green, bg: Yellow, underline: Reset, modifier: BOLD,
]
}"#
);
#[cfg(not(feature = "underline-color"))]
assert_eq!(
format!("{buf:?}"),
indoc::indoc!(
"
Buffer {
area: Rect { x: 0, y: 0, width: 12, height: 2 },
content: [
\"Hello World!\",
\"G'day World!\",
],
styles: [
x: 0, y: 0, fg: Reset, bg: Reset, modifier: NONE,
x: 0, y: 1, fg: Green, bg: Yellow, modifier: BOLD,
]
}"
)
let expected = indoc::indoc!(
r#"
Buffer {
area: Rect { x: 0, y: 0, width: 12, height: 2 },
content: [
"Hello World!",
"G'day World!",
],
styles: [
x: 0, y: 0, fg: Reset, bg: Reset, modifier: NONE,
x: 0, y: 1, fg: Green, bg: Yellow, modifier: BOLD,
]
}"#
);
assert_eq!(result, expected);
}
#[test]
@@ -559,27 +579,27 @@ mod tests {
// Zero-width
buffer.set_stringn(0, 0, "aaa", 0, Style::default());
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" "]));
assert_eq!(buffer, Buffer::with_lines([" "]));
buffer.set_string(0, 0, "aaa", Style::default());
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["aaa "]));
assert_eq!(buffer, Buffer::with_lines(["aaa "]));
// Width limit:
buffer.set_stringn(0, 0, "bbbbbbbbbbbbbb", 4, Style::default());
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["bbbb "]));
assert_eq!(buffer, Buffer::with_lines(["bbbb "]));
buffer.set_string(0, 0, "12345", Style::default());
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["12345"]));
assert_eq!(buffer, Buffer::with_lines(["12345"]));
// Width truncation:
buffer.set_string(0, 0, "123456", Style::default());
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["12345"]));
assert_eq!(buffer, Buffer::with_lines(["12345"]));
// multi-line
buffer = Buffer::empty(Rect::new(0, 0, 5, 2));
buffer.set_string(0, 0, "12345", Style::default());
buffer.set_string(0, 1, "67890", Style::default());
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["12345", "67890"]));
assert_eq!(buffer, Buffer::with_lines(["12345", "67890"]));
}
#[test]
@@ -590,7 +610,7 @@ mod tests {
// multi-width overwrite
buffer.set_string(0, 0, "aaaaa", Style::default());
buffer.set_string(0, 0, "称号", Style::default());
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["称号a"]));
assert_eq!(buffer, Buffer::with_lines(["称号a"]));
}
#[test]
@@ -601,12 +621,12 @@ mod tests {
// Leading grapheme with zero width
let s = "\u{1}a";
buffer.set_stringn(0, 0, s, 1, Style::default());
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["a"]));
assert_eq!(buffer, Buffer::with_lines(["a"]));
// Trailing grapheme with zero with
let s = "a\u{1}";
buffer.set_stringn(0, 0, s, 1, Style::default());
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["a"]));
assert_eq!(buffer, Buffer::with_lines(["a"]));
}
#[test]
@@ -614,11 +634,11 @@ mod tests {
let area = Rect::new(0, 0, 5, 1);
let mut buffer = Buffer::empty(area);
buffer.set_string(0, 0, "コン", Style::default());
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["コン "]));
assert_eq!(buffer, Buffer::with_lines(["コン "]));
// Only 1 space left.
buffer.set_string(0, 0, "コンピ", Style::default());
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["コン "]));
assert_eq!(buffer, Buffer::with_lines(["コン "]));
}
#[fixture]
@@ -643,7 +663,7 @@ mod tests {
// set_line
let mut expected_buffer = Buffer::empty(small_one_line_buffer.area);
expected_buffer.set_string(0, 0, expected, Style::default());
assert_buffer_eq!(small_one_line_buffer, expected_buffer);
assert_eq!(small_one_line_buffer, expected_buffer);
}
#[rstest]
@@ -684,28 +704,39 @@ mod tests {
#[test]
fn set_style() {
let mut buffer = Buffer::with_lines(vec!["aaaaa", "bbbbb", "ccccc"]);
let mut buffer = Buffer::with_lines(["aaaaa", "bbbbb", "ccccc"]);
buffer.set_style(Rect::new(0, 1, 5, 1), Style::new().red());
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec!["aaaaa".into(), "bbbbb".red(), "ccccc".into(),])
);
#[rustfmt::skip]
let expected = Buffer::with_lines([
"aaaaa".into(),
"bbbbb".red(),
"ccccc".into(),
]);
assert_eq!(buffer, expected);
}
#[test]
fn set_style_does_not_panic_when_out_of_area() {
let mut buffer = Buffer::with_lines(vec!["aaaaa", "bbbbb", "ccccc"]);
let mut buffer = Buffer::with_lines(["aaaaa", "bbbbb", "ccccc"]);
buffer.set_style(Rect::new(0, 1, 10, 3), Style::new().red());
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec!["aaaaa".into(), "bbbbb".red(), "ccccc".red(),])
);
#[rustfmt::skip]
let expected = Buffer::with_lines([
"aaaaa".into(),
"bbbbb".red(),
"ccccc".red(),
]);
assert_eq!(buffer, expected);
}
#[test]
fn with_lines() {
let buffer =
Buffer::with_lines(vec!["┌────────┐", "│コンピュ│", "│ーa 上で│", "└────────┘"]);
#[rustfmt::skip]
let buffer = Buffer::with_lines([
"┌────────┐",
"│コンピュ│",
"│ーa 上で│",
"└────────┘",
]);
assert_eq!(buffer.area.x, 0);
assert_eq!(buffer.area.y, 0);
assert_eq!(buffer.area.width, 10);
@@ -741,14 +772,14 @@ mod tests {
#[test]
fn diff_single_width() {
let prev = Buffer::with_lines(vec![
let prev = Buffer::with_lines([
" ",
"┌Title─┐ ",
"│ │ ",
"│ │ ",
"└──────┘ ",
]);
let next = Buffer::with_lines(vec![
let next = Buffer::with_lines([
" ",
"┌TITLE─┐ ",
"│ │ ",
@@ -770,11 +801,11 @@ mod tests {
#[test]
#[rustfmt::skip]
fn diff_multi_width() {
let prev = Buffer::with_lines(vec![
let prev = Buffer::with_lines([
"┌Title─┐ ",
"└──────┘ ",
]);
let next = Buffer::with_lines(vec![
let next = Buffer::with_lines([
"┌称号──┐ ",
"└──────┘ ",
]);
@@ -793,8 +824,8 @@ mod tests {
#[test]
fn diff_multi_width_offset() {
let prev = Buffer::with_lines(vec!["┌称号──┐"]);
let next = Buffer::with_lines(vec!["┌─称号─┐"]);
let prev = Buffer::with_lines(["┌称号──┐"]);
let next = Buffer::with_lines(["┌─称号─┐"]);
let diff = prev.diff(&next);
assert_eq!(
@@ -805,8 +836,8 @@ mod tests {
#[test]
fn diff_skip() {
let prev = Buffer::with_lines(vec!["123"]);
let mut next = Buffer::with_lines(vec!["456"]);
let prev = Buffer::with_lines(["123"]);
let mut next = Buffer::with_lines(["456"]);
for i in 1..3 {
next.content[i].set_skip(true);
}
@@ -836,7 +867,7 @@ mod tests {
Cell::default().set_symbol("2"),
);
one.merge(&two);
assert_buffer_eq!(one, Buffer::with_lines(vec!["11", "11", "22", "22"]));
assert_eq!(one, Buffer::with_lines(["11", "11", "22", "22"]));
}
#[test]
@@ -860,10 +891,7 @@ mod tests {
Cell::default().set_symbol("2"),
);
one.merge(&two);
assert_buffer_eq!(
one,
Buffer::with_lines(vec!["22 ", "22 ", " 11", " 11"])
);
assert_eq!(one, Buffer::with_lines(["22 ", "22 ", " 11", " 11"]));
}
#[test]
@@ -887,14 +915,14 @@ mod tests {
Cell::default().set_symbol("2"),
);
one.merge(&two);
let mut merged = Buffer::with_lines(vec!["222 ", "222 ", "2221", "2221"]);
let mut merged = Buffer::with_lines(["222 ", "222 ", "2221", "2221"]);
merged.area = Rect {
x: 1,
y: 1,
width: 4,
height: 4,
};
assert_buffer_eq!(one, merged);
assert_eq!(one, merged);
}
#[test]
@@ -953,6 +981,6 @@ mod tests {
let mut buf = Buffer::empty(Rect::new(0, 0, 3, 2));
buf.set_string(0, 0, "foo", Style::new().red());
buf.set_string(0, 1, "bar", Style::new().blue());
assert_eq!(buf, Buffer::with_lines(vec!["foo".red(), "bar".blue()]));
assert_eq!(buf, Buffer::with_lines(["foo".red(), "bar".blue()]));
}
}

View File

@@ -1,5 +1,3 @@
use std::fmt::Debug;
use compact_str::CompactString;
use crate::prelude::*;

View File

@@ -1,4 +1,4 @@
use std::fmt::{self, Display};
use std::fmt;
use itertools::Itertools;
use strum::EnumIs;
@@ -362,7 +362,7 @@ impl Default for Constraint {
}
}
impl Display for Constraint {
impl fmt::Display for Constraint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Percentage(p) => write!(f, "Percentage({p})"),

View File

@@ -448,7 +448,7 @@ impl Layout {
/// # }
pub fn areas<const N: usize>(&self, area: Rect) -> [Rect; N] {
let (areas, _) = self.split_with_spacers(area);
areas.to_vec().try_into().expect("invalid number of rects")
areas.as_ref().try_into().expect("invalid number of rects")
}
/// Split the rect into a number of sub-rects according to the given [`Layout`] and return just
@@ -482,7 +482,7 @@ impl Layout {
pub fn spacers<const N: usize>(&self, area: Rect) -> [Rect; N] {
let (_, spacers) = self.split_with_spacers(area);
spacers
.to_vec()
.as_ref()
.try_into()
.expect("invalid number of rects")
}
@@ -1334,7 +1334,6 @@ mod tests {
use rstest::rstest;
use crate::{
assert_buffer_eq,
layout::flex::Flex,
prelude::{Constraint::*, *},
widgets::Paragraph,
@@ -1361,8 +1360,7 @@ mod tests {
let s = c.to_string().repeat(area.width as usize);
Paragraph::new(s).render(layout[i], &mut buffer);
}
let expected = Buffer::with_lines(vec![expected]);
assert_buffer_eq!(buffer, expected);
assert_eq!(buffer, Buffer::with_lines([expected]));
}
#[rstest]

View File

@@ -1,4 +1,4 @@
use std::fmt::{self, Display};
use std::fmt;
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
pub struct Margin {
@@ -15,7 +15,7 @@ impl Margin {
}
}
impl Display for Margin {
impl fmt::Display for Margin {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}x{}", self.horizontal, self.vertical)
}

View File

@@ -47,6 +47,14 @@ impl fmt::Display for Rect {
}
impl Rect {
/// A zero sized Rect at position 0,0
pub const ZERO: Self = Self {
x: 0,
y: 0,
width: 0,
height: 0,
};
/// Creates a new `Rect`, with width and height limited to keep the area under max `u16`. If
/// clipped, aspect ratio will be preserved.
pub fn new(x: u16, y: u16, width: u16, height: u16) -> Self {
@@ -111,13 +119,14 @@ impl Rect {
/// Returns a new `Rect` inside the current one, with the given margin on each side.
///
/// If the margin is larger than the `Rect`, the returned `Rect` will have no area.
#[allow(clippy::trivially_copy_pass_by_ref)] // See PR #1008
#[must_use = "method returns the modified value"]
pub fn inner(self, margin: &Margin) -> Self {
pub const fn inner(self, margin: &Margin) -> Self {
let doubled_margin_horizontal = margin.horizontal.saturating_mul(2);
let doubled_margin_vertical = margin.vertical.saturating_mul(2);
if self.width < doubled_margin_horizontal || self.height < doubled_margin_vertical {
Self::default()
Self::ZERO
} else {
Self {
x: self.x.saturating_add(margin.horizontal),
@@ -313,6 +322,18 @@ impl Rect {
height: self.height,
}
}
/// indents the x value of the `Rect` by a given `offset`
///
/// This is pub(crate) for now as we need to stabilize the naming / design of this API.
#[must_use]
pub(crate) const fn indent_x(self, offset: u16) -> Self {
Self {
x: self.x.saturating_add(offset),
width: self.width.saturating_sub(offset),
..self
}
}
}
impl From<(Position, Size)> for Rect {

View File

@@ -139,8 +139,7 @@
//!
//! fn ui(frame: &mut Frame) {
//! frame.render_widget(
//! Paragraph::new("Hello World!")
//! .block(Block::default().title("Greeting").borders(Borders::ALL)),
//! Paragraph::new("Hello World!").block(Block::bordered().title("Greeting")),
//! frame.size(),
//! );
//! }
@@ -184,14 +183,8 @@
//! [Constraint::Percentage(50), Constraint::Percentage(50)],
//! )
//! .split(main_layout[1]);
//! frame.render_widget(
//! Block::default().borders(Borders::ALL).title("Left"),
//! inner_layout[0],
//! );
//! frame.render_widget(
//! Block::default().borders(Borders::ALL).title("Right"),
//! inner_layout[1],
//! );
//! frame.render_widget(Block::bordered().title("Left"), inner_layout[0]);
//! frame.render_widget(Block::bordered().title("Right"), inner_layout[1]);
//! }
//! ```
//!

View File

@@ -68,14 +68,14 @@
//! [`prelude`]: crate::prelude
//! [`Span`]: crate::text::Span
use std::fmt::{self, Debug};
use std::fmt;
use bitflags::bitflags;
mod color;
mod stylize;
pub use color::Color;
pub use color::{Color, ParseColorError};
pub use stylize::{Styled, Stylize};
pub mod palette;
@@ -119,7 +119,7 @@ impl fmt::Debug for Modifier {
if self.is_empty() {
return write!(f, "NONE");
}
fmt::Debug::fmt(&self.0, f)
write!(f, "{}", self.0)
}
}
@@ -550,34 +550,30 @@ impl From<(Color, Color, Modifier, Modifier)> for Style {
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
fn styles() -> Vec<Style> {
vec![
Style::default(),
Style::default().fg(Color::Yellow),
Style::default().bg(Color::Yellow),
Style::default().add_modifier(Modifier::BOLD),
Style::default().remove_modifier(Modifier::BOLD),
Style::default().add_modifier(Modifier::ITALIC),
Style::default().remove_modifier(Modifier::ITALIC),
Style::default().add_modifier(Modifier::ITALIC | Modifier::BOLD),
Style::default().remove_modifier(Modifier::ITALIC | Modifier::BOLD),
]
}
use super::*;
#[test]
fn combined_patch_gives_same_result_as_individual_patch() {
let styles = styles();
let styles = [
Style::new(),
Style::new().fg(Color::Yellow),
Style::new().bg(Color::Yellow),
Style::new().add_modifier(Modifier::BOLD),
Style::new().remove_modifier(Modifier::BOLD),
Style::new().add_modifier(Modifier::ITALIC),
Style::new().remove_modifier(Modifier::ITALIC),
Style::new().add_modifier(Modifier::ITALIC | Modifier::BOLD),
Style::new().remove_modifier(Modifier::ITALIC | Modifier::BOLD),
];
for &a in &styles {
for &b in &styles {
for &c in &styles {
for &d in &styles {
let combined = a.patch(b.patch(c.patch(d)));
assert_eq!(
Style::default().patch(a).patch(b).patch(c).patch(d),
Style::default().patch(combined)
Style::new().patch(a).patch(b).patch(c).patch(d),
Style::new().patch(a.patch(b.patch(c.patch(d))))
);
}
}
@@ -589,7 +585,7 @@ mod tests {
fn combine_individual_modifiers() {
use crate::{buffer::Buffer, layout::Rect};
let mods = vec![
let mods = [
Modifier::BOLD,
Modifier::DIM,
Modifier::ITALIC,
@@ -603,37 +599,30 @@ mod tests {
let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
for m in &mods {
for m in mods {
buffer.get_mut(0, 0).set_style(Style::reset());
buffer
.get_mut(0, 0)
.set_style(Style::default().add_modifier(*m));
buffer.get_mut(0, 0).set_style(Style::new().add_modifier(m));
let style = buffer.get(0, 0).style();
assert!(style.add_modifier.contains(*m));
assert!(!style.sub_modifier.contains(*m));
assert!(style.add_modifier.contains(m));
assert!(!style.sub_modifier.contains(m));
}
}
#[test]
fn modifier_debug() {
assert_eq!(format!("{:?}", Modifier::empty()), "NONE");
assert_eq!(format!("{:?}", Modifier::BOLD), "BOLD");
assert_eq!(format!("{:?}", Modifier::DIM), "DIM");
assert_eq!(format!("{:?}", Modifier::ITALIC), "ITALIC");
assert_eq!(format!("{:?}", Modifier::UNDERLINED), "UNDERLINED");
assert_eq!(format!("{:?}", Modifier::SLOW_BLINK), "SLOW_BLINK");
assert_eq!(format!("{:?}", Modifier::RAPID_BLINK), "RAPID_BLINK");
assert_eq!(format!("{:?}", Modifier::REVERSED), "REVERSED");
assert_eq!(format!("{:?}", Modifier::HIDDEN), "HIDDEN");
assert_eq!(format!("{:?}", Modifier::CROSSED_OUT), "CROSSED_OUT");
assert_eq!(
format!("{:?}", Modifier::BOLD | Modifier::DIM),
"BOLD | DIM"
);
assert_eq!(
format!("{:?}", Modifier::all()),
"BOLD | DIM | ITALIC | UNDERLINED | SLOW_BLINK | RAPID_BLINK | REVERSED | HIDDEN | CROSSED_OUT"
);
#[rstest]
#[case(Modifier::empty(), "NONE")]
#[case(Modifier::BOLD, "BOLD")]
#[case(Modifier::DIM, "DIM")]
#[case(Modifier::ITALIC, "ITALIC")]
#[case(Modifier::UNDERLINED, "UNDERLINED")]
#[case(Modifier::SLOW_BLINK, "SLOW_BLINK")]
#[case(Modifier::RAPID_BLINK, "RAPID_BLINK")]
#[case(Modifier::REVERSED, "REVERSED")]
#[case(Modifier::HIDDEN, "HIDDEN")]
#[case(Modifier::CROSSED_OUT, "CROSSED_OUT")]
#[case(Modifier::BOLD | Modifier::DIM, "BOLD | DIM")]
#[case(Modifier::all(), "BOLD | DIM | ITALIC | UNDERLINED | SLOW_BLINK | RAPID_BLINK | REVERSED | HIDDEN | CROSSED_OUT")]
fn modifier_debug(#[case] modifier: Modifier, #[case] expected: &str) {
assert_eq!(format!("{modifier:?}"), expected);
}
#[test]

View File

@@ -1,9 +1,6 @@
#![allow(clippy::unreadable_literal)]
use std::{
fmt::{self, Debug, Display},
str::FromStr,
};
use std::{fmt, str::FromStr};
/// ANSI Color
///
@@ -66,7 +63,6 @@ use std::{
///
/// [ANSI color table]: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub enum Color {
/// Resets the foreground or background color
#[default]
@@ -141,14 +137,102 @@ impl Color {
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for Color {
/// This utilises the [`fmt::Display`] implementation for serialization.
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for Color {
/// This is used to deserialize a value into Color via serde.
///
/// This implementation uses the `FromStr` trait to deserialize strings, so named colours, RGB,
/// and indexed values are able to be deserialized. In addition, values that were produced by
/// the the older serialization implementation of Color are also able to be deserialized.
///
/// Prior to v0.26.0, Ratatui would be serialized using a map for indexed and RGB values, for
/// examples in json `{"Indexed": 10}` and `{"Rgb": [255, 0, 255]}` respectively. Now they are
/// serialized using the string representation of the index and the RGB hex value, for example
/// in json it would now be `"10"` and `"#FF00FF"` respectively.
///
/// See the [`Color`] documentation for more information on color names.
///
/// # Examples
///
/// ```
/// use ratatui::prelude::*;
///
/// #[derive(Debug, serde::Deserialize)]
/// struct Theme {
/// color: Color,
/// }
///
/// # fn get_theme() -> Result<(), serde_json::Error> {
/// let theme: Theme = serde_json::from_str(r#"{"color": "bright-white"}"#)?;
/// assert_eq!(theme.color, Color::White);
///
/// let theme: Theme = serde_json::from_str(r##"{"color": "#00FF00"}"##)?;
/// assert_eq!(theme.color, Color::Rgb(0, 255, 0));
///
/// let theme: Theme = serde_json::from_str(r#"{"color": "42"}"#)?;
/// assert_eq!(theme.color, Color::Indexed(42));
///
/// let err = serde_json::from_str::<Theme>(r#"{"color": "invalid"}"#).unwrap_err();
/// assert!(err.is_data());
/// assert_eq!(
/// err.to_string(),
/// "Failed to parse Colors at line 1 column 20"
/// );
///
/// // Deserializing from the previous serialization implementation
/// let theme: Theme = serde_json::from_str(r#"{"color": {"Rgb":[255,0,255]}}"#)?;
/// assert_eq!(theme.color, Color::Rgb(255, 0, 255));
///
/// let theme: Theme = serde_json::from_str(r#"{"color": {"Indexed":10}}"#)?;
/// assert_eq!(theme.color, Color::Indexed(10));
/// # Ok(())
/// # }
/// ```
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
FromStr::from_str(&s).map_err(serde::de::Error::custom)
/// Colors are currently serialized with the `Display` implementation, so
/// RGB values are serialized via hex, for example "#FFFFFF".
///
/// Previously they were serialized using serde derive, which encoded
/// RGB values as a map, for example { "rgb": [255, 255, 255] }.
///
/// The deserialization implementation utilises a `Helper` struct
/// to be able to support both formats for backwards compatibility.
#[derive(serde::Deserialize)]
enum ColorWrapper {
Rgb(u8, u8, u8),
Indexed(u8),
}
#[derive(serde::Deserialize)]
#[serde(untagged)]
enum ColorFormat {
V2(String),
V1(ColorWrapper),
}
let multi_type = ColorFormat::deserialize(deserializer)
.map_err(|err| serde::de::Error::custom(format!("Failed to parse Colors: {err}")))?;
match multi_type {
ColorFormat::V2(s) => FromStr::from_str(&s).map_err(serde::de::Error::custom),
ColorFormat::V1(color_wrapper) => match color_wrapper {
ColorWrapper::Rgb(red, green, blue) => Ok(Self::Rgb(red, green, blue)),
ColorWrapper::Indexed(index) => Ok(Self::Indexed(index)),
},
}
}
}
@@ -156,8 +240,8 @@ impl<'de> serde::Deserialize<'de> for Color {
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub struct ParseColorError;
impl std::fmt::Display for ParseColorError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl fmt::Display for ParseColorError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Failed to parse Colors")
}
}
@@ -249,7 +333,7 @@ impl FromStr for Color {
}
}
impl Display for Color {
impl fmt::Display for Color {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Reset => write!(f, "Reset"),
@@ -579,4 +663,42 @@ mod tests {
Color::deserialize("#00000000".into_deserializer());
assert!(color.is_err());
}
#[cfg(feature = "serde")]
#[test]
fn serialize_then_deserialize() -> Result<(), serde_json::Error> {
let json_rgb = serde_json::to_string(&Color::Rgb(255, 0, 255))?;
assert_eq!(json_rgb, r##""#FF00FF""##);
assert_eq!(
serde_json::from_str::<Color>(&json_rgb)?,
Color::Rgb(255, 0, 255)
);
let json_white = serde_json::to_string(&Color::White)?;
assert_eq!(json_white, r#""White""#);
let json_indexed = serde_json::to_string(&Color::Indexed(10))?;
assert_eq!(json_indexed, r#""10""#);
assert_eq!(
serde_json::from_str::<Color>(&json_indexed)?,
Color::Indexed(10)
);
Ok(())
}
#[cfg(feature = "serde")]
#[test]
fn deserialize_with_previous_format() -> Result<(), serde_json::Error> {
assert_eq!(Color::White, serde_json::from_str::<Color>("\"White\"")?);
assert_eq!(
Color::Rgb(255, 0, 255),
serde_json::from_str::<Color>(r#"{"Rgb":[255,0,255]}"#)?
);
assert_eq!(
Color::Indexed(10),
serde_json::from_str::<Color>(r#"{"Indexed":10}"#)?
);
Ok(())
}
}

View File

@@ -133,11 +133,7 @@ macro_rules! modifier {
/// "world".green().on_yellow().not_bold(),
/// ]);
/// let paragraph = Paragraph::new(line).italic().underlined();
/// let block = Block::default()
/// .title("Title")
/// .borders(Borders::ALL)
/// .on_white()
/// .bold();
/// let block = Block::bordered().title("Title").on_white().bold();
/// ```
pub trait Stylize<'a, T>: Sized {
#[must_use = "`bg` returns the modified style without modifying the original"]

View File

@@ -65,7 +65,7 @@ impl Frame<'_> {
/// # let backend = TestBackend::new(5, 5);
/// # let mut terminal = Terminal::new(backend).unwrap();
/// # let mut frame = terminal.get_frame();
/// let block = Block::default();
/// let block = Block::new();
/// let area = Rect::new(0, 0, 5, 5);
/// frame.render_widget(block, area);
/// ```
@@ -87,7 +87,7 @@ impl Frame<'_> {
/// # let backend = TestBackend::new(5, 5);
/// # let mut terminal = Terminal::new(backend).unwrap();
/// # let mut frame = terminal.get_frame();
/// let block = Block::default();
/// let block = Block::new();
/// let area = Rect::new(0, 0, 5, 5);
/// frame.render_widget_ref(block, area);
/// ```

View File

@@ -25,21 +25,20 @@
//! // Converted to Line(vec![
//! // Span { content: Cow::Borrowed("My title"), style: Style { .. } }
//! // ])
//! let block = Block::default().title("My title");
//! let block = Block::new().title("My title");
//!
//! // A simple string with a unique style.
//! // Converted to Line(vec![
//! // Span { content: Cow::Borrowed("My title"), style: Style { fg: Some(Color::Yellow), .. }
//! // ])
//! let block =
//! Block::default().title(Span::styled("My title", Style::default().fg(Color::Yellow)));
//! let block = Block::new().title(Span::styled("My title", Style::default().fg(Color::Yellow)));
//!
//! // A string with multiple styles.
//! // Converted to Line(vec![
//! // Span { content: Cow::Borrowed("My"), style: Style { fg: Some(Color::Yellow), .. } },
//! // Span { content: Cow::Borrowed(" title"), .. }
//! // ])
//! let block = Block::default().title(vec![
//! let block = Block::new().title(vec![
//! Span::styled("My", Style::default().fg(Color::Yellow)),
//! Span::raw(" title"),
//! ]);

View File

@@ -1,5 +1,8 @@
#![deny(missing_docs)]
use std::borrow::Cow;
#![warn(clippy::pedantic, clippy::nursery, clippy::arithmetic_side_effects)]
use std::{borrow::Cow, fmt};
use unicode_truncate::UnicodeTruncateStr;
use super::StyledGrapheme;
use crate::prelude::*;
@@ -552,32 +555,100 @@ impl Widget for Line<'_> {
impl WidgetRef for Line<'_> {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
let area = area.intersection(buf.area);
buf.set_style(area, self.style);
let width = self.width() as u16;
let offset = match self.alignment {
Some(Alignment::Center) => (area.width.saturating_sub(width)) / 2,
Some(Alignment::Right) => area.width.saturating_sub(width),
Some(Alignment::Left) | None => 0,
};
let mut x = area.left().saturating_add(offset);
for span in &self.spans {
let span_width = span.width() as u16;
let span_area = Rect {
x,
width: span_width.min(area.right() - x),
..area
};
span.render(span_area, buf);
x = x.saturating_add(span_width);
if x >= area.right() {
break;
}
if area.is_empty() {
return;
}
let line_width = self.width();
if line_width == 0 {
return;
}
buf.set_style(area, self.style);
let area_width = usize::from(area.width);
let can_render_complete_line = line_width <= area_width;
if can_render_complete_line {
let indent_width = match self.alignment {
Some(Alignment::Center) => (area_width.saturating_sub(line_width)) / 2,
Some(Alignment::Right) => area_width.saturating_sub(line_width),
Some(Alignment::Left) | None => 0,
};
let indent_width = u16::try_from(indent_width).unwrap_or(u16::MAX);
let area = area.indent_x(indent_width);
render_spans(&self.spans, area, buf, 0);
} else {
// There is not enough space to render the whole line. As the right side is truncated by
// the area width, only truncate the left.
let skip_width = match self.alignment {
Some(Alignment::Center) => (line_width.saturating_sub(area_width)) / 2,
Some(Alignment::Right) => line_width.saturating_sub(area_width),
Some(Alignment::Left) | None => 0,
};
render_spans(&self.spans, area, buf, skip_width);
};
}
}
impl std::fmt::Display for Line<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
/// Renders all the spans of the line that should be visible.
fn render_spans(spans: &[Span], mut area: Rect, buf: &mut Buffer, span_skip_width: usize) {
for (span, span_width, offset) in spans_after_width(spans, span_skip_width) {
area = area.indent_x(offset);
if area.is_empty() {
break;
}
span.render_ref(area, buf);
let span_width = u16::try_from(span_width).unwrap_or(u16::MAX);
area = area.indent_x(span_width);
}
}
/// Returns an iterator over the spans that lie after a given skip widtch from the start of the
/// `Line` (including a partially visible span if the `skip_width` lands within a span).
fn spans_after_width<'a>(
spans: &'a [Span],
mut skip_width: usize,
) -> impl Iterator<Item = (Span<'a>, usize, u16)> {
spans
.iter()
.map(|span| (span, span.width()))
// Filter non visible spans out.
.filter_map(move |(span, span_width)| {
// Ignore spans that are completely before the offset. Decrement `span_skip_width` by
// the span width until we find a span that is partially or completely visible.
if skip_width >= span_width {
skip_width = skip_width.saturating_sub(span_width);
return None;
}
// Apply the skip from the start of the span, not the end as the end will be trimmed
// when rendering the span to the buffer.
let available_width = span_width.saturating_sub(skip_width);
skip_width = 0; // ensure the next span is rendered in full
Some((span, span_width, available_width))
})
.map(|(span, span_width, available_width)| {
if span_width <= available_width {
// Span is fully visible. Clone here is fast as the underlying content is `Cow`.
return (span.clone(), span_width, 0u16);
}
// Span is only partially visible. As the end is truncated by the area width, only
// truncate the start of the span.
let (content, actual_width) = span.content.unicode_truncate_start(available_width);
// When the first grapheme of the span was truncated, start rendering from a position
// that takes that into account by indenting the start of the area
let first_grapheme_offset = available_width.saturating_sub(actual_width);
let first_grapheme_offset = u16::try_from(first_grapheme_offset).unwrap_or(u16::MAX);
(
Span::styled(content, span.style),
actual_width,
first_grapheme_offset,
)
})
}
impl fmt::Display for Line<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for span in &self.spans {
write!(f, "{span}")?;
}
@@ -878,8 +949,12 @@ mod tests {
}
mod widget {
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use super::*;
use crate::assert_buffer_eq;
use crate::buffer::Cell;
const BLUE: Style = Style::new().fg(Color::Blue);
const GREEN: Style = Style::new().fg(Color::Green);
const ITALIC: Style = Style::new().add_modifier(Modifier::ITALIC);
@@ -897,37 +972,36 @@ mod tests {
fn render() {
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
hello_world().render(Rect::new(0, 0, 15, 1), &mut buf);
let mut expected = Buffer::with_lines(vec!["Hello world! "]);
let mut expected = Buffer::with_lines(["Hello world! "]);
expected.set_style(Rect::new(0, 0, 15, 1), ITALIC);
expected.set_style(Rect::new(0, 0, 6, 1), BLUE);
expected.set_style(Rect::new(6, 0, 6, 1), GREEN);
assert_buffer_eq!(buf, expected);
assert_eq!(buf, expected);
}
#[rstest]
fn render_out_of_bounds(hello_world: Line<'static>, mut small_buf: Buffer) {
let out_of_bounds = Rect::new(20, 20, 10, 1);
hello_world.render(out_of_bounds, &mut small_buf);
assert_buffer_eq!(small_buf, Buffer::empty(small_buf.area));
assert_eq!(small_buf, Buffer::empty(small_buf.area));
}
#[test]
fn render_only_styles_line_area() {
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 1));
hello_world().render(Rect::new(0, 0, 15, 1), &mut buf);
let mut expected = Buffer::with_lines(vec!["Hello world! "]);
let mut expected = Buffer::with_lines(["Hello world! "]);
expected.set_style(Rect::new(0, 0, 15, 1), ITALIC);
expected.set_style(Rect::new(0, 0, 6, 1), BLUE);
expected.set_style(Rect::new(6, 0, 6, 1), GREEN);
assert_buffer_eq!(buf, expected);
assert_eq!(buf, expected);
}
#[test]
fn render_truncates() {
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 1));
Line::from("Hello world!").render(Rect::new(0, 0, 5, 1), &mut buf);
let expected = Buffer::with_lines(vec!["Hello "]);
assert_buffer_eq!(buf, expected);
assert_eq!(buf, Buffer::with_lines(["Hello "]));
}
#[test]
@@ -935,11 +1009,11 @@ mod tests {
let line = hello_world().alignment(Alignment::Center);
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
line.render(Rect::new(0, 0, 15, 1), &mut buf);
let mut expected = Buffer::with_lines(vec![" Hello world! "]);
let mut expected = Buffer::with_lines([" Hello world! "]);
expected.set_style(Rect::new(0, 0, 15, 1), ITALIC);
expected.set_style(Rect::new(1, 0, 6, 1), BLUE);
expected.set_style(Rect::new(7, 0, 6, 1), GREEN);
assert_buffer_eq!(buf, expected);
assert_eq!(buf, expected);
}
#[test]
@@ -947,11 +1021,255 @@ mod tests {
let line = hello_world().alignment(Alignment::Right);
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
line.render(Rect::new(0, 0, 15, 1), &mut buf);
let mut expected = Buffer::with_lines(vec![" Hello world!"]);
let mut expected = Buffer::with_lines([" Hello world!"]);
expected.set_style(Rect::new(0, 0, 15, 1), ITALIC);
expected.set_style(Rect::new(3, 0, 6, 1), BLUE);
expected.set_style(Rect::new(9, 0, 6, 1), GREEN);
assert_buffer_eq!(buf, expected);
assert_eq!(buf, expected);
}
#[test]
fn render_truncates_left() {
let mut buf = Buffer::empty(Rect::new(0, 0, 5, 1));
Line::from("Hello world")
.left_aligned()
.render(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["Hello"]));
}
#[test]
fn render_truncates_right() {
let mut buf = Buffer::empty(Rect::new(0, 0, 5, 1));
Line::from("Hello world")
.right_aligned()
.render(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["world"]));
}
#[test]
fn render_truncates_center() {
let mut buf = Buffer::empty(Rect::new(0, 0, 5, 1));
Line::from("Hello world")
.centered()
.render(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["lo wo"]));
}
/// Part of a regression test for <https://github.com/ratatui-org/ratatui/issues/1032> which
/// found panics with truncating lines that contained multi-byte characters.
#[test]
fn regression_1032() {
let line = Line::from(
"🦀 RFC8628 OAuth 2.0 Device Authorization GrantでCLIからGithubのaccess tokenを取得する"
);
let mut buf = Buffer::empty(Rect::new(0, 0, 83, 1));
line.render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines([
"🦀 RFC8628 OAuth 2.0 Device Authorization GrantでCLIからGithubのaccess tokenを取得 "
]));
}
/// Documentary test to highlight the crab emoji width / length discrepancy
///
/// Part of a regression test for <https://github.com/ratatui-org/ratatui/issues/1032> which
/// found panics with truncating lines that contained multi-byte characters.
#[test]
fn crab_emoji_width() {
let crab = "🦀";
assert_eq!(crab.len(), 4); // bytes
assert_eq!(crab.chars().count(), 1);
assert_eq!(crab.graphemes(true).count(), 1);
assert_eq!(crab.width(), 2); // display width
}
/// Part of a regression test for <https://github.com/ratatui-org/ratatui/issues/1032> which
/// found panics with truncating lines that contained multi-byte characters.
#[rstest]
#[case::left_4(Alignment::Left, 4, "1234")]
#[case::left_5(Alignment::Left, 5, "1234 ")]
#[case::left_6(Alignment::Left, 6, "1234🦀")]
#[case::left_7(Alignment::Left, 7, "1234🦀7")]
#[case::right_4(Alignment::Right, 4, "7890")]
#[case::right_5(Alignment::Right, 5, " 7890")]
#[case::right_6(Alignment::Right, 6, "🦀7890")]
#[case::right_7(Alignment::Right, 7, "4🦀7890")]
fn render_truncates_emoji(
#[case] alignment: Alignment,
#[case] buf_width: u16,
#[case] expected: &str,
) {
let line = Line::from("1234🦀7890").alignment(alignment);
let mut buf = Buffer::empty(Rect::new(0, 0, buf_width, 1));
line.render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines([expected]));
}
/// Part of a regression test for <https://github.com/ratatui-org/ratatui/issues/1032> which
/// found panics with truncating lines that contained multi-byte characters.
///
/// centering is tricky because there's an ambiguity about whether to take one more char
/// from the left or the right when the line width is odd. This interacts with the width of
/// the crab emoji, which is 2 characters wide by hitting the left or right side of the
/// emoji.
#[rstest]
#[case::center_6_0(6, 0, "")]
#[case::center_6_1(6, 1, " ")] // lef side of "🦀"
#[case::center_6_2(6, 2, "🦀")]
#[case::center_6_3(6, 3, "b🦀")]
#[case::center_6_4(6, 4, "b🦀c")]
#[case::center_7_0(7, 0, "")]
#[case::center_7_1(7, 1, " ")] // right side of "🦀"
#[case::center_7_2(7, 2, "🦀")]
#[case::center_7_3(7, 3, "🦀c")]
#[case::center_7_4(7, 4, "b🦀c")]
#[case::center_8_0(8, 0, "")]
#[case::center_8_1(8, 1, " ")] // right side of "🦀"
#[case::center_8_2(8, 2, " c")] // right side of "🦀c"
#[case::center_8_3(8, 3, "🦀c")]
#[case::center_8_4(8, 4, "🦀cd")]
#[case::center_8_5(8, 5, "b🦀cd")]
#[case::center_9_0(9, 0, "")]
#[case::center_9_1(9, 1, "c")]
#[case::center_9_2(9, 2, " c")] // right side of "🦀c"
#[case::center_9_3(9, 3, " cd")]
#[case::center_9_4(9, 4, "🦀cd")]
#[case::center_9_5(9, 5, "🦀cde")]
#[case::center_9_6(9, 6, "b🦀cde")]
fn render_truncates_emoji_center(
#[case] line_width: u16,
#[case] buf_width: u16,
#[case] expected: &str,
) {
// because the crab emoji is 2 characters wide, it will can cause the centering tests
// intersect with either the left or right part of the emoji, which causes the emoji to
// be not rendered. Checking for four different widths of the line is enough to cover
// all the possible cases.
let value = match line_width {
6 => "ab🦀cd",
7 => "ab🦀cde",
8 => "ab🦀cdef",
9 => "ab🦀cdefg",
_ => unreachable!(),
};
let line = Line::from(value).centered();
let mut buf = Buffer::empty(Rect::new(0, 0, buf_width, 1));
line.render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines([expected]));
}
/// Ensures the rendering also works away from the 0x0 position.
///
/// Particularly of note is that an emoji that is truncated will not overwrite the
/// characters that are already in the buffer. This is inentional (consider how a line
/// that is rendered on a border should not overwrite the border with a partial emoji).
#[rstest]
#[case::left(Alignment::Left, "XXa🦀bcXXX")]
#[case::center(Alignment::Center, "XX🦀bc🦀XX")]
#[case::right(Alignment::Right, "XXXbc🦀dXX")]
fn render_truncates_away_from_0x0(#[case] alignment: Alignment, #[case] expected: &str) {
let line = Line::from(vec![Span::raw("a🦀b"), Span::raw("c🦀d")]).alignment(alignment);
// Fill buffer with stuff to ensure the output is indeed padded
let mut buf = Buffer::filled(Rect::new(0, 0, 10, 1), Cell::default().set_symbol("X"));
let area = Rect::new(2, 0, 6, 1);
line.render_ref(area, &mut buf);
assert_eq!(buf, Buffer::with_lines([expected]));
}
/// When two spans are rendered after each other the first needs to be padded in accordance
/// to the skipped unicode width. In this case the first crab does not fit at width 6 which
/// takes a front white space.
#[rstest]
#[case::right_4(4, "c🦀d")]
#[case::right_5(5, "bc🦀d")]
#[case::right_6(6, "Xbc🦀d")]
#[case::right_7(7, "🦀bc🦀d")]
#[case::right_8(8, "a🦀bc🦀d")]
fn render_right_aligned_multi_span(#[case] buf_width: u16, #[case] expected: &str) {
let line = Line::from(vec![Span::raw("a🦀b"), Span::raw("c🦀d")]).right_aligned();
let area = Rect::new(0, 0, buf_width, 1);
// Fill buffer with stuff to ensure the output is indeed padded
let mut buf = Buffer::filled(area, Cell::default().set_symbol("X"));
line.render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines([expected]));
}
/// Part of a regression test for <https://github.com/ratatui-org/ratatui/issues/1032> which
/// found panics with truncating lines that contained multi-byte characters.
///
/// Flag emoji are actually two independent characters, so they can be truncated in the
/// middle of the emoji. This test documents just the emoji part of the test.
#[test]
fn flag_emoji() {
let str = "🇺🇸1234";
assert_eq!(str.len(), 12); // flag is 4 bytes
assert_eq!(str.chars().count(), 6); // flag is 2 chars
assert_eq!(str.graphemes(true).count(), 5); // flag is 1 grapheme
assert_eq!(str.width(), 6); // flag is 2 display width
}
/// Part of a regression test for <https://github.com/ratatui-org/ratatui/issues/1032> which
/// found panics with truncating lines that contained multi-byte characters.
#[rstest]
#[case::flag_1(1, " ")]
#[case::flag_2(2, "🇺🇸")]
#[case::flag_3(3, "🇺🇸1")]
#[case::flag_4(4, "🇺🇸12")]
#[case::flag_5(5, "🇺🇸123")]
#[case::flag_6(6, "🇺🇸1234")]
#[case::flag_7(7, "🇺🇸1234 ")]
fn render_truncates_flag(#[case] buf_width: u16, #[case] expected: &str) {
let line = Line::from("🇺🇸1234");
let mut buf = Buffer::empty(Rect::new(0, 0, buf_width, 1));
line.render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines([expected]));
}
// Buffer width is `u16`. A line can be longer.
#[rstest]
#[case::left(Alignment::Left, "This is some content with a some")]
#[case::right(Alignment::Right, "horribly long Line over u16::MAX")]
fn render_truncates_very_long_line_of_many_spans(
#[case] alignment: Alignment,
#[case] expected: &str,
) {
let part = "This is some content with a somewhat long width to be repeated over and over again to create horribly long Line over u16::MAX";
let min_width = usize::from(u16::MAX).saturating_add(1);
// width == len as only ASCII is used here
let factor = min_width.div_ceil(part.len());
let line = Line::from(vec![Span::raw(part); factor]).alignment(alignment);
dbg!(line.width());
assert!(line.width() >= min_width);
let mut buf = Buffer::empty(Rect::new(0, 0, 32, 1));
line.render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines([expected]));
}
// Buffer width is `u16`. A single span inside a line can be longer.
#[rstest]
#[case::left(Alignment::Left, "This is some content with a some")]
#[case::right(Alignment::Right, "horribly long Line over u16::MAX")]
fn render_truncates_very_long_single_span_line(
#[case] alignment: Alignment,
#[case] expected: &str,
) {
let part = "This is some content with a somewhat long width to be repeated over and over again to create horribly long Line over u16::MAX";
let min_width = usize::from(u16::MAX).saturating_add(1);
// width == len as only ASCII is used here
let factor = min_width.div_ceil(part.len());
let line = Line::from(vec![Span::raw(part.repeat(factor))]).alignment(alignment);
dbg!(line.width());
assert!(line.width() >= min_width);
let mut buf = Buffer::empty(Rect::new(0, 0, 32, 1));
line.render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines([expected]));
}
}

View File

@@ -1,7 +1,4 @@
use std::{
borrow::Cow,
fmt::{self, Debug, Display},
};
use std::{borrow::Cow, fmt};
use super::Text;
@@ -19,7 +16,7 @@ use super::Text;
/// let password = Masked::new("12345", 'x');
///
/// Paragraph::new(password).render(buffer.area, &mut buffer);
/// assert_eq!(buffer, Buffer::with_lines(vec!["xxxxx"]));
/// assert_eq!(buffer, Buffer::with_lines(["xxxxx"]));
/// ```
#[derive(Default, Clone, Eq, PartialEq, Hash)]
pub struct Masked<'a> {
@@ -46,17 +43,18 @@ impl<'a> Masked<'a> {
}
}
impl Debug for Masked<'_> {
impl fmt::Debug for Masked<'_> {
/// Debug representation of a masked string is the underlying string
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Display::fmt(&self.inner, f)
// note that calling display instead of Debug here is intentional
fmt::Display::fmt(&self.inner, f)
}
}
impl Display for Masked<'_> {
impl fmt::Display for Masked<'_> {
/// Display representation of a masked string is the masked string
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Display::fmt(&self.value(), f)
fmt::Display::fmt(&self.value(), f)
}
}
@@ -112,12 +110,14 @@ mod tests {
fn debug() {
let masked = Masked::new("12345", 'x');
assert_eq!(format!("{masked:?}"), "12345");
assert_eq!(format!("{masked:.3?}"), "123", "Debug truncates");
}
#[test]
fn display() {
let masked = Masked::new("12345", 'x');
assert_eq!(format!("{masked}"), "xxxxx");
assert_eq!(format!("{masked:.3}"), "xxx", "Display truncates");
}
#[test]

View File

@@ -1,4 +1,4 @@
use std::{borrow::Cow, fmt::Debug};
use std::{borrow::Cow, fmt};
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
@@ -395,9 +395,9 @@ impl WidgetRef for Span<'_> {
}
}
impl std::fmt::Display for Span<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", &self.content)
impl fmt::Display for Span<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.content, f)
}
}
@@ -529,15 +529,15 @@ mod tests {
#[test]
fn display_span() {
let span = Span::raw("test content");
assert_eq!(format!("{span}"), "test content");
assert_eq!(format!("{span:.4}"), "test");
}
#[test]
fn display_styled_span() {
let stylized_span = Span::styled("stylized test content", Style::new().green());
assert_eq!(format!("{stylized_span}"), "stylized test content");
assert_eq!(format!("{stylized_span:.8}"), "stylized");
}
#[test]
@@ -565,7 +565,6 @@ mod tests {
use rstest::rstest;
use super::*;
use crate::assert_buffer_eq;
#[test]
fn render() {
@@ -573,12 +572,11 @@ mod tests {
let span = Span::styled("test content", style);
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
span.render(buf.area, &mut buf);
let expected = Buffer::with_lines(vec![Line::from(vec![
let expected = Buffer::with_lines([Line::from(vec![
"test content".green().on_yellow(),
" ".into(),
])]);
assert_buffer_eq!(buf, expected);
assert_eq!(buf, expected);
}
#[rstest]
@@ -598,10 +596,11 @@ mod tests {
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 1));
span.render(Rect::new(0, 0, 5, 1), &mut buf);
let mut expected = Buffer::with_lines(vec![Line::from("test ")]);
expected.set_style(Rect::new(0, 0, 5, 1), (Color::Green, Color::Yellow));
assert_buffer_eq!(buf, expected);
let expected = Buffer::with_lines([Line::from(vec![
"test ".green().on_yellow(),
" ".into(),
])]);
assert_eq!(buf, expected);
}
/// When there is already a style set on the buffer, the style of the span should be
@@ -613,12 +612,11 @@ mod tests {
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
buf.set_style(buf.area, Style::new().italic());
span.render(buf.area, &mut buf);
let expected = Buffer::with_lines(vec![Line::from(vec![
let expected = Buffer::with_lines([Line::from(vec![
"test content".green().on_yellow().italic(),
" ".italic(),
])]);
assert_buffer_eq!(buf, expected);
assert_eq!(buf, expected);
}
/// When the span contains a multi-width grapheme, the grapheme will ensure that the cells
@@ -629,12 +627,11 @@ mod tests {
let span = Span::styled("test 😃 content", style);
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
span.render(buf.area, &mut buf);
// The existing code in buffer.set_line() handles multi-width graphemes by clearing the
// cells of the hidden characters. This test ensures that the existing behavior is
// preserved.
let expected = Buffer::with_lines(vec!["test 😃 content".green().on_yellow()]);
assert_buffer_eq!(buf, expected);
let expected = Buffer::with_lines(["test 😃 content".green().on_yellow()]);
assert_eq!(buf, expected);
}
/// When the span contains a multi-width grapheme that does not fit in the area passed to
@@ -647,11 +644,9 @@ mod tests {
let mut buf = Buffer::empty(Rect::new(0, 0, 6, 1));
span.render(buf.area, &mut buf);
let expected = Buffer::with_lines(vec![Line::from(vec![
"test ".green().on_yellow(),
" ".into(),
])]);
assert_buffer_eq!(buf, expected);
let expected =
Buffer::with_lines([Line::from(vec!["test ".green().on_yellow(), " ".into()])]);
assert_eq!(buf, expected);
}
/// When the area passed to render overflows the buffer, the content should be truncated
@@ -663,11 +658,11 @@ mod tests {
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
span.render(Rect::new(10, 0, 20, 1), &mut buf);
let expected = Buffer::with_lines(vec![Line::from(vec![
let expected = Buffer::with_lines([Line::from(vec![
" ".into(),
"test ".green().on_yellow(),
])]);
assert_buffer_eq!(buf, expected);
assert_eq!(buf, expected);
}
}
}

View File

@@ -1,5 +1,5 @@
#![warn(missing_docs)]
use std::borrow::Cow;
use std::{borrow::Cow, fmt};
use itertools::{Itertools, Position};
@@ -580,8 +580,8 @@ where
}
}
impl std::fmt::Display for Text<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl fmt::Display for Text<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (position, line) in self.iter().with_position() {
if position == Position::Last {
write!(f, "{line}")?;
@@ -962,19 +962,14 @@ mod tests {
mod widget {
use super::*;
use crate::assert_buffer_eq;
#[test]
fn render() {
let text = Text::from("foo");
let area = Rect::new(0, 0, 5, 1);
let mut buf = Buffer::empty(area);
text.render(area, &mut buf);
let expected_buf = Buffer::with_lines(vec!["foo "]);
assert_buffer_eq!(buf, expected_buf);
assert_eq!(buf, Buffer::with_lines(["foo "]));
}
#[rstest]
@@ -987,40 +982,28 @@ mod tests {
#[test]
fn render_right_aligned() {
let text = Text::from("foo").alignment(Alignment::Right);
let area = Rect::new(0, 0, 5, 1);
let mut buf = Buffer::empty(area);
text.render(area, &mut buf);
let expected_buf = Buffer::with_lines(vec![" foo"]);
assert_buffer_eq!(buf, expected_buf);
assert_eq!(buf, Buffer::with_lines([" foo"]));
}
#[test]
fn render_centered_odd() {
let text = Text::from("foo").alignment(Alignment::Center);
let area = Rect::new(0, 0, 5, 1);
let mut buf = Buffer::empty(area);
text.render(area, &mut buf);
let expected_buf = Buffer::with_lines(vec![" foo "]);
assert_buffer_eq!(buf, expected_buf);
assert_eq!(buf, Buffer::with_lines([" foo "]));
}
#[test]
fn render_centered_even() {
let text = Text::from("foo").alignment(Alignment::Center);
let area = Rect::new(0, 0, 6, 1);
let mut buf = Buffer::empty(area);
text.render(area, &mut buf);
let expected_buf = Buffer::with_lines(vec![" foo "]);
assert_buffer_eq!(buf, expected_buf);
assert_eq!(buf, Buffer::with_lines([" foo "]));
}
#[test]
@@ -1030,14 +1013,10 @@ mod tests {
Line::from("bar").alignment(Alignment::Center),
])
.alignment(Alignment::Right);
let area = Rect::new(0, 0, 5, 2);
let mut buf = Buffer::empty(area);
text.render(area, &mut buf);
let expected_buf = Buffer::with_lines(vec![" foo", " bar "]);
assert_buffer_eq!(buf, expected_buf);
assert_eq!(buf, Buffer::with_lines([" foo", " bar "]));
}
#[test]
@@ -1046,10 +1025,9 @@ mod tests {
let mut buf = Buffer::empty(area);
Text::from("foo".on_blue()).render(area, &mut buf);
let mut expected = Buffer::with_lines(vec!["foo "]);
let mut expected = Buffer::with_lines(["foo "]);
expected.set_style(Rect::new(0, 0, 3, 1), Style::new().bg(Color::Blue));
assert_buffer_eq!(buf, expected);
assert_eq!(buf, expected);
}
#[test]
@@ -1057,10 +1035,9 @@ mod tests {
let mut buf = Buffer::empty(Rect::new(0, 0, 6, 1));
Text::from("foobar".on_blue()).render(Rect::new(0, 0, 3, 1), &mut buf);
let mut expected = Buffer::with_lines(vec!["foo "]);
let mut expected = Buffer::with_lines(["foo "]);
expected.set_style(Rect::new(0, 0, 3, 1), Style::new().bg(Color::Blue));
assert_buffer_eq!(buf, expected);
assert_eq!(buf, expected);
}
}

View File

@@ -653,6 +653,6 @@ mod tests {
#[rstest]
fn string_option_render_ref(mut buf: Buffer) {
Some(String::from("hello world")).render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]),);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
}

View File

@@ -45,7 +45,7 @@ pub use bar_group::BarGroup;
/// use ratatui::{prelude::*, widgets::*};
///
/// BarChart::default()
/// .block(Block::default().title("BarChart").borders(Borders::ALL))
/// .block(Block::bordered().title("BarChart"))
/// .bar_width(3)
/// .bar_gap(1)
/// .group_gap(3)
@@ -615,151 +615,140 @@ mod tests {
use itertools::iproduct;
use super::*;
use crate::{
assert_buffer_eq,
widgets::{BorderType, Borders},
};
use crate::widgets::BorderType;
#[test]
fn default() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
let widget = BarChart::default();
widget.render(buffer.area, &mut buffer);
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" "; 3]));
assert_eq!(buffer, Buffer::with_lines([" "; 3]));
}
#[test]
fn data() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
let widget = BarChart::default().data(&[("foo", 1), ("bar", 2)]);
widget.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
" ",
"1 2 ",
"f b ",
])
);
#[rustfmt::skip]
let expected = Buffer::with_lines([
"",
"1 2 ",
"f b ",
]);
assert_eq!(buffer, expected);
}
#[test]
fn block() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 5));
let block = Block::default()
.title("Block")
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 5));
let block = Block::bordered()
.border_type(BorderType::Double)
.borders(Borders::ALL);
.title("Block");
let widget = BarChart::default()
.data(&[("foo", 1), ("bar", 2)])
.block(block);
widget.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
"╔Block════════╗",
"",
"║1 2 ║",
"║f b ║",
"╚═════════════╝",
])
);
let expected = Buffer::with_lines([
"╔Block═══╗",
"║ █ ║",
"║1 2 ║",
"f b",
"╚════════╝",
]);
assert_eq!(buffer, expected);
}
#[test]
fn max() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
let without_max = BarChart::default().data(&[("foo", 1), ("bar", 2), ("baz", 100)]);
without_max.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
" ",
" ",
"f b b ",
])
);
#[rustfmt::skip]
let expected = Buffer::with_lines([
"",
"",
"f b b ",
]);
assert_eq!(buffer, expected);
let with_max = BarChart::default()
.data(&[("foo", 1), ("bar", 2), ("baz", 100)])
.max(2);
with_max.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
" █ █ ",
"1 2 █ ",
"f b b ",
])
);
#[rustfmt::skip]
let expected = Buffer::with_lines([
" █ █ ",
"1 2 █ ",
"f b b ",
]);
assert_eq!(buffer, expected);
}
#[test]
fn bar_style() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
let widget = BarChart::default()
.data(&[("foo", 1), ("bar", 2)])
.bar_style(Style::new().red());
widget.render(buffer.area, &mut buffer);
let mut expected = Buffer::with_lines(vec![
"",
"1 2 ",
"f b ",
#[rustfmt::skip]
let mut expected = Buffer::with_lines([
" ",
"1 2 ",
"f b ",
]);
for (x, y) in iproduct!([0, 2], [0, 1]) {
expected.get_mut(x, y).set_fg(Color::Red);
}
assert_buffer_eq!(buffer, expected);
assert_eq!(buffer, expected);
}
#[test]
fn bar_width() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
let widget = BarChart::default()
.data(&[("foo", 1), ("bar", 2)])
.bar_width(3);
widget.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
" ███ ",
"█1█ █2█ ",
"foo bar ",
])
);
#[rustfmt::skip]
let expected = Buffer::with_lines([
" ███ ",
"█1█ █2█ ",
"foo bar ",
]);
assert_eq!(buffer, expected);
}
#[test]
fn bar_gap() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
let widget = BarChart::default()
.data(&[("foo", 1), ("bar", 2)])
.bar_gap(2);
widget.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
" ",
"1 2 ",
"f b ",
])
);
#[rustfmt::skip]
let expected = Buffer::with_lines([
"",
"1 2 ",
"f b ",
]);
assert_eq!(buffer, expected);
}
#[test]
fn bar_set() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
let widget = BarChart::default()
.data(&[("foo", 0), ("bar", 1), ("baz", 3)])
.bar_set(symbols::bar::THREE_LEVELS);
widget.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
" ",
" ▄ 3 ",
"f b b ",
])
);
#[rustfmt::skip]
let expected = Buffer::with_lines([
"",
" ▄ 3 ",
"f b b ",
]);
assert_eq!(buffer, expected);
}
#[test]
@@ -779,67 +768,68 @@ mod tests {
])
.bar_set(symbols::bar::NINE_LEVELS);
widget.render(Rect::new(0, 1, 18, 2), &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
" ",
" ▁ ▂ ▃ ▄ ▅ ▆ ▇ 8 ",
"a b c d e f g h i ",
])
);
let expected = Buffer::with_lines([
" ",
" ▁ ▂ ▃ ▄ ▅ ▆ ▇ 8 ",
"a b c d e f g h i ",
]);
assert_eq!(buffer, expected);
}
#[test]
fn value_style() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
let widget = BarChart::default()
.data(&[("foo", 1), ("bar", 2)])
.bar_width(3)
.value_style(Style::new().red());
widget.render(buffer.area, &mut buffer);
let mut expected = Buffer::with_lines(vec![
" ███ ",
"█1█ █2█ ",
"foo bar ",
#[rustfmt::skip]
let mut expected = Buffer::with_lines([
" ███ ",
"█1█ █2█ ",
"foo bar ",
]);
expected.get_mut(1, 1).set_fg(Color::Red);
expected.get_mut(5, 1).set_fg(Color::Red);
assert_buffer_eq!(buffer, expected);
assert_eq!(buffer, expected);
}
#[test]
fn label_style() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
let widget = BarChart::default()
.data(&[("foo", 1), ("bar", 2)])
.label_style(Style::new().red());
widget.render(buffer.area, &mut buffer);
let mut expected = Buffer::with_lines(vec![
"",
"1 2 ",
"f b ",
#[rustfmt::skip]
let mut expected = Buffer::with_lines([
" ",
"1 2 ",
"f b ",
]);
expected.get_mut(0, 2).set_fg(Color::Red);
expected.get_mut(2, 2).set_fg(Color::Red);
assert_buffer_eq!(buffer, expected);
assert_eq!(buffer, expected);
}
#[test]
fn style() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
let widget = BarChart::default()
.data(&[("foo", 1), ("bar", 2)])
.style(Style::new().red());
widget.render(buffer.area, &mut buffer);
let mut expected = Buffer::with_lines(vec![
"",
"1 2 ",
"f b ",
#[rustfmt::skip]
let mut expected = Buffer::with_lines([
" ",
"1 2 ",
"f b ",
]);
for (x, y) in iproduct!(0..15, 0..3) {
for (x, y) in iproduct!(0..10, 0..3) {
expected.get_mut(x, y).set_fg(Color::Red);
}
assert_buffer_eq!(buffer, expected);
assert_eq!(buffer, expected);
}
#[test]
@@ -865,8 +855,13 @@ mod tests {
let mut buffer = Buffer::empty(Rect::new(0, 0, 3, 3));
chart.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines(vec!["", "1 2", "G "]);
assert_buffer_eq!(buffer, expected);
#[rustfmt::skip]
let expected = Buffer::with_lines([
"",
"1 2",
"G ",
]);
assert_eq!(buffer, expected);
}
fn build_test_barchart<'a>() -> BarChart<'a> {
@@ -892,7 +887,7 @@ mod tests {
let mut buffer = Buffer::empty(Rect::new(0, 0, 5, 8));
chart.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines(vec![
let expected = Buffer::with_lines([
"2█ ",
"3██ ",
"4███ ",
@@ -902,8 +897,7 @@ mod tests {
"5████",
"G2 ",
]);
assert_buffer_eq!(buffer, expected);
assert_eq!(buffer, expected);
}
#[test]
@@ -912,7 +906,7 @@ mod tests {
let mut buffer = Buffer::empty(Rect::new(0, 0, 5, 7));
chart.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines(vec![
let expected = Buffer::with_lines([
"2█ ",
"3██ ",
"4███ ",
@@ -921,8 +915,7 @@ mod tests {
"4███ ",
"5████",
]);
assert_buffer_eq!(buffer, expected);
assert_eq!(buffer, expected);
}
#[test]
@@ -931,9 +924,15 @@ mod tests {
let mut buffer = Buffer::empty(Rect::new(0, 0, 5, 5));
chart.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines(vec!["2█ ", "3██ ", "4███ ", "G1 ", "3██ "]);
assert_buffer_eq!(buffer, expected);
#[rustfmt::skip]
let expected = Buffer::with_lines([
"2█ ",
"3██ ",
"4███ ",
"G1 ",
"3██ ",
]);
assert_eq!(buffer, expected);
}
fn test_horizontal_bars_label_width_greater_than_bar(bar_color: Option<Color>) {
@@ -956,7 +955,7 @@ mod tests {
let mut buffer = Buffer::empty(Rect::new(0, 0, 5, 2));
chart.render(buffer.area, &mut buffer);
let mut expected = Buffer::with_lines(vec!["label", "5████"]);
let mut expected = Buffer::with_lines(["label", "5████"]);
// first line has a yellow foreground. first cell contains italic "5"
expected.get_mut(0, 1).modifier.insert(Modifier::ITALIC);
@@ -981,7 +980,7 @@ mod tests {
expected.get_mut(3, 0).set_fg(expected_color);
expected.get_mut(4, 0).set_fg(expected_color);
assert_buffer_eq!(buffer, expected);
assert_eq!(buffer, expected);
}
#[test]
@@ -1004,9 +1003,13 @@ mod tests {
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
chart.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines(vec!["Jan 10█ ", "Feb 20████", "Mar 5 "]);
assert_buffer_eq!(buffer, expected);
#[rustfmt::skip]
let expected = Buffer::with_lines([
"Jan 10█ ",
"Feb 20████",
"Mar 5 ",
]);
assert_eq!(buffer, expected);
}
#[test]
@@ -1027,13 +1030,13 @@ mod tests {
// G1 should have the bold red style
// bold: because of BarChart::label_style
// red: is included with the label itself
let mut expected = Buffer::with_lines(vec!["2████", "G1 "]);
let mut expected = Buffer::with_lines(["2████", "G1 "]);
let cell = expected.get_mut(0, 1).set_fg(Color::Red);
cell.modifier.insert(Modifier::BOLD);
let cell = expected.get_mut(1, 1).set_fg(Color::Red);
cell.modifier.insert(Modifier::BOLD);
assert_buffer_eq!(buffer, expected);
assert_eq!(buffer, expected);
}
#[test]
@@ -1050,17 +1053,14 @@ mod tests {
let mut buffer = Buffer::empty(Rect::new(0, 0, 13, 5));
chart.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
" ▂ █ ▂",
" ▄ █ █ ▄ █",
"▆ 2 3 4 ▆ 2 3",
"a b c c a b c",
" G1 G2 ",
])
);
let expected = Buffer::with_lines([
" ▂ █ ▂",
" ▄ █ █ ▄ █",
"▆ 2 3 4 ▆ 2 3",
"a b c c a b c",
" G1 G2 ",
]);
assert_eq!(buffer, expected);
}
#[test]
@@ -1073,9 +1073,13 @@ mod tests {
let mut buffer = Buffer::empty(Rect::new(0, 0, 3, 3));
chart.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines(vec!["", "▆ 5", " G"]);
assert_buffer_eq!(buffer, expected);
#[rustfmt::skip]
let expected = Buffer::with_lines([
"",
"▆ 5",
" G",
]);
assert_eq!(buffer, expected);
}
#[test]
@@ -1098,15 +1102,14 @@ mod tests {
let mut buffer = Buffer::empty(Rect::new(0, 0, 11, 5));
chart.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines(vec![
let expected = Buffer::with_lines([
" ▆▆▆ ███",
" ███ ███",
"▃▃▃ ███ ███",
"写█ 写█ 写█",
"B1 B2 B2 ",
]);
assert_buffer_eq!(buffer, expected);
assert_eq!(buffer, expected);
}
#[test]
@@ -1118,7 +1121,7 @@ mod tests {
.bar_gap(0);
let mut buffer = Buffer::empty(Rect::new(0, 0, 0, 10));
chart.render(buffer.area, &mut buffer);
assert_buffer_eq!(buffer, Buffer::empty(Rect::new(0, 0, 0, 10)));
assert_eq!(buffer, Buffer::empty(Rect::new(0, 0, 0, 10)));
}
#[test]
@@ -1143,8 +1146,7 @@ mod tests {
let mut buffer = Buffer::empty(Rect::new(0, 0, 17, 1));
chart.render(buffer.area, &mut buffer);
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" ▁ ▂ ▃ ▄ ▅ ▆ ▇ 8"]));
assert_eq!(buffer, Buffer::with_lines([" ▁ ▂ ▃ ▄ ▅ ▆ ▇ 8"]));
}
#[test]
@@ -1169,15 +1171,12 @@ mod tests {
let mut buffer = Buffer::empty(Rect::new(0, 0, 17, 3));
chart.render(Rect::new(0, 1, buffer.area.width, 2), &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
" ",
" ▁ ▂ ▃ ▄ ▅ ▆ ▇ 8",
"a b c d e f g h i",
])
);
let expected = Buffer::with_lines([
" ",
" ▁ ▂ ▃ ▄ ▅ ▆ ▇ 8",
"a b c d e f g h i",
]);
assert_eq!(buffer, expected);
}
#[test]
@@ -1202,15 +1201,12 @@ mod tests {
let mut buffer = Buffer::empty(Rect::new(0, 0, 17, 3));
chart.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
" ▁ ▂ ▃ ▄ ▅ ▆ ▇ 8",
"a b c d e f g h i",
" Group ",
])
);
let expected = Buffer::with_lines([
" ▁ ▂ ▃ ▄ ▅ ▆ ▇ 8",
"a b c d e f g h i",
" Group ",
]);
assert_eq!(buffer, expected);
}
#[test]
@@ -1235,15 +1231,12 @@ mod tests {
let mut buffer = Buffer::empty(Rect::new(0, 0, 26, 3));
chart.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
" 1▁ 2▂ 3▃ 4▄ 5▅ 6▆ 7▇ 8█",
"a b c d e f g h i ",
" Group ",
])
);
let expected = Buffer::with_lines([
" 1▁ 2▂ 3▃ 4▄ 5▅ 6▆ 7▇ 8█",
"a b c d e f g h i ",
" Group ",
]);
assert_eq!(buffer, expected);
}
#[test]
@@ -1268,16 +1261,13 @@ mod tests {
let mut buffer = Buffer::empty(Rect::new(0, 0, 17, 4));
chart.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
" ▂ ▄ ▆ █",
" ▂ ▄ ▆ 4 5 6 7 8",
"a b c d e f g h i",
" Group ",
])
);
let expected = Buffer::with_lines([
" ▂ ▄ ▆ █",
" ▂ ▄ ▆ 4 5 6 7 8",
"a b c d e f g h i",
" Group ",
]);
assert_eq!(buffer, expected);
}
#[test]
@@ -1300,15 +1290,12 @@ mod tests {
let mut buffer = Buffer::empty(Rect::new(0, 0, 17, 3));
chart.render(Rect::new(0, 1, buffer.area.width, 2), &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
" ",
" ▁ ▂ ▃ ▄ ▅ ▆ ▇ 8",
" Group ",
])
);
let expected = Buffer::with_lines([
" ",
" ▁ ▂ ▃ ▄ ▅ ▆ ▇ 8",
" Group ",
]);
assert_eq!(buffer, expected);
}
#[test]
@@ -1319,13 +1306,9 @@ mod tests {
let mut buffer = Buffer::empty(Rect::new(0, 0, 59, 1));
chart.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
" ▁ ▁ ▁ ▁ ▂ ▂ ▂ ▃ ▃ ▃ ▃ ▄ ▄ ▄ ▄ ▅ ▅ ▅ ▆ ▆ ▆ ▆ ▇ ▇ ▇ █",
])
);
let expected =
Buffer::with_lines([" ▁ ▁ ▁ ▁ ▂ ▂ ▂ ▃ ▃ ▃ ▃ ▄ ▄ ▄ ▄ ▅ ▅ ▅ ▆ ▆ ▆ ▆ ▇ ▇ ▇ █"]);
assert_eq!(buffer, expected);
}
#[test]
@@ -1337,17 +1320,14 @@ mod tests {
let mut buffer = Buffer::empty(Rect::new(0, 0, 7, 6));
chart.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
" ██ ",
"",
"▄▄ ██ ",
"██ ██ ",
"1█ 2█ ",
"a b ",
])
);
let expected = Buffer::with_lines([
" ██ ",
" ██ ",
"▄▄ ██ ",
"██ ██ ",
"1█ 2",
"a b ",
]);
assert_eq!(buffer, expected);
}
}

View File

@@ -41,12 +41,12 @@ pub use title::{Position, Title};
/// ```
/// use ratatui::{prelude::*, widgets::*};
///
/// Block::default()
/// .title("Block")
/// Block::new()
/// .border_type(BorderType::Rounded)
/// .borders(Borders::LEFT | Borders::RIGHT)
/// .border_style(Style::default().fg(Color::White))
/// .border_type(BorderType::Rounded)
/// .style(Style::default().bg(Color::Black));
/// .style(Style::default().bg(Color::Black))
/// .title("Block");
/// ```
///
/// You may also use multiple titles like in the following:
@@ -56,7 +56,7 @@ pub use title::{Position, Title};
/// widgets::{block::*, *},
/// };
///
/// Block::default()
/// Block::new()
/// .title("Title 1")
/// .title(Title::from("Title 2").position(Position::Bottom));
/// ```
@@ -173,6 +173,11 @@ impl<'a> Block<'a> {
}
/// Create a new block with [all borders](Borders::ALL) shown
///
/// ```
/// # use ratatui::widgets::{Block, Borders};
/// assert_eq!(Block::bordered(), Block::new().borders(Borders::ALL));
/// ```
pub const fn bordered() -> Self {
let mut block = Self::new();
block.borders = Borders::ALL;
@@ -219,7 +224,7 @@ impl<'a> Block<'a> {
/// widgets::{block::*, *},
/// };
///
/// Block::default()
/// Block::new()
/// .title("Title") // By default in the top left corner
/// .title(Title::from("Left").alignment(Alignment::Left)) // also on the left
/// .title(Title::from("Right").alignment(Alignment::Right))
@@ -325,12 +330,12 @@ impl<'a> Block<'a> {
/// widgets::{block::*, *},
/// };
///
/// Block::default()
/// Block::new()
/// .title_alignment(Alignment::Center)
/// // This title won't be aligned in the center
/// .title(Title::from("right").alignment(Alignment::Right))
/// .title("foo")
/// .title("bar")
/// .title_alignment(Alignment::Center);
/// .title("bar");
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub const fn title_alignment(mut self, alignment: Alignment) -> Self {
@@ -352,12 +357,12 @@ impl<'a> Block<'a> {
/// widgets::{block::*, *},
/// };
///
/// Block::default()
/// Block::new()
/// .title_position(Position::Bottom)
/// // This title won't be aligned in the center
/// .title(Title::from("top").position(Position::Top))
/// .title("foo")
/// .title("bar")
/// .title_position(Position::Bottom);
/// .title("bar");
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub const fn title_position(mut self, position: Position) -> Self {
@@ -377,9 +382,7 @@ impl<'a> Block<'a> {
/// This example shows a `Block` with blue borders.
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// Block::default()
/// .borders(Borders::ALL)
/// .border_style(Style::new().blue());
/// Block::bordered().border_style(Style::new().blue());
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn border_style<S: Into<Style>>(mut self, style: S) -> Self {
@@ -409,17 +412,13 @@ impl<'a> Block<'a> {
///
/// # Examples
///
/// Simply show all borders.
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// Block::default().borders(Borders::ALL);
/// ```
///
/// Display left and right borders.
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// Block::default().borders(Borders::LEFT | Borders::RIGHT);
/// Block::new().borders(Borders::LEFT | Borders::RIGHT);
/// ```
///
/// To show all borders you can abbreviate this with [`Block::bordered`]
#[must_use = "method moves the value of self and returns the modified value"]
pub const fn borders(mut self, flag: Borders) -> Self {
self.borders = flag;
@@ -437,10 +436,9 @@ impl<'a> Block<'a> {
///
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// Block::default()
/// .title("Block")
/// .borders(Borders::ALL)
/// .border_type(BorderType::Rounded);
/// Block::bordered()
/// .border_type(BorderType::Rounded)
/// .title("Block");
/// // Renders
/// // ╭Block╮
/// // │ │
@@ -460,7 +458,7 @@ impl<'a> Block<'a> {
///
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// Block::default().title("Block").borders(Borders::ALL).border_set(symbols::border::DOUBLE);
/// Block::bordered().border_set(symbols::border::DOUBLE).title("Block");
/// // Renders
/// // ╔Block╗
/// // ║ ║
@@ -479,8 +477,8 @@ impl<'a> Block<'a> {
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// # fn render_nested_block(frame: &mut Frame) {
/// let outer_block = Block::default().title("Outer").borders(Borders::ALL);
/// let inner_block = Block::default().title("Inner").borders(Borders::ALL);
/// let outer_block = Block::bordered().title("Outer");
/// let inner_block = Block::bordered().title("Inner");
///
/// let outer_area = frame.size();
/// let inner_area = outer_block.inner(outer_area);
@@ -501,14 +499,14 @@ impl<'a> Block<'a> {
inner.x = inner.x.saturating_add(1).min(inner.right());
inner.width = inner.width.saturating_sub(1);
}
if self.borders.intersects(Borders::TOP) || self.have_title_at_position(Position::Top) {
if self.borders.intersects(Borders::TOP) || self.has_title_at_position(Position::Top) {
inner.y = inner.y.saturating_add(1).min(inner.bottom());
inner.height = inner.height.saturating_sub(1);
}
if self.borders.intersects(Borders::RIGHT) {
inner.width = inner.width.saturating_sub(1);
}
if self.borders.intersects(Borders::BOTTOM) || self.have_title_at_position(Position::Bottom)
if self.borders.intersects(Borders::BOTTOM) || self.has_title_at_position(Position::Bottom)
{
inner.height = inner.height.saturating_sub(1);
}
@@ -526,7 +524,7 @@ impl<'a> Block<'a> {
inner
}
fn have_title_at_position(&self, position: Position) -> bool {
fn has_title_at_position(&self, position: Position) -> bool {
self.titles
.iter()
.any(|title| title.position.unwrap_or(self.titles_position) == position)
@@ -541,9 +539,7 @@ impl<'a> Block<'a> {
/// This renders a `Block` with no padding (the default).
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// Block::default()
/// .borders(Borders::ALL)
/// .padding(Padding::zero());
/// Block::bordered().padding(Padding::zero());
/// // Renders
/// // ┌───────┐
/// // │content│
@@ -554,9 +550,7 @@ impl<'a> Block<'a> {
/// Notice the two spaces before and after the content.
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// Block::default()
/// .borders(Borders::ALL)
/// .padding(Padding::horizontal(2));
/// Block::bordered().padding(Padding::horizontal(2));
/// // Renders
/// // ┌───────────┐
/// // │ content │
@@ -866,10 +860,10 @@ impl<'a> Styled for Block<'a> {
#[cfg(test)]
mod tests {
use rstest::rstest;
use strum::ParseError;
use super::*;
use crate::assert_buffer_eq;
#[test]
fn create_with_all_borders() {
@@ -877,234 +871,112 @@ mod tests {
assert_eq!(block.borders, Borders::all());
}
#[allow(clippy::too_many_lines)]
#[test]
fn inner_takes_into_account_the_borders() {
// No borders
assert_eq!(
Block::default().inner(Rect::default()),
Rect::new(0, 0, 0, 0),
"no borders, width=0, height=0"
);
assert_eq!(
Block::default().inner(Rect::new(0, 0, 1, 1)),
Rect::new(0, 0, 1, 1),
"no borders, width=1, height=1"
);
#[rstest]
#[case::none_0(Borders::NONE, Rect::ZERO, Rect::ZERO)]
#[case::none_1(Borders::NONE, Rect::new(0, 0, 1, 1), Rect::new(0, 0, 1, 1))]
#[case::left_0(Borders::LEFT, Rect::ZERO, Rect::ZERO)]
#[case::left_w1(Borders::LEFT, Rect::new(0, 0, 0, 1), Rect::new(0, 0, 0, 1))]
#[case::left_w2(Borders::LEFT, Rect::new(0, 0, 1, 1), Rect::new(1, 0, 0, 1))]
#[case::left_w3(Borders::LEFT, Rect::new(0, 0, 2, 1), Rect::new(1, 0, 1, 1))]
#[case::top_0(Borders::TOP, Rect::ZERO, Rect::ZERO)]
#[case::top_h1(Borders::TOP, Rect::new(0, 0, 1, 0), Rect::new(0, 0, 1, 0))]
#[case::top_h2(Borders::TOP, Rect::new(0, 0, 1, 1), Rect::new(0, 1, 1, 0))]
#[case::top_h3(Borders::TOP, Rect::new(0, 0, 1, 2), Rect::new(0, 1, 1, 1))]
#[case::right_0(Borders::RIGHT, Rect::ZERO, Rect::ZERO)]
#[case::right_w1(Borders::RIGHT, Rect::new(0, 0, 0, 1), Rect::new(0, 0, 0, 1))]
#[case::right_w2(Borders::RIGHT, Rect::new(0, 0, 1, 1), Rect::new(0, 0, 0, 1))]
#[case::right_w3(Borders::RIGHT, Rect::new(0, 0, 2, 1), Rect::new(0, 0, 1, 1))]
#[case::bottom_0(Borders::BOTTOM, Rect::ZERO, Rect::ZERO)]
#[case::bottom_h1(Borders::BOTTOM, Rect::new(0, 0, 1, 0), Rect::new(0, 0, 1, 0))]
#[case::bottom_h2(Borders::BOTTOM, Rect::new(0, 0, 1, 1), Rect::new(0, 0, 1, 0))]
#[case::bottom_h3(Borders::BOTTOM, Rect::new(0, 0, 1, 2), Rect::new(0, 0, 1, 1))]
#[case::all_0(Borders::ALL, Rect::ZERO, Rect::ZERO)]
#[case::all_1(Borders::ALL, Rect::new(0, 0, 1, 1), Rect::new(1, 1, 0, 0))]
#[case::all_2(Borders::ALL, Rect::new(0, 0, 2, 2), Rect::new(1, 1, 0, 0))]
#[case::all_3(Borders::ALL, Rect::new(0, 0, 3, 3), Rect::new(1, 1, 1, 1))]
fn inner_takes_into_account_the_borders(
#[case] borders: Borders,
#[case] area: Rect,
#[case] expected: Rect,
) {
let block = Block::new().borders(borders);
assert_eq!(block.inner(area), expected);
}
// Left border
assert_eq!(
Block::default()
.borders(Borders::LEFT)
.inner(Rect::new(0, 0, 0, 1)),
Rect::new(0, 0, 0, 1),
"left, width=0"
);
assert_eq!(
Block::default()
.borders(Borders::LEFT)
.inner(Rect::new(0, 0, 1, 1)),
Rect::new(1, 0, 0, 1),
"left, width=1"
);
assert_eq!(
Block::default()
.borders(Borders::LEFT)
.inner(Rect::new(0, 0, 2, 1)),
Rect::new(1, 0, 1, 1),
"left, width=2"
);
#[rstest]
#[case::left(Alignment::Left)]
#[case::center(Alignment::Center)]
#[case::right(Alignment::Right)]
fn inner_takes_into_account_the_title(#[case] alignment: Alignment) {
let area = Rect::new(0, 0, 0, 1);
let expected = Rect::new(0, 1, 0, 0);
// Top border
assert_eq!(
Block::default()
.borders(Borders::TOP)
.inner(Rect::new(0, 0, 1, 0)),
Rect::new(0, 0, 1, 0),
"top, height=0"
);
assert_eq!(
Block::default()
.borders(Borders::TOP)
.inner(Rect::new(0, 0, 1, 1)),
Rect::new(0, 1, 1, 0),
"top, height=1"
);
assert_eq!(
Block::default()
.borders(Borders::TOP)
.inner(Rect::new(0, 0, 1, 2)),
Rect::new(0, 1, 1, 1),
"top, height=2"
);
let block = Block::new().title(Title::from("Test").alignment(alignment));
assert_eq!(block.inner(area), expected);
}
// Right border
assert_eq!(
Block::default()
.borders(Borders::RIGHT)
.inner(Rect::new(0, 0, 0, 1)),
Rect::new(0, 0, 0, 1),
"right, width=0"
);
assert_eq!(
Block::default()
.borders(Borders::RIGHT)
.inner(Rect::new(0, 0, 1, 1)),
Rect::new(0, 0, 0, 1),
"right, width=1"
);
assert_eq!(
Block::default()
.borders(Borders::RIGHT)
.inner(Rect::new(0, 0, 2, 1)),
Rect::new(0, 0, 1, 1),
"right, width=2"
);
// Bottom border
assert_eq!(
Block::default()
.borders(Borders::BOTTOM)
.inner(Rect::new(0, 0, 1, 0)),
Rect::new(0, 0, 1, 0),
"bottom, height=0"
);
assert_eq!(
Block::default()
.borders(Borders::BOTTOM)
.inner(Rect::new(0, 0, 1, 1)),
Rect::new(0, 0, 1, 0),
"bottom, height=1"
);
assert_eq!(
Block::default()
.borders(Borders::BOTTOM)
.inner(Rect::new(0, 0, 1, 2)),
Rect::new(0, 0, 1, 1),
"bottom, height=2"
);
// All borders
assert_eq!(
Block::default()
.borders(Borders::ALL)
.inner(Rect::default()),
Rect::new(0, 0, 0, 0),
"all borders, width=0, height=0"
);
assert_eq!(
Block::default()
.borders(Borders::ALL)
.inner(Rect::new(0, 0, 1, 1)),
Rect::new(1, 1, 0, 0),
"all borders, width=1, height=1"
);
assert_eq!(
Block::default()
.borders(Borders::ALL)
.inner(Rect::new(0, 0, 2, 2)),
Rect::new(1, 1, 0, 0),
"all borders, width=2, height=2"
);
assert_eq!(
Block::default()
.borders(Borders::ALL)
.inner(Rect::new(0, 0, 3, 3)),
Rect::new(1, 1, 1, 1),
"all borders, width=3, height=3"
);
#[rstest]
#[case::top_top(Borders::TOP, Position::Top, Rect::new(0, 1, 0, 1))]
#[case::top_bot(Borders::BOTTOM, Position::Top, Rect::new(0, 1, 0, 0))]
#[case::bot_top(Borders::TOP, Position::Bottom, Rect::new(0, 1, 0, 0))]
#[case::top_top(Borders::BOTTOM, Position::Bottom, Rect::new(0, 0, 0, 1))]
fn inner_takes_into_account_border_and_title(
#[case] borders: Borders,
#[case] position: Position,
#[case] expected: Rect,
) {
let area = Rect::new(0, 0, 0, 2);
let block = Block::new()
.borders(borders)
.title(Title::from("Test").position(position));
assert_eq!(block.inner(area), expected);
}
#[test]
fn inner_takes_into_account_the_title() {
assert_eq!(
Block::default().title("Test").inner(Rect::new(0, 0, 0, 1)),
Rect::new(0, 1, 0, 0),
);
assert_eq!(
Block::default()
.title(Title::from("Test").alignment(Alignment::Center))
.inner(Rect::new(0, 0, 0, 1)),
Rect::new(0, 1, 0, 0),
);
assert_eq!(
Block::default()
.title(Title::from("Test").alignment(Alignment::Right))
.inner(Rect::new(0, 0, 0, 1)),
Rect::new(0, 1, 0, 0),
);
}
fn has_title_at_position_takes_into_account_all_positioning_declarations() {
let block = Block::new();
assert!(!block.has_title_at_position(Position::Top));
assert!(!block.has_title_at_position(Position::Bottom));
#[test]
fn inner_takes_into_account_border_and_title() {
let test_rect = Rect::new(0, 0, 0, 2);
let block = Block::new().title(Title::from("Test").position(Position::Top));
assert!(block.has_title_at_position(Position::Top));
assert!(!block.has_title_at_position(Position::Bottom));
let top_top = Block::default()
.title(Title::from("Test").position(Position::Top))
.borders(Borders::TOP);
assert_eq!(top_top.inner(test_rect), Rect::new(0, 1, 0, 1));
let block = Block::new().title(Title::from("Test").position(Position::Bottom));
assert!(!block.has_title_at_position(Position::Top));
assert!(block.has_title_at_position(Position::Bottom));
let top_bot = Block::default()
.title(Title::from("Test").position(Position::Top))
.borders(Borders::BOTTOM);
assert_eq!(top_bot.inner(test_rect), Rect::new(0, 1, 0, 0));
let bot_top = Block::default()
.title(Title::from("Test").position(Position::Bottom))
.borders(Borders::TOP);
assert_eq!(bot_top.inner(test_rect), Rect::new(0, 1, 0, 0));
let bot_bot = Block::default()
.title(Title::from("Test").position(Position::Bottom))
.borders(Borders::BOTTOM);
assert_eq!(bot_bot.inner(test_rect), Rect::new(0, 0, 0, 1));
}
#[test]
fn have_title_at_position_takes_into_account_all_positioning_declarations() {
let block = Block::default();
assert!(!block.have_title_at_position(Position::Top));
assert!(!block.have_title_at_position(Position::Bottom));
let block = Block::default().title(Title::from("Test").position(Position::Top));
assert!(block.have_title_at_position(Position::Top));
assert!(!block.have_title_at_position(Position::Bottom));
let block = Block::default().title(Title::from("Test").position(Position::Bottom));
assert!(!block.have_title_at_position(Position::Top));
assert!(block.have_title_at_position(Position::Bottom));
let block = Block::default()
let block = Block::new()
.title(Title::from("Test").position(Position::Top))
.title_position(Position::Bottom);
assert!(block.have_title_at_position(Position::Top));
assert!(!block.have_title_at_position(Position::Bottom));
assert!(block.has_title_at_position(Position::Top));
assert!(!block.has_title_at_position(Position::Bottom));
let block = Block::default()
let block = Block::new()
.title(Title::from("Test").position(Position::Bottom))
.title_position(Position::Top);
assert!(!block.have_title_at_position(Position::Top));
assert!(block.have_title_at_position(Position::Bottom));
assert!(!block.has_title_at_position(Position::Top));
assert!(block.has_title_at_position(Position::Bottom));
let block = Block::default()
let block = Block::new()
.title(Title::from("Test").position(Position::Top))
.title(Title::from("Test").position(Position::Bottom));
assert!(block.have_title_at_position(Position::Top));
assert!(block.have_title_at_position(Position::Bottom));
assert!(block.has_title_at_position(Position::Top));
assert!(block.has_title_at_position(Position::Bottom));
let block = Block::default()
let block = Block::new()
.title(Title::from("Test").position(Position::Top))
.title(Title::from("Test"))
.title_position(Position::Bottom);
assert!(block.have_title_at_position(Position::Top));
assert!(block.have_title_at_position(Position::Bottom));
assert!(block.has_title_at_position(Position::Top));
assert!(block.has_title_at_position(Position::Bottom));
let block = Block::default()
let block = Block::new()
.title(Title::from("Test"))
.title(Title::from("Test").position(Position::Bottom))
.title_position(Position::Top);
assert!(block.have_title_at_position(Position::Top));
assert!(block.have_title_at_position(Position::Bottom));
assert!(block.has_title_at_position(Position::Top));
assert!(block.has_title_at_position(Position::Bottom));
}
#[test]
@@ -1134,50 +1006,49 @@ mod tests {
const fn block_can_be_const() {
const _DEFAULT_STYLE: Style = Style::new();
const _DEFAULT_PADDING: Padding = Padding::uniform(1);
const _DEFAULT_BLOCK: Block = Block::new()
const _DEFAULT_BLOCK: Block = Block::bordered()
// the following methods are no longer const because they use Into<Style>
// .style(_DEFAULT_STYLE) // no longer const
// .border_style(_DEFAULT_STYLE) // no longer const
// .title_style(_DEFAULT_STYLE) // no longer const
.title_alignment(Alignment::Left)
.title_position(Position::Top)
.borders(Borders::ALL)
.padding(_DEFAULT_PADDING);
}
/// This test ensures that we have some coverage on the [`Style::from()`] implementations
/// Ensure Style from/into works the way a user would use it.
#[test]
fn block_style() {
fn style_into_works_from_user_view() {
// nominal style
let block = Block::default().style(Style::new().red());
let block = Block::new().style(Style::new().red());
assert_eq!(block.style, Style::new().red());
// auto-convert from Color
let block = Block::default().style(Color::Red);
let block = Block::new().style(Color::Red);
assert_eq!(block.style, Style::new().red());
// auto-convert from (Color, Color)
let block = Block::default().style((Color::Red, Color::Blue));
let block = Block::new().style((Color::Red, Color::Blue));
assert_eq!(block.style, Style::new().red().on_blue());
// auto-convert from Modifier
let block = Block::default().style(Modifier::BOLD | Modifier::ITALIC);
let block = Block::new().style(Modifier::BOLD | Modifier::ITALIC);
assert_eq!(block.style, Style::new().bold().italic());
// auto-convert from (Modifier, Modifier)
let block = Block::default().style((Modifier::BOLD | Modifier::ITALIC, Modifier::DIM));
let block = Block::new().style((Modifier::BOLD | Modifier::ITALIC, Modifier::DIM));
assert_eq!(block.style, Style::new().bold().italic().not_dim());
// auto-convert from (Color, Modifier)
let block = Block::default().style((Color::Red, Modifier::BOLD));
let block = Block::new().style((Color::Red, Modifier::BOLD));
assert_eq!(block.style, Style::new().red().bold());
// auto-convert from (Color, Color, Modifier)
let block = Block::default().style((Color::Red, Color::Blue, Modifier::BOLD));
let block = Block::new().style((Color::Red, Color::Blue, Modifier::BOLD));
assert_eq!(block.style, Style::new().red().on_blue().bold());
// auto-convert from (Color, Color, Modifier, Modifier)
let block = Block::default().style((
let block = Block::new().style((
Color::Red,
Color::Blue,
Modifier::BOLD | Modifier::ITALIC,
@@ -1191,7 +1062,7 @@ mod tests {
#[test]
fn can_be_stylized() {
let block = Block::default().black().on_white().bold().not_dim();
let block = Block::new().black().on_white().bold().not_dim();
assert_eq!(
block.style,
Style::default()
@@ -1206,7 +1077,7 @@ mod tests {
fn title() {
use Alignment::*;
use Position::*;
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
let mut buffer = Buffer::empty(Rect::new(0, 0, 11, 3));
Block::bordered()
.title(Title::from("A").position(Top).alignment(Left))
.title(Title::from("B").position(Top).alignment(Center))
@@ -1215,19 +1086,18 @@ mod tests {
.title(Title::from("E").position(Bottom).alignment(Center))
.title(Title::from("F").position(Bottom).alignment(Right))
.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
"┌A─────B─────C┐",
"│ │",
"└D─────E─────F┘",
])
);
#[rustfmt::skip]
let expected = Buffer::with_lines([
"┌A───B───C┐",
"│ │",
"└D───E───F┘",
]);
assert_eq!(buffer, expected);
}
#[test]
fn title_top_bottom() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
let mut buffer = Buffer::empty(Rect::new(0, 0, 11, 3));
Block::bordered()
.title_top(Line::raw("A").left_aligned())
.title_top(Line::raw("B").centered())
@@ -1236,14 +1106,13 @@ mod tests {
.title_bottom(Line::raw("E").centered())
.title_bottom(Line::raw("F").right_aligned())
.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
"┌A─────B─────C┐",
"│ │",
"└D─────E─────F┘",
])
);
#[rustfmt::skip]
let expected = Buffer::with_lines([
"┌A───B───C┐",
"│ │",
"└D───E───F┘",
]);
assert_eq!(buffer, expected);
}
#[test]
@@ -1255,11 +1124,11 @@ mod tests {
];
for (alignment, expected) in tests {
let mut buffer = Buffer::empty(Rect::new(0, 0, 8, 1));
Block::default()
.title("test")
Block::new()
.title_alignment(alignment)
.title("test")
.render(buffer.area, &mut buffer);
assert_buffer_eq!(buffer, Buffer::with_lines(vec![expected]));
assert_eq!(buffer, Buffer::with_lines([expected]));
}
}
@@ -1272,11 +1141,11 @@ mod tests {
];
for (block_title_alignment, alignment, expected) in tests {
let mut buffer = Buffer::empty(Rect::new(0, 0, 8, 1));
Block::default()
.title(Title::from("test").alignment(alignment))
Block::new()
.title_alignment(block_title_alignment)
.title(Title::from("test").alignment(alignment))
.render(buffer.area, &mut buffer);
assert_buffer_eq!(buffer, Buffer::with_lines(vec![expected]));
assert_eq!(buffer, Buffer::with_lines([expected]));
}
}
@@ -1284,43 +1153,32 @@ mod tests {
#[test]
fn render_right_aligned_empty_title() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
Block::default()
.title("")
Block::new()
.title_alignment(Alignment::Right)
.title("")
.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
" ",
" ",
" ",
])
);
assert_eq!(buffer, Buffer::with_lines([" "; 3]));
}
#[test]
fn title_position() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 2));
Block::default()
.title("test")
Block::new()
.title_position(Position::Bottom)
.title("test")
.render(buffer.area, &mut buffer);
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" ", "test"]));
assert_eq!(buffer, Buffer::with_lines([" ", "test"]));
}
#[test]
fn title_content_style() {
for alignment in [Alignment::Left, Alignment::Center, Alignment::Right] {
let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 1));
Block::default()
.title("test".yellow())
Block::new()
.title_alignment(alignment)
.title("test".yellow())
.render(buffer.area, &mut buffer);
let mut expected_buffer = Buffer::with_lines(vec!["test"]);
expected_buffer.set_style(Rect::new(0, 0, 4, 1), Style::new().yellow());
assert_buffer_eq!(buffer, expected_buffer);
assert_eq!(buffer, Buffer::with_lines(["test".yellow()]));
}
}
@@ -1328,16 +1186,12 @@ mod tests {
fn block_title_style() {
for alignment in [Alignment::Left, Alignment::Center, Alignment::Right] {
let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 1));
Block::default()
.title("test")
.title_style(Style::new().yellow())
Block::new()
.title_alignment(alignment)
.title_style(Style::new().yellow())
.title("test")
.render(buffer.area, &mut buffer);
let mut expected_buffer = Buffer::with_lines(vec!["test"]);
expected_buffer.set_style(Rect::new(0, 0, 4, 1), Style::new().yellow());
assert_buffer_eq!(buffer, expected_buffer);
assert_eq!(buffer, Buffer::with_lines(["test".yellow()]));
}
}
@@ -1345,37 +1199,31 @@ mod tests {
fn title_style_overrides_block_title_style() {
for alignment in [Alignment::Left, Alignment::Center, Alignment::Right] {
let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 1));
Block::default()
.title("test".yellow())
.title_style(Style::new().green().on_red())
Block::new()
.title_alignment(alignment)
.title_style(Style::new().green().on_red())
.title("test".yellow())
.render(buffer.area, &mut buffer);
let mut expected_buffer = Buffer::with_lines(vec!["test"]);
expected_buffer.set_style(Rect::new(0, 0, 4, 1), Style::new().yellow().on_red());
assert_buffer_eq!(buffer, expected_buffer);
assert_eq!(buffer, Buffer::with_lines(["test".yellow().on_red()]));
}
}
#[test]
fn title_border_style() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
Block::default()
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
Block::bordered()
.title("test")
.borders(Borders::ALL)
.border_style(Style::new().yellow())
.render(buffer.area, &mut buffer);
let mut expected_buffer = Buffer::with_lines(vec![
"┌test─────────",
" ",
"└─────────────",
#[rustfmt::skip]
let mut expected = Buffer::with_lines([
"┌test────┐",
"│ │",
"└────────┘",
]);
expected_buffer.set_style(Rect::new(0, 0, 15, 3), Style::new().yellow());
expected_buffer.set_style(Rect::new(1, 1, 13, 1), Style::reset());
assert_buffer_eq!(buffer, expected_buffer);
expected.set_style(Rect::new(0, 0, 10, 3), Style::new().yellow());
expected.set_style(Rect::new(1, 1, 8, 1), Style::reset());
assert_eq!(buffer, expected);
}
#[test]
@@ -1397,111 +1245,98 @@ mod tests {
#[test]
fn render_plain_border() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
Block::default()
.borders(Borders::ALL)
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
Block::bordered()
.border_type(BorderType::Plain)
.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
"┌─────────────┐",
"│ │",
"└─────────────┘"
])
);
#[rustfmt::skip]
let expected = Buffer::with_lines([
"┌────────┐",
"│ │",
"└────────┘",
]);
assert_eq!(buffer, expected);
}
#[test]
fn render_rounded_border() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
Block::default()
.borders(Borders::ALL)
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
Block::bordered()
.border_type(BorderType::Rounded)
.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
"╭─────────────╮",
"│ │",
"╰─────────────╯"
])
);
#[rustfmt::skip]
let expected = Buffer::with_lines([
"╭────────╮",
"│ │",
"╰────────╯",
]);
assert_eq!(buffer, expected);
}
#[test]
fn render_double_border() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
Block::default()
.borders(Borders::ALL)
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
Block::bordered()
.border_type(BorderType::Double)
.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
"╔═════════════╗",
"║ ║",
"╚═════════════╝"
])
);
#[rustfmt::skip]
let expected = Buffer::with_lines([
"╔════════╗",
"║ ║",
"╚════════╝",
]);
assert_eq!(buffer, expected);
}
#[test]
fn render_quadrant_inside() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
Block::default()
.borders(Borders::ALL)
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
Block::bordered()
.border_type(BorderType::QuadrantInside)
.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
"▗▄▄▄▄▄▄▄▄▄▄▄▄▄▖",
"▐ ▌",
"▝▀▀▀▀▀▀▀▀▀▀▀▀▀▘",
])
);
#[rustfmt::skip]
let expected = Buffer::with_lines([
"▗▄▄▄▄▄▄▄▄▖",
"▐ ▌",
"▝▀▀▀▀▀▀▀▀▘",
]);
assert_eq!(buffer, expected);
}
#[test]
fn render_border_quadrant_outside() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
Block::default()
.borders(Borders::ALL)
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
Block::bordered()
.border_type(BorderType::QuadrantOutside)
.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
"▛▀▀▀▀▀▀▀▀▀▀▀▀▀▜",
"▌ ▐",
"▙▄▄▄▄▄▄▄▄▄▄▄▄▄▟",
])
);
#[rustfmt::skip]
let expected = Buffer::with_lines([
"▛▀▀▀▀▀▀▀▀▜",
"▌ ▐",
"▙▄▄▄▄▄▄▄▄▟",
]);
assert_eq!(buffer, expected);
}
#[test]
fn render_solid_border() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
Block::default()
.borders(Borders::ALL)
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
Block::bordered()
.border_type(BorderType::Thick)
.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
"┏━━━━━━━━━━━━━┓",
"┃ ┃",
"┗━━━━━━━━━━━━━┛"
])
);
#[rustfmt::skip]
let expected = Buffer::with_lines([
"┏━━━━━━━━┓",
"┃ ┃",
"┗━━━━━━━━┛",
]);
assert_eq!(buffer, expected);
}
#[test]
fn render_custom_border_set() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
Block::default()
.borders(Borders::ALL)
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
Block::bordered()
.border_set(border::Set {
top_left: "1",
top_right: "2",
@@ -1513,13 +1348,12 @@ mod tests {
horizontal_bottom: "B",
})
.render(buffer.area, &mut buffer);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
"1TTTTTTTTTTTTT2",
"L R",
"3BBBBBBBBBBBBB4",
])
);
#[rustfmt::skip]
let expected = Buffer::with_lines([
"1TTTTTTTT2",
"L R",
"3BBBBBBBB4",
]);
assert_eq!(buffer, expected);
}
}

View File

@@ -1,4 +1,4 @@
use std::fmt::{self, Debug};
use std::fmt;
use bitflags::bitflags;
@@ -24,7 +24,7 @@ bitflags! {
/// Implement the `Debug` trait for the `Borders` bitflags. This is a manual implementation to
/// display the flags in a more readable way. The default implementation would display the
/// flags as 'Border(0x0)' for `Borders::NONE` for example.
impl Debug for Borders {
impl fmt::Debug for Borders {
/// Display the Borders bitflags as a list of names. For example, `Borders::NONE` will be
/// displayed as `NONE` and `Borders::ALL` will be displayed as `ALL`. If multiple flags are
/// set, they will be displayed separated by a pipe character.

View File

@@ -28,14 +28,14 @@ pub struct Monthly<'a, DS: DateStyler> {
impl<'a, DS: DateStyler> Monthly<'a, DS> {
/// Construct a calendar for the `display_date` and highlight the `events`
pub fn new(display_date: Date, events: DS) -> Self {
pub const fn new(display_date: Date, events: DS) -> Self {
Self {
display_date,
events,
show_surrounding: None,
show_weekday: None,
show_month: None,
default_style: Style::default(),
default_style: Style::new(),
block: None,
}
}
@@ -90,10 +90,10 @@ impl<'a, DS: DateStyler> Monthly<'a, DS> {
}
/// Return a style with only the background from the default style
fn default_bg(&self) -> Style {
const fn default_bg(&self) -> Style {
match self.default_style.bg {
None => Style::default(),
Some(c) => Style::default().bg(c),
None => Style::new(),
Some(c) => Style::new().bg(c),
}
}
@@ -164,7 +164,7 @@ impl<DS: DateStyler> Monthly<'_, DS> {
let mut y = days_area.y;
// go through all the weeks containing a day in the target month.
while curr_day.month() as u8 != self.display_date.month().next() as u8 {
while curr_day.month() != self.display_date.month().next() {
let mut spans = Vec::with_capacity(14);
for i in 0..7 {
// Draw the gutter. Do it here so we can avoid worrying about

View File

@@ -19,7 +19,7 @@ mod points;
mod rectangle;
mod world;
use std::{fmt::Debug, iter::zip};
use std::{fmt, iter::zip};
use itertools::Itertools;
@@ -70,7 +70,7 @@ struct Layer {
/// resolution of the grid might exceed the number of rows and columns. For example, a grid of
/// Braille patterns will have a resolution of 2x4 dots per cell. This means that a grid of 10x10
/// cells will have a resolution of 20x40 dots.
trait Grid: Debug {
trait Grid: fmt::Debug {
/// Get the resolution of the grid in number of dots.
///
/// This doesn't have to be the same as the number of rows and columns of the grid. For example,
@@ -567,7 +567,7 @@ impl<'a> Context<'a> {
/// };
///
/// Canvas::default()
/// .block(Block::default().title("Canvas").borders(Borders::ALL))
/// .block(Block::bordered().title("Canvas"))
/// .x_bounds([-180.0, 180.0])
/// .y_bounds([-90.0, 90.0])
/// .paint(|ctx| {

View File

@@ -58,7 +58,7 @@ mod tests {
.x_bounds([-10.0, 10.0])
.y_bounds([-10.0, 10.0]);
canvas.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines(vec![
let expected = Buffer::with_lines([
" ⢀⣠⢤⣀ ",
" ⢰⠋ ⠈⣇",
" ⠘⣆⡀ ⣠⠇",

View File

@@ -112,154 +112,111 @@ fn draw_line_high(painter: &mut Painter, x1: usize, y1: usize, x2: usize, y2: us
#[cfg(test)]
mod tests {
use super::Line;
use crate::{assert_buffer_eq, prelude::*, widgets::canvas::Canvas};
use rstest::rstest;
#[allow(clippy::needless_pass_by_value)]
#[track_caller]
fn test(line: Line, expected_lines: Vec<&str>) {
use super::{super::*, *};
use crate::{buffer::Buffer, layout::Rect};
#[rstest]
#[case::off_grid(&Line::new(-1.0, -1.0, 10.0, 10.0, Color::Red), [" "; 10])]
#[case::off_grid(&Line::new(0.0, 0.0, 11.0, 11.0, Color::Red), [" "; 10])]
#[case::horizontal(&Line::new(0.0, 0.0, 10.0, 0.0, Color::Red), [
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
"••••••••••",
])]
#[case::horizontal(&Line::new(10.0, 10.0, 0.0, 10.0, Color::Red), [
"••••••••••",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
])]
#[case::vertical(&Line::new(0.0, 0.0, 0.0, 10.0, Color::Red), [""; 10])]
#[case::vertical(&Line::new(10.0, 10.0, 10.0, 0.0, Color::Red), [""; 10])]
// dy < dx, x1 < x2
#[case::diagonal(&Line::new(0.0, 0.0, 10.0, 5.0, Color::Red), [
" ",
" ",
" ",
" ",
"",
" •• ",
" •• ",
" •• ",
" •• ",
"",
])]
// dy < dx, x1 > x2
#[case::diagonal(&Line::new(10.0, 0.0, 0.0, 5.0, Color::Red), [
" ",
" ",
" ",
" ",
"",
" •• ",
" •• ",
" •• ",
" •• ",
"",
])]
// dy > dx, y1 < y2
#[case::diagonal(&Line::new(0.0, 0.0, 5.0, 10.0, Color::Red), [
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
])]
// dy > dx, y1 > y2
#[case::diagonal(&Line::new(0.0, 10.0, 5.0, 0.0, Color::Red), [
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
])]
fn tests<'expected_line, ExpectedLines>(#[case] line: &Line, #[case] expected: ExpectedLines)
where
ExpectedLines: IntoIterator,
ExpectedLines::Item: Into<crate::text::Line<'expected_line>>,
{
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 10));
let canvas = Canvas::default()
.marker(Marker::Dot)
.x_bounds([0.0, 10.0])
.y_bounds([0.0, 10.0])
.paint(|context| {
context.draw(&line);
});
.paint(|context| context.draw(line));
canvas.render(buffer.area, &mut buffer);
let mut expected = Buffer::with_lines(expected_lines);
let mut expected = Buffer::with_lines(expected);
for cell in &mut expected.content {
if cell.symbol() == "" {
cell.set_style(Style::new().red());
}
}
assert_buffer_eq!(buffer, expected);
}
#[test]
fn off_grid() {
test(
Line::new(-1.0, -1.0, 10.0, 10.0, Color::Red),
vec![" "; 10],
);
test(
Line::new(0.0, 0.0, 11.0, 11.0, Color::Red),
vec![" "; 10],
);
}
#[test]
fn horizontal() {
test(
Line::new(0.0, 0.0, 10.0, 0.0, Color::Red),
vec![
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
"••••••••••",
],
);
test(
Line::new(10.0, 10.0, 0.0, 10.0, Color::Red),
vec![
"••••••••••",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
],
);
}
#[test]
fn vertical() {
test(
Line::new(0.0, 0.0, 0.0, 10.0, Color::Red),
vec![""; 10],
);
test(
Line::new(10.0, 10.0, 10.0, 0.0, Color::Red),
vec![""; 10],
);
}
#[test]
fn diagonal() {
// dy < dx, x1 < x2
test(
Line::new(0.0, 0.0, 10.0, 5.0, Color::Red),
vec![
" ",
" ",
" ",
" ",
"",
" •• ",
" •• ",
" •• ",
" •• ",
"",
],
);
// dy < dx, x1 > x2
test(
Line::new(10.0, 0.0, 0.0, 5.0, Color::Red),
vec![
" ",
" ",
" ",
" ",
"",
" •• ",
" •• ",
" •• ",
" •• ",
"",
],
);
// dy > dx, y1 < y2
test(
Line::new(0.0, 0.0, 5.0, 10.0, Color::Red),
vec![
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
],
);
// dy > dx, y1 > y2
test(
Line::new(0.0, 10.0, 5.0, 0.0, Color::Red),
vec![
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
],
);
assert_eq!(buffer, expected);
}
}

View File

@@ -27,7 +27,7 @@ pub enum MapResolution {
}
impl MapResolution {
fn data(self) -> &'static [(f64, f64)] {
const fn data(self) -> &'static [(f64, f64)] {
match self {
Self::Low => &WORLD_LOW_RESOLUTION,
Self::High => &WORLD_HIGH_RESOLUTION,
@@ -65,7 +65,7 @@ mod tests {
use strum::ParseError;
use super::*;
use crate::{assert_buffer_eq, prelude::*, widgets::canvas::Canvas};
use crate::{prelude::*, widgets::canvas::Canvas};
#[test]
fn map_resolution_to_string() {
@@ -101,7 +101,7 @@ mod tests {
context.draw(&Map::default());
});
canvas.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines(vec![
let expected = Buffer::with_lines([
" ",
" ••••••• •• •• •• • ",
" •••••••••••••• ••• •••• ••• •• •••• ",
@@ -143,7 +143,7 @@ mod tests {
"",
" ",
]);
assert_buffer_eq!(buffer, expected);
assert_eq!(buffer, expected);
}
#[test]
@@ -160,7 +160,7 @@ mod tests {
});
});
canvas.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines(vec![
let expected = Buffer::with_lines([
" ",
" ⢀⣠⠤⠤⠤⠔⢤⣤⡄⠤⡠⣄⠢⠂⢢⠰⣠⡄⣀⡀ ⣀ ",
" ⢀⣀⡤⣦⠲⢶⣿⣮⣿⡉⣰⢶⢏⡂ ⢀⣟⠁ ⢺⣻⢿⠏ ⠈⠉⠁ ⢀⣀ ⠈⠓⢳⣢⣂⡀ ",
@@ -202,6 +202,6 @@ mod tests {
"⠶⠔⠲⠤⠠⠜⢗⠤⠄ ⠘⠉ ⠁ ⠈⠉⠒⠔⠤",
" ",
]);
assert_buffer_eq!(buffer, expected);
assert_eq!(buffer, expected);
}
}

View File

@@ -66,7 +66,7 @@ impl Shape for Rectangle {
#[cfg(test)]
mod tests {
use super::*;
use crate::{assert_buffer_eq, prelude::*, widgets::canvas::Canvas};
use crate::{prelude::*, widgets::canvas::Canvas};
#[test]
fn draw_block_lines() {
@@ -85,7 +85,7 @@ mod tests {
});
});
canvas.render(buffer.area, &mut buffer);
let mut expected = Buffer::with_lines(vec![
let mut expected = Buffer::with_lines([
"██████████",
"█ █",
"█ █",
@@ -99,7 +99,7 @@ mod tests {
]);
expected.set_style(buffer.area, Style::new().red());
expected.set_style(buffer.area.inner(&Margin::new(1, 1)), Style::reset());
assert_buffer_eq!(buffer, expected);
assert_eq!(buffer, expected);
}
#[test]
@@ -119,7 +119,7 @@ mod tests {
});
});
canvas.render(buffer.area, &mut buffer);
let mut expected = Buffer::with_lines(vec![
let mut expected = Buffer::with_lines([
"█▀▀▀▀▀▀▀▀█",
"█ █",
"█ █",
@@ -134,7 +134,7 @@ mod tests {
expected.set_style(buffer.area, Style::new().red().on_red());
expected.set_style(buffer.area.inner(&Margin::new(1, 0)), Style::reset().red());
expected.set_style(buffer.area.inner(&Margin::new(1, 1)), Style::reset());
assert_buffer_eq!(buffer, expected);
assert_eq!(buffer, expected);
}
#[test]
@@ -163,7 +163,7 @@ mod tests {
});
});
canvas.render(buffer.area, &mut buffer);
let mut expected = Buffer::with_lines(vec![
let mut expected = Buffer::with_lines([
"⡏⠉⠉⠉⠉⠉⠉⠉⠉⢹",
"⡇⢠⠤⠤⠤⠤⠤⠤⡄⢸",
"⡇⢸ ⡇⢸",
@@ -178,6 +178,6 @@ mod tests {
expected.set_style(buffer.area, Style::new().red());
expected.set_style(buffer.area.inner(&Margin::new(1, 1)), Style::new().green());
expected.set_style(buffer.area.inner(&Margin::new(2, 2)), Style::reset());
assert_buffer_eq!(buffer, expected);
assert_eq!(buffer, expected);
}
}

View File

@@ -1,5 +1,5 @@
/// [Source data](http://www.gnuplotting.org/plotting-the-world-revisited)
pub static WORLD_HIGH_RESOLUTION: [(f64, f64); 5125] = [
pub const WORLD_HIGH_RESOLUTION: [(f64, f64); 5125] = [
(-163.7128, -78.5956),
(-163.1058, -78.2233),
(-161.2451, -78.3801),
@@ -5127,7 +5127,7 @@ pub static WORLD_HIGH_RESOLUTION: [(f64, f64); 5125] = [
(180.0, -84.71338),
];
pub static WORLD_LOW_RESOLUTION: [(f64, f64); 1166] = [
pub const WORLD_LOW_RESOLUTION: [(f64, f64); 1166] = [
(-92.32, 48.24),
(-88.13, 48.92),
(-83.11, 46.27),

View File

@@ -8,7 +8,7 @@ use crate::{
prelude::*,
widgets::{
canvas::{Canvas, Line as CanvasLine, Points},
Block, Borders,
Block,
},
};
@@ -475,7 +475,7 @@ struct ChartLayout {
///
/// // Create the chart and link all the parts together
/// let chart = Chart::new(datasets)
/// .block(Block::default().title("Chart"))
/// .block(Block::new().title("Chart"))
/// .x_axis(x_axis)
/// .y_axis(y_axis);
/// ```
@@ -1050,9 +1050,7 @@ impl WidgetRef for Chart<'_> {
if let Some(legend_area) = layout.legend_area {
buf.set_style(legend_area, original_style);
Block::default()
.borders(Borders::ALL)
.render(legend_area, buf);
Block::bordered().render(legend_area, buf);
for (i, (dataset_name, dataset_style)) in self
.datasets
@@ -1113,10 +1111,10 @@ impl<'a> Styled for Chart<'a> {
#[cfg(test)]
mod tests {
use rstest::rstest;
use strum::ParseError;
use super::*;
use crate::assert_buffer_eq;
struct LegendTestCase {
chart_area: Rect,
@@ -1211,7 +1209,6 @@ mod tests {
.x_axis(Axis::default().title("xxxxxxxxxxxxxxxx"));
let mut buffer = Buffer::empty(Rect::new(0, 0, 8, 4));
widget.render(buffer.area, &mut buffer);
assert_eq!(buffer, Buffer::with_lines(vec![" ".repeat(8); 4]));
}
@@ -1246,30 +1243,25 @@ mod tests {
let widget = Chart::new(vec![long_dataset_name, short_dataset])
.hidden_legend_constraints((100.into(), 100.into()));
let mut buffer = Buffer::empty(Rect::new(0, 0, 20, 5));
widget.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines(vec![
let expected = Buffer::with_lines([
" ┌──────────────┐",
" │Very long name│",
" │ Short name│",
" └──────────────┘",
" ",
]);
assert_buffer_eq!(buffer, expected);
assert_eq!(buffer, expected);
}
#[test]
fn test_chart_have_a_topleft_legend() {
let chart = Chart::new(vec![Dataset::default().name("Ds1")])
.legend_position(Some(LegendPosition::TopLeft));
let area = Rect::new(0, 0, 30, 20);
let mut buffer = Buffer::empty(area);
chart.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines(vec![
let expected = Buffer::with_lines([
"┌───┐ ",
"│Ds1│ ",
"└───┘ ",
@@ -1291,7 +1283,6 @@ mod tests {
" ",
" ",
]);
assert_eq!(buffer, expected);
}
@@ -1299,13 +1290,10 @@ mod tests {
fn test_chart_have_a_long_y_axis_title_overlapping_legend() {
let chart = Chart::new(vec![Dataset::default().name("Ds1")])
.y_axis(Axis::default().title("The title overlap a legend."));
let area = Rect::new(0, 0, 30, 20);
let mut buffer = Buffer::empty(area);
chart.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines(vec![
let expected = Buffer::with_lines([
"The title overlap a legend. ",
" ┌───┐",
" │Ds1│",
@@ -1327,7 +1315,6 @@ mod tests {
" ",
" ",
]);
assert_eq!(buffer, expected);
}
@@ -1335,13 +1322,10 @@ mod tests {
fn test_chart_have_overflowed_y_axis() {
let chart = Chart::new(vec![Dataset::default().name("Ds1")])
.y_axis(Axis::default().title("The title overlap a legend."));
let area = Rect::new(0, 0, 10, 10);
let mut buffer = Buffer::empty(area);
chart.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines(vec![
let expected = Buffer::with_lines([
" ",
" ",
" ",
@@ -1353,7 +1337,6 @@ mod tests {
" ",
" ",
]);
assert_eq!(buffer, expected);
}
@@ -1362,12 +1345,8 @@ mod tests {
let name = "Data";
let chart = Chart::new(vec![Dataset::default().name(name)])
.hidden_legend_constraints((Constraint::Percentage(100), Constraint::Percentage(100)));
let area = Rect::new(0, 0, name.len() as u16 + 2, 3);
let mut buffer = Buffer::empty(area);
let expected = Buffer::with_lines(vec!["┌────┐", "│Data│", "└────┘"]);
for position in [
LegendPosition::TopLeft,
LegendPosition::Top,
@@ -1381,171 +1360,103 @@ mod tests {
let chart = chart.clone().legend_position(Some(position));
buffer.reset();
chart.render(buffer.area, &mut buffer);
#[rustfmt::skip]
let expected = Buffer::with_lines([
"┌────┐",
"│Data│",
"└────┘",
]);
assert_eq!(buffer, expected);
}
}
#[allow(clippy::too_many_lines)]
#[test]
fn test_legend_of_chart_have_odd_margin_size() {
#[rstest]
#[case(Some(LegendPosition::TopLeft), [
"┌────┐ ",
"│Data│ ",
"└────┘ ",
" ",
" ",
" ",
])]
#[case(Some(LegendPosition::Top), [
" ┌────┐ ",
" │Data│ ",
" └────┘ ",
" ",
" ",
" ",
])]
#[case(Some(LegendPosition::TopRight), [
" ┌────┐",
" │Data│",
" └────┘",
" ",
" ",
" ",
])]
#[case(Some(LegendPosition::Left), [
" ",
"┌────┐ ",
"│Data│ ",
"└────┘ ",
" ",
" ",
])]
#[case(Some(LegendPosition::Right), [
" ",
" ┌────┐",
" │Data│",
" └────┘",
" ",
" ",
])]
#[case(Some(LegendPosition::BottomLeft), [
" ",
" ",
" ",
"┌────┐ ",
"│Data│ ",
"└────┘ ",
])]
#[case(Some(LegendPosition::Bottom), [
" ",
" ",
" ",
" ┌────┐ ",
" │Data│ ",
" └────┘ ",
])]
#[case(Some(LegendPosition::BottomRight), [
" ",
" ",
" ",
" ┌────┐",
" │Data│",
" └────┘",
])]
#[case(None, [
" ",
" ",
" ",
" ",
" ",
" ",
])]
fn test_legend_of_chart_have_odd_margin_size<'line, Lines>(
#[case] legend_position: Option<LegendPosition>,
#[case] expected: Lines,
) where
Lines: IntoIterator,
Lines::Item: Into<Line<'line>>,
{
let name = "Data";
let base_chart = Chart::new(vec![Dataset::default().name(name)])
.hidden_legend_constraints((Constraint::Percentage(100), Constraint::Percentage(100)));
let area = Rect::new(0, 0, name.len() as u16 + 2 + 3, 3 + 3);
let mut buffer = Buffer::empty(area);
let chart = base_chart
.clone()
.legend_position(Some(LegendPosition::TopLeft));
buffer.reset();
let chart = Chart::new(vec![Dataset::default().name(name)])
.legend_position(legend_position)
.hidden_legend_constraints((Constraint::Percentage(100), Constraint::Percentage(100)));
chart.render(buffer.area, &mut buffer);
assert_eq!(
buffer,
Buffer::with_lines(vec![
"┌────┐ ",
"│Data│ ",
"└────┘ ",
" ",
" ",
" ",
])
);
buffer.reset();
let chart = base_chart
.clone()
.legend_position(Some(LegendPosition::Top));
buffer.reset();
chart.render(buffer.area, &mut buffer);
assert_eq!(
buffer,
Buffer::with_lines(vec![
" ┌────┐ ",
" │Data│ ",
" └────┘ ",
" ",
" ",
" ",
])
);
let chart = base_chart
.clone()
.legend_position(Some(LegendPosition::TopRight));
buffer.reset();
chart.render(buffer.area, &mut buffer);
assert_eq!(
buffer,
Buffer::with_lines(vec![
" ┌────┐",
" │Data│",
" └────┘",
" ",
" ",
" ",
])
);
let chart = base_chart
.clone()
.legend_position(Some(LegendPosition::Left));
buffer.reset();
chart.render(buffer.area, &mut buffer);
assert_eq!(
buffer,
Buffer::with_lines(vec![
" ",
"┌────┐ ",
"│Data│ ",
"└────┘ ",
" ",
" ",
])
);
buffer.reset();
let chart = base_chart
.clone()
.legend_position(Some(LegendPosition::Right));
buffer.reset();
chart.render(buffer.area, &mut buffer);
assert_eq!(
buffer,
Buffer::with_lines(vec![
" ",
" ┌────┐",
" │Data│",
" └────┘",
" ",
" ",
])
);
let chart = base_chart
.clone()
.legend_position(Some(LegendPosition::BottomLeft));
buffer.reset();
chart.render(buffer.area, &mut buffer);
assert_eq!(
buffer,
Buffer::with_lines(vec![
" ",
" ",
" ",
"┌────┐ ",
"│Data│ ",
"└────┘ ",
])
);
let chart = base_chart
.clone()
.legend_position(Some(LegendPosition::Bottom));
buffer.reset();
chart.render(buffer.area, &mut buffer);
assert_eq!(
buffer,
Buffer::with_lines(vec![
" ",
" ",
" ",
" ┌────┐ ",
" │Data│ ",
" └────┘ ",
])
);
let chart = base_chart
.clone()
.legend_position(Some(LegendPosition::BottomRight));
buffer.reset();
chart.render(buffer.area, &mut buffer);
assert_eq!(
buffer,
Buffer::with_lines(vec![
" ",
" ",
" ",
" ┌────┐",
" │Data│",
" └────┘",
])
);
let chart = base_chart.clone().legend_position(None);
buffer.reset();
chart.render(buffer.area, &mut buffer);
assert_eq!(
buffer,
Buffer::with_lines(vec![
" ",
" ",
" ",
" ",
" ",
" ",
])
);
assert_eq!(buffer, Buffer::with_lines(expected));
}
}

View File

@@ -11,7 +11,7 @@ use crate::prelude::*;
/// use ratatui::{prelude::*, widgets::*};
///
/// fn draw_on_clear(f: &mut Frame, area: Rect) {
/// let block = Block::default().title("Block").borders(Borders::ALL);
/// let block = Block::bordered().title("Block");
/// f.render_widget(Clear, area); // <- this will clear/reset the area first
/// f.render_widget(block, area); // now render the block widget
/// }
@@ -43,24 +43,21 @@ impl WidgetRef for Clear {
#[cfg(test)]
mod tests {
use super::*;
use crate::assert_buffer_eq;
#[test]
fn render() {
let mut buf = Buffer::with_lines(vec!["xxxxxxxxxxxxxxx"; 7]);
let mut buffer = Buffer::with_lines(["xxxxxxxxxxxxxxx"; 7]);
let clear = Clear;
clear.render(Rect::new(1, 2, 3, 4), &mut buf);
assert_buffer_eq!(
buf,
Buffer::with_lines(vec![
"xxxxxxxxxxxxxxx",
"xxxxxxxxxxxxxxx",
"x xxxxxxxxxxx",
"x xxxxxxxxxxx",
"x xxxxxxxxxxx",
"x xxxxxxxxxxx",
"xxxxxxxxxxxxxxx",
])
);
clear.render(Rect::new(1, 2, 3, 4), &mut buffer);
let expected = Buffer::with_lines([
"xxxxxxxxxxxxxxx",
"xxxxxxxxxxxxxxx",
"x xxxxxxxxxxx",
"x xxxxxxxxxxx",
"x xxxxxxxxxxx",
"x xxxxxxxxxxx",
"xxxxxxxxxxxxxxx",
]);
assert_eq!(buffer, expected);
}
}

View File

@@ -17,7 +17,7 @@ use crate::{prelude::*, widgets::Block};
/// use ratatui::{prelude::*, widgets::*};
///
/// Gauge::default()
/// .block(Block::default().borders(Borders::ALL).title("Progress"))
/// .block(Block::bordered().title("Progress"))
/// .gauge_style(
/// Style::default()
/// .fg(Color::White)
@@ -30,6 +30,7 @@ use crate::{prelude::*, widgets::Block};
/// # See also
///
/// - [`LineGauge`] for a thin progress bar
#[allow(clippy::struct_field_names)] // gauge_style needs to be differentiated to style
#[derive(Debug, Clone, PartialEq)]
pub struct Gauge<'a> {
block: Option<Block<'a>>,
@@ -164,12 +165,12 @@ impl WidgetRef for Gauge<'_> {
buf.set_style(area, self.style);
self.block.render_ref(area, buf);
let inner = self.block.inner_if_some(area);
self.render_gague(inner, buf);
self.render_gauge(inner, buf);
}
}
impl Gauge<'_> {
fn render_gague(&self, gauge_area: Rect, buf: &mut Buffer) {
fn render_gauge(&self, gauge_area: Rect, buf: &mut Buffer) {
if gauge_area.is_empty() {
return;
}
@@ -249,7 +250,7 @@ fn get_unicode_block<'a>(frac: f64) -> &'a str {
/// use ratatui::{prelude::*, widgets::*};
///
/// LineGauge::default()
/// .block(Block::default().borders(Borders::ALL).title("Progress"))
/// .block(Block::bordered().title("Progress"))
/// .gauge_style(
/// Style::default()
/// .fg(Color::White)

File diff suppressed because it is too large Load Diff

View File

@@ -31,7 +31,7 @@ const fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Align
/// "Third line".into(),
/// ];
/// Paragraph::new(text)
/// .block(Block::new().title("Paragraph").borders(Borders::ALL))
/// .block(Block::bordered().title("Paragraph"))
/// .style(Style::new().white().on_black())
/// .alignment(Alignment::Center)
/// .wrap(Wrap { trim: true });
@@ -126,8 +126,7 @@ impl<'a> Paragraph<'a> {
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let paragraph = Paragraph::new("Hello, world!")
/// .block(Block::default().title("Paragraph").borders(Borders::ALL));
/// let paragraph = Paragraph::new("Hello, world!").block(Block::bordered().title("Paragraph"));
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn block(mut self, block: Block<'a>) -> Self {
@@ -256,6 +255,8 @@ impl<'a> Paragraph<'a> {
/// need in order to be fully rendered. For paragraphs that do not use wrapping, this count is
/// simply the number of lines present in the paragraph.
///
/// Note: The design for text wrapping is not stable and might affect this API.
///
/// # Example
///
/// ```ignore
@@ -267,7 +268,6 @@ impl<'a> Paragraph<'a> {
/// ```
#[stability::unstable(
feature = "rendered-line-info",
reason = "The design for text wrapping is not stable and might affect this API.",
issue = "https://github.com/ratatui-org/ratatui/issues/293"
)]
pub fn line_count(&self, width: u16) -> usize {
@@ -297,6 +297,8 @@ impl<'a> Paragraph<'a> {
/// Calculates the shortest line width needed to avoid any word being wrapped or truncated.
///
/// Note: The design for text wrapping is not stable and might affect this API.
///
/// # Example
///
/// ```ignore
@@ -309,7 +311,6 @@ impl<'a> Paragraph<'a> {
/// ```
#[stability::unstable(
feature = "rendered-line-info",
reason = "The design for text wrapping is not stable and might affect this API.",
issue = "https://github.com/ratatui-org/ratatui/issues/293"
)]
pub fn line_width(&self) -> usize {
@@ -413,73 +414,65 @@ mod test {
/// area and comparing the rendered and expected content.
/// This can be used for easy testing of varying configured paragraphs with the same expected
/// buffer or any other test case really.
#[allow(clippy::needless_pass_by_value)]
fn test_case(paragraph: &Paragraph, expected: Buffer) {
#[track_caller]
fn test_case(paragraph: &Paragraph, expected: &Buffer) {
let backend = TestBackend::new(expected.area.width, expected.area.height);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let size = f.size();
f.render_widget(paragraph.clone(), size);
})
.unwrap();
terminal.backend().assert_buffer(&expected);
terminal.backend().assert_buffer(expected);
}
#[test]
fn zero_width_char_at_end_of_line() {
let line = "foo\0";
let paragraphs = vec![
for paragraph in [
Paragraph::new(line),
Paragraph::new(line).wrap(Wrap { trim: false }),
Paragraph::new(line).wrap(Wrap { trim: true }),
];
for paragraph in paragraphs {
test_case(&paragraph, Buffer::with_lines(vec!["foo"]));
test_case(&paragraph, Buffer::with_lines(vec!["foo "]));
test_case(&paragraph, Buffer::with_lines(vec!["foo ", " "]));
test_case(&paragraph, Buffer::with_lines(vec!["foo", " "]));
] {
test_case(&paragraph, &Buffer::with_lines(["foo"]));
test_case(&paragraph, &Buffer::with_lines(["foo "]));
test_case(&paragraph, &Buffer::with_lines(["foo ", " "]));
test_case(&paragraph, &Buffer::with_lines(["foo", " "]));
}
}
#[test]
fn test_render_empty_paragraph() {
let paragraphs = vec![
for paragraph in [
Paragraph::new(""),
Paragraph::new("").wrap(Wrap { trim: false }),
Paragraph::new("").wrap(Wrap { trim: true }),
];
for paragraph in paragraphs {
test_case(&paragraph, Buffer::with_lines(vec![" "]));
test_case(&paragraph, Buffer::with_lines(vec![" "]));
test_case(&paragraph, Buffer::with_lines(vec![" "; 10]));
test_case(&paragraph, Buffer::with_lines(vec![" ", " "]));
] {
test_case(&paragraph, &Buffer::with_lines([" "]));
test_case(&paragraph, &Buffer::with_lines([" "]));
test_case(&paragraph, &Buffer::with_lines([" "; 10]));
test_case(&paragraph, &Buffer::with_lines([" ", " "]));
}
}
#[test]
fn test_render_single_line_paragraph() {
let text = "Hello, world!";
let truncated_paragraph = Paragraph::new(text);
let wrapped_paragraph = Paragraph::new(text).wrap(Wrap { trim: false });
let trimmed_paragraph = Paragraph::new(text).wrap(Wrap { trim: true });
let paragraphs = vec![&truncated_paragraph, &wrapped_paragraph, &trimmed_paragraph];
for paragraph in paragraphs {
test_case(paragraph, Buffer::with_lines(vec!["Hello, world! "]));
test_case(paragraph, Buffer::with_lines(vec!["Hello, world!"]));
for paragraph in [
Paragraph::new(text),
Paragraph::new(text).wrap(Wrap { trim: false }),
Paragraph::new(text).wrap(Wrap { trim: true }),
] {
test_case(&paragraph, &Buffer::with_lines(["Hello, world! "]));
test_case(&paragraph, &Buffer::with_lines(["Hello, world!"]));
test_case(
paragraph,
Buffer::with_lines(vec!["Hello, world! ", " "]),
&paragraph,
&Buffer::with_lines(["Hello, world! ", " "]),
);
test_case(
paragraph,
Buffer::with_lines(vec!["Hello, world!", " "]),
&paragraph,
&Buffer::with_lines(["Hello, world!", " "]),
);
}
}
@@ -487,29 +480,22 @@ mod test {
#[test]
fn test_render_multi_line_paragraph() {
let text = "This is a\nmultiline\nparagraph.";
let paragraphs = vec![
for paragraph in [
Paragraph::new(text),
Paragraph::new(text).wrap(Wrap { trim: false }),
Paragraph::new(text).wrap(Wrap { trim: true }),
];
for paragraph in paragraphs {
] {
test_case(
&paragraph,
Buffer::with_lines(vec!["This is a ", "multiline ", "paragraph."]),
&Buffer::with_lines(["This is a ", "multiline ", "paragraph."]),
);
test_case(
&paragraph,
Buffer::with_lines(vec![
"This is a ",
"multiline ",
"paragraph. ",
]),
&Buffer::with_lines(["This is a ", "multiline ", "paragraph. "]),
);
test_case(
&paragraph,
Buffer::with_lines(vec![
&Buffer::with_lines([
"This is a ",
"multiline ",
"paragraph. ",
@@ -525,17 +511,15 @@ mod test {
// We use the slightly unconventional "worlds" instead of "world" here to make sure when we
// can truncate this without triggering the typos linter.
let text = "Hello, worlds!";
let truncated_paragraph =
Paragraph::new(text).block(Block::default().title("Title").borders(Borders::ALL));
let truncated_paragraph = Paragraph::new(text).block(Block::bordered().title("Title"));
let wrapped_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: false });
let trimmed_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: true });
let paragraphs = vec![&truncated_paragraph, &wrapped_paragraph, &trimmed_paragraph];
for paragraph in paragraphs {
for paragraph in [&truncated_paragraph, &wrapped_paragraph, &trimmed_paragraph] {
#[rustfmt::skip]
test_case(
paragraph,
Buffer::with_lines(vec![
&Buffer::with_lines([
"┌Title─────────┐",
"│Hello, worlds!│",
"└──────────────┘",
@@ -543,7 +527,7 @@ mod test {
);
test_case(
paragraph,
Buffer::with_lines(vec![
&Buffer::with_lines([
"┌Title───────────┐",
"│Hello, worlds! │",
"└────────────────┘",
@@ -551,7 +535,7 @@ mod test {
);
test_case(
paragraph,
Buffer::with_lines(vec![
&Buffer::with_lines([
"┌Title────────────┐",
"│Hello, worlds! │",
"│ │",
@@ -562,7 +546,7 @@ mod test {
test_case(
&truncated_paragraph,
Buffer::with_lines(vec![
&Buffer::with_lines([
"┌Title───────┐",
"│Hello, world│",
"│ │",
@@ -571,7 +555,7 @@ mod test {
);
test_case(
&wrapped_paragraph,
Buffer::with_lines(vec![
&Buffer::with_lines([
"┌Title──────┐",
"│Hello, │",
"│worlds! │",
@@ -580,7 +564,7 @@ mod test {
);
test_case(
&trimmed_paragraph,
Buffer::with_lines(vec![
&Buffer::with_lines([
"┌Title──────┐",
"│Hello, │",
"│worlds! │",
@@ -598,42 +582,41 @@ mod test {
let paragraph = Paragraph::new(vec![l0, l1, l2, l3]);
let mut expected =
Buffer::with_lines(vec!["unformatted", "bold text", "cyan text", "dim text"]);
Buffer::with_lines(["unformatted", "bold text", "cyan text", "dim text"]);
expected.set_style(Rect::new(0, 1, 9, 1), Style::new().bold());
expected.set_style(Rect::new(0, 2, 9, 1), Style::new().cyan());
expected.set_style(Rect::new(0, 3, 8, 1), Style::new().dim());
test_case(&paragraph, expected);
test_case(&paragraph, &expected);
}
#[test]
fn test_render_line_spans_styled() {
let l0 = Line::default().spans(vec![
let l0 = Line::default().spans([
Span::styled("bold", Style::new().bold()),
Span::raw(" and "),
Span::styled("cyan", Style::new().cyan()),
]);
let l1 = Line::default().spans(vec![Span::raw("unformatted")]);
let l1 = Line::default().spans([Span::raw("unformatted")]);
let paragraph = Paragraph::new(vec![l0, l1]);
let mut expected = Buffer::with_lines(vec!["bold and cyan", "unformatted"]);
let mut expected = Buffer::with_lines(["bold and cyan", "unformatted"]);
expected.set_style(Rect::new(0, 0, 4, 1), Style::new().bold());
expected.set_style(Rect::new(9, 0, 4, 1), Style::new().cyan());
test_case(&paragraph, expected);
test_case(&paragraph, &expected);
}
#[test]
fn test_render_paragraph_with_block_with_bottom_title_and_border() {
let block = Block::default()
.title("Title")
let block = Block::new()
.borders(Borders::BOTTOM)
.title_position(Position::Bottom)
.borders(Borders::BOTTOM);
.title("Title");
let paragraph = Paragraph::new("Hello, world!").block(block);
test_case(
&paragraph,
Buffer::with_lines(vec!["Hello, world! ", "Title──────────"]),
&Buffer::with_lines(["Hello, world! ", "Title──────────"]),
);
}
@@ -645,7 +628,7 @@ mod test {
test_case(
&wrapped_paragraph,
Buffer::with_lines(vec![
&Buffer::with_lines([
"This is a long line",
"of text that should",
"wrap and ",
@@ -656,7 +639,7 @@ mod test {
);
test_case(
&wrapped_paragraph,
Buffer::with_lines(vec![
&Buffer::with_lines([
"This is a ",
"long line of",
"text that ",
@@ -671,7 +654,7 @@ mod test {
test_case(
&trimmed_paragraph,
Buffer::with_lines(vec![
&Buffer::with_lines([
"This is a long line",
"of text that should",
"wrap and ",
@@ -682,7 +665,7 @@ mod test {
);
test_case(
&trimmed_paragraph,
Buffer::with_lines(vec![
&Buffer::with_lines([
"This is a ",
"long line of",
"text that ",
@@ -703,19 +686,19 @@ mod test {
test_case(
&truncated_paragraph,
Buffer::with_lines(vec!["This is a long line of"]),
&Buffer::with_lines(["This is a long line of"]),
);
test_case(
&truncated_paragraph,
Buffer::with_lines(vec!["This is a long line of te"]),
&Buffer::with_lines(["This is a long line of te"]),
);
test_case(
&truncated_paragraph,
Buffer::with_lines(vec!["This is a long line of "]),
&Buffer::with_lines(["This is a long line of "]),
);
test_case(
&truncated_paragraph.clone().scroll((0, 2)),
Buffer::with_lines(vec!["is is a long line of te"]),
&Buffer::with_lines(["is is a long line of te"]),
);
}
@@ -726,21 +709,19 @@ mod test {
let wrapped_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: false });
let trimmed_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: true });
let paragraphs = vec![&truncated_paragraph, &wrapped_paragraph, &trimmed_paragraph];
for paragraph in paragraphs {
test_case(paragraph, Buffer::with_lines(vec!["Hello, world! "]));
test_case(paragraph, Buffer::with_lines(vec!["Hello, world!"]));
for paragraph in [&truncated_paragraph, &wrapped_paragraph, &trimmed_paragraph] {
test_case(paragraph, &Buffer::with_lines(["Hello, world! "]));
test_case(paragraph, &Buffer::with_lines(["Hello, world!"]));
}
test_case(&truncated_paragraph, Buffer::with_lines(vec!["Hello, wor"]));
test_case(&truncated_paragraph, &Buffer::with_lines(["Hello, wor"]));
test_case(
&wrapped_paragraph,
Buffer::with_lines(vec!["Hello, ", "world! "]),
&Buffer::with_lines(["Hello, ", "world! "]),
);
test_case(
&trimmed_paragraph,
Buffer::with_lines(vec!["Hello, ", "world! "]),
&Buffer::with_lines(["Hello, ", "world! "]),
);
}
@@ -751,23 +732,21 @@ mod test {
let wrapped_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: false });
let trimmed_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: true });
let paragraphs = vec![&truncated_paragraph, &wrapped_paragraph, &trimmed_paragraph];
for paragraph in paragraphs {
test_case(paragraph, Buffer::with_lines(vec![" Hello, world! "]));
test_case(paragraph, Buffer::with_lines(vec![" Hello, world! "]));
test_case(paragraph, Buffer::with_lines(vec![" Hello, world! "]));
test_case(paragraph, Buffer::with_lines(vec!["Hello, world!"]));
for paragraph in [&truncated_paragraph, &wrapped_paragraph, &trimmed_paragraph] {
test_case(paragraph, &Buffer::with_lines([" Hello, world! "]));
test_case(paragraph, &Buffer::with_lines([" Hello, world! "]));
test_case(paragraph, &Buffer::with_lines([" Hello, world! "]));
test_case(paragraph, &Buffer::with_lines(["Hello, world!"]));
}
test_case(&truncated_paragraph, Buffer::with_lines(vec!["Hello, wor"]));
test_case(&truncated_paragraph, &Buffer::with_lines(["Hello, wor"]));
test_case(
&wrapped_paragraph,
Buffer::with_lines(vec![" Hello, ", " world! "]),
&Buffer::with_lines([" Hello, ", " world! "]),
);
test_case(
&trimmed_paragraph,
Buffer::with_lines(vec![" Hello, ", " world! "]),
&Buffer::with_lines([" Hello, ", " world! "]),
);
}
@@ -778,21 +757,19 @@ mod test {
let wrapped_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: false });
let trimmed_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: true });
let paragraphs = vec![&truncated_paragraph, &wrapped_paragraph, &trimmed_paragraph];
for paragraph in paragraphs {
test_case(paragraph, Buffer::with_lines(vec![" Hello, world!"]));
test_case(paragraph, Buffer::with_lines(vec!["Hello, world!"]));
for paragraph in [&truncated_paragraph, &wrapped_paragraph, &trimmed_paragraph] {
test_case(paragraph, &Buffer::with_lines([" Hello, world!"]));
test_case(paragraph, &Buffer::with_lines(["Hello, world!"]));
}
test_case(&truncated_paragraph, Buffer::with_lines(vec!["Hello, wor"]));
test_case(&truncated_paragraph, &Buffer::with_lines(["Hello, wor"]));
test_case(
&wrapped_paragraph,
Buffer::with_lines(vec![" Hello,", " world!"]),
&Buffer::with_lines([" Hello,", " world!"]),
);
test_case(
&trimmed_paragraph,
Buffer::with_lines(vec![" Hello,", " world!"]),
&Buffer::with_lines([" Hello,", " world!"]),
);
}
@@ -803,57 +780,51 @@ mod test {
let wrapped_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: false });
let trimmed_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: true });
let paragraphs = vec![&truncated_paragraph, &wrapped_paragraph, &trimmed_paragraph];
for paragraph in paragraphs {
for paragraph in [&truncated_paragraph, &wrapped_paragraph, &trimmed_paragraph] {
test_case(
paragraph,
Buffer::with_lines(vec!["multiline ", "paragraph. ", " "]),
&Buffer::with_lines(["multiline ", "paragraph. ", " "]),
);
test_case(paragraph, Buffer::with_lines(vec!["multiline "]));
test_case(paragraph, &Buffer::with_lines(["multiline "]));
}
test_case(
&truncated_paragraph.clone().scroll((2, 4)),
Buffer::with_lines(vec!["iline ", "graph. "]),
&Buffer::with_lines(["iline ", "graph. "]),
);
test_case(
&wrapped_paragraph,
Buffer::with_lines(vec!["cool ", "multili", "ne "]),
&Buffer::with_lines(["cool ", "multili", "ne "]),
);
}
#[test]
fn test_render_paragraph_with_zero_width_area() {
let text = "Hello, world!";
let area = Rect::new(0, 0, 0, 3);
let paragraphs = vec![
for paragraph in [
Paragraph::new(text),
Paragraph::new(text).wrap(Wrap { trim: false }),
Paragraph::new(text).wrap(Wrap { trim: true }),
];
let area = Rect::new(0, 0, 0, 3);
for paragraph in paragraphs {
test_case(&paragraph, Buffer::empty(area));
test_case(&paragraph.clone().scroll((2, 4)), Buffer::empty(area));
] {
test_case(&paragraph, &Buffer::empty(area));
test_case(&paragraph.clone().scroll((2, 4)), &Buffer::empty(area));
}
}
#[test]
fn test_render_paragraph_with_zero_height_area() {
let text = "Hello, world!";
let area = Rect::new(0, 0, 10, 0);
let paragraphs = vec![
for paragraph in [
Paragraph::new(text),
Paragraph::new(text).wrap(Wrap { trim: false }),
Paragraph::new(text).wrap(Wrap { trim: true }),
];
let area = Rect::new(0, 0, 10, 0);
for paragraph in paragraphs {
test_case(&paragraph, Buffer::empty(area));
test_case(&paragraph.clone().scroll((2, 4)), Buffer::empty(area));
] {
test_case(&paragraph, &Buffer::empty(area));
test_case(&paragraph.clone().scroll((2, 4)), &Buffer::empty(area));
}
}
@@ -864,13 +835,7 @@ mod test {
Span::styled("world!", Style::default().fg(Color::Blue)),
]);
let paragraphs = vec![
Paragraph::new(text.clone()),
Paragraph::new(text.clone()).wrap(Wrap { trim: false }),
Paragraph::new(text.clone()).wrap(Wrap { trim: true }),
];
let mut expected_buffer = Buffer::with_lines(vec!["Hello, world!"]);
let mut expected_buffer = Buffer::with_lines(["Hello, world!"]);
expected_buffer.set_style(
Rect::new(0, 0, 7, 1),
Style::default().fg(Color::Red).bg(Color::Green),
@@ -879,10 +844,15 @@ mod test {
Rect::new(7, 0, 6, 1),
Style::default().fg(Color::Blue).bg(Color::Green),
);
for paragraph in paragraphs {
for paragraph in [
Paragraph::new(text.clone()),
Paragraph::new(text.clone()).wrap(Wrap { trim: false }),
Paragraph::new(text.clone()).wrap(Wrap { trim: true }),
] {
test_case(
&paragraph.style(Style::default().bg(Color::Green)),
expected_buffer.clone(),
&expected_buffer,
);
}
}
@@ -890,22 +860,20 @@ mod test {
#[test]
fn test_render_paragraph_with_special_characters() {
let text = "Hello, <world>!";
let paragraphs = vec![
for paragraph in [
Paragraph::new(text),
Paragraph::new(text).wrap(Wrap { trim: false }),
Paragraph::new(text).wrap(Wrap { trim: true }),
];
for paragraph in paragraphs {
test_case(&paragraph, Buffer::with_lines(vec!["Hello, <world>!"]));
test_case(&paragraph, Buffer::with_lines(vec!["Hello, <world>! "]));
] {
test_case(&paragraph, &Buffer::with_lines(["Hello, <world>!"]));
test_case(&paragraph, &Buffer::with_lines(["Hello, <world>! "]));
test_case(
&paragraph,
Buffer::with_lines(vec!["Hello, <world>! ", " "]),
&Buffer::with_lines(["Hello, <world>! ", " "]),
);
test_case(
&paragraph,
Buffer::with_lines(vec!["Hello, <world>!", " "]),
&Buffer::with_lines(["Hello, <world>!", " "]),
);
}
}
@@ -917,27 +885,25 @@ mod test {
let wrapped_paragraph = Paragraph::new(text).wrap(Wrap { trim: false });
let trimmed_paragraph = Paragraph::new(text).wrap(Wrap { trim: true });
let paragraphs = vec![&truncated_paragraph, &wrapped_paragraph, &trimmed_paragraph];
for paragraph in paragraphs {
test_case(paragraph, Buffer::with_lines(vec!["こんにちは, 世界! 😃"]));
for paragraph in [&truncated_paragraph, &wrapped_paragraph, &trimmed_paragraph] {
test_case(paragraph, &Buffer::with_lines(["こんにちは, 世界! 😃"]));
test_case(
paragraph,
Buffer::with_lines(vec!["こんにちは, 世界! 😃 "]),
&Buffer::with_lines(["こんにちは, 世界! 😃 "]),
);
}
test_case(
&truncated_paragraph,
Buffer::with_lines(vec!["こんにちは, 世 "]),
&Buffer::with_lines(["こんにちは, 世 "]),
);
test_case(
&wrapped_paragraph,
Buffer::with_lines(vec!["こんにちは, ", "世界! 😃 "]),
&Buffer::with_lines(["こんにちは, ", "世界! 😃 "]),
);
test_case(
&trimmed_paragraph,
Buffer::with_lines(vec!["こんにちは, ", "世界! 😃 "]),
&Buffer::with_lines(["こんにちは, ", "世界! 😃 "]),
);
}
@@ -1025,13 +991,12 @@ mod test {
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 3));
paragraph.render(Rect::new(0, 0, 20, 3), &mut buf);
let mut expected = Buffer::with_lines(vec![
let mut expected = Buffer::with_lines([
"┌──────────────────┐",
"│Styled text │",
"└──────────────────┘",
]);
expected.set_style(Rect::new(1, 1, 11, 1), Style::default().fg(Color::Green));
assert_eq!(buf, expected);
}
}

View File

@@ -6,6 +6,7 @@ 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
@@ -104,8 +105,8 @@ where
let mut has_seen_non_whitespace = false;
for StyledGrapheme { symbol, style } in line_symbols {
let symbol_whitespace =
symbol.chars().all(&char::is_whitespace) && symbol != NBSP;
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 {
@@ -706,4 +707,12 @@ mod test {
vec![Alignment::Left, Alignment::Right, Alignment::Center]
);
}
#[test]
fn line_composer_zero_width_white_space() {
let width = 3;
let line = "foo\u{200b}bar";
let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: true }, line, width);
assert_eq!(word_wrapper, vec!["foo", "bar"]);
}
}

View File

@@ -29,6 +29,11 @@ use crate::{prelude::*, symbols::scrollbar::*};
/// └─────────── begin
/// ```
///
/// # Important
///
/// You must specify the [`ScrollbarState::content_length`] before rendering the `Scrollbar`, or
/// else the `Scrollbar` will render blank.
///
/// # Examples
///
/// ```rust
@@ -669,113 +674,107 @@ mod tests {
}
#[rstest]
#[case("#-", 0, 2, "area 2, position_0")]
#[case("-#", 1, 2, "area 2, position_1")]
#[case::area_2_position_0("#-", 0, 2)]
#[case::area_2_position_1("-#", 1, 2)]
fn render_scrollbar_simplest(
#[case] expected: &str,
#[case] position: usize,
#[case] content_length: usize,
#[case] description: &str,
scrollbar_no_arrows: Scrollbar,
) {
let mut buffer = Buffer::empty(Rect::new(0, 0, expected.width() as u16, 1));
let mut state = ScrollbarState::new(content_length).position(position);
scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
assert_eq!(buffer, Buffer::with_lines(vec![expected]), "{description}",);
assert_eq!(buffer, Buffer::with_lines([expected]));
}
#[rstest]
#[case("#####-----", 0, 10, "position_0")]
#[case("-#####----", 1, 10, "position_1")]
#[case("-#####----", 2, 10, "position_2")]
#[case("--#####---", 3, 10, "position_3")]
#[case("--#####---", 4, 10, "position_4")]
#[case("---#####--", 5, 10, "position_5")]
#[case("---#####--", 6, 10, "position_6")]
#[case("----#####-", 7, 10, "position_7")]
#[case("----#####-", 8, 10, "position_8")]
#[case("-----#####", 9, 10, "position_9")]
#[case::position_0("#####-----", 0, 10)]
#[case::position_1("-#####----", 1, 10)]
#[case::position_2("-#####----", 2, 10)]
#[case::position_3("--#####---", 3, 10)]
#[case::position_4("--#####---", 4, 10)]
#[case::position_5("---#####--", 5, 10)]
#[case::position_6("---#####--", 6, 10)]
#[case::position_7("----#####-", 7, 10)]
#[case::position_8("----#####-", 8, 10)]
#[case::position_9("-----#####", 9, 10)]
fn render_scrollbar_simple(
#[case] expected: &str,
#[case] position: usize,
#[case] content_length: usize,
#[case] description: &str,
scrollbar_no_arrows: Scrollbar,
) {
let mut buffer = Buffer::empty(Rect::new(0, 0, expected.width() as u16, 1));
let mut state = ScrollbarState::new(content_length).position(position);
scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
assert_eq!(buffer, Buffer::with_lines(vec![expected]), "{description}",);
assert_eq!(buffer, Buffer::with_lines([expected]));
}
#[rstest]
#[case(" ", 0, 0, "position_0")]
#[case::position_0(" ", 0, 0)]
fn render_scrollbar_nobar(
#[case] expected: &str,
#[case] position: usize,
#[case] content_length: usize,
#[case] description: &str,
scrollbar_no_arrows: Scrollbar,
) {
let size = expected.width();
let mut buffer = Buffer::empty(Rect::new(0, 0, size as u16, 1));
let mut state = ScrollbarState::new(content_length).position(position);
scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
assert_eq!(buffer, Buffer::with_lines(vec![expected]), "{description}",);
assert_eq!(buffer, Buffer::with_lines([expected]));
}
#[rstest]
#[case("##########", 0, 1, "fullbar position 0")]
#[case("#########-", 0, 2, "almost fullbar position 0")]
#[case("-#########", 1, 2, "almost fullbar position 1")]
#[case::fullbar_position_0("##########", 0, 1)]
#[case::almost_fullbar_position_0("#########-", 0, 2)]
#[case::almost_fullbar_position_1("-#########", 1, 2)]
fn render_scrollbar_fullbar(
#[case] expected: &str,
#[case] position: usize,
#[case] content_length: usize,
#[case] description: &str,
scrollbar_no_arrows: Scrollbar,
) {
let size = expected.width();
let mut buffer = Buffer::empty(Rect::new(0, 0, size as u16, 1));
let mut state = ScrollbarState::new(content_length).position(position);
scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
assert_eq!(buffer, Buffer::with_lines(vec![expected]), "{description}",);
assert_eq!(buffer, Buffer::with_lines([expected]));
}
#[rstest]
#[case("#########-", 0, 2, "position_0")]
#[case("-#########", 1, 2, "position_1")]
#[case::position_0("#########-", 0, 2)]
#[case::position_1("-#########", 1, 2)]
fn render_scrollbar_almost_fullbar(
#[case] expected: &str,
#[case] position: usize,
#[case] content_length: usize,
#[case] description: &str,
scrollbar_no_arrows: Scrollbar,
) {
let size = expected.width();
let mut buffer = Buffer::empty(Rect::new(0, 0, size as u16, 1));
let mut state = ScrollbarState::new(content_length).position(position);
scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
assert_eq!(buffer, Buffer::with_lines(vec![expected]), "{description}",);
assert_eq!(buffer, Buffer::with_lines([expected]));
}
#[rstest]
#[case("█████═════", 0, 10, "position_0")]
#[case("═█████════", 1, 10, "position_1")]
#[case("═█████════", 2, 10, "position_2")]
#[case("══█████═══", 3, 10, "position_3")]
#[case("══█████═══", 4, 10, "position_4")]
#[case("═══█████══", 5, 10, "position_5")]
#[case("═══█████══", 6, 10, "position_6")]
#[case("════█████═", 7, 10, "position_7")]
#[case("════█████═", 8, 10, "position_8")]
#[case("═════█████", 9, 10, "position_9")]
#[case("═════█████", 100, 10, "position_out_of_bounds")]
#[case::position_0("█████═════", 0, 10)]
#[case::position_1("═█████════", 1, 10)]
#[case::position_2("═█████════", 2, 10)]
#[case::position_3("══█████═══", 3, 10)]
#[case::position_4("══█████═══", 4, 10)]
#[case::position_5("═══█████══", 5, 10)]
#[case::position_6("═══█████══", 6, 10)]
#[case::position_7("════█████═", 7, 10)]
#[case::position_8("════█████═", 8, 10)]
#[case::position_9("═════█████", 9, 10)]
#[case::position_out_of_bounds("═════█████", 100, 10)]
fn render_scrollbar_without_symbols(
#[case] expected: &str,
#[case] position: usize,
#[case] content_length: usize,
#[case] assertion_message: &str,
) {
let size = expected.width() as u16;
let mut buffer = Buffer::empty(Rect::new(0, 0, size, 1));
@@ -784,30 +783,25 @@ mod tests {
.begin_symbol(None)
.end_symbol(None)
.render(buffer.area, &mut buffer, &mut state);
assert_eq!(
buffer,
Buffer::with_lines(vec![expected]),
"{assertion_message}",
);
assert_eq!(buffer, Buffer::with_lines([expected]));
}
#[rstest]
#[case("█████ ", 0, 10, "position_0")]
#[case(" █████ ", 1, 10, "position_1")]
#[case(" █████ ", 2, 10, "position_2")]
#[case(" █████ ", 3, 10, "position_3")]
#[case(" █████ ", 4, 10, "position_4")]
#[case(" █████ ", 5, 10, "position_5")]
#[case(" █████ ", 6, 10, "position_6")]
#[case(" █████ ", 7, 10, "position_7")]
#[case(" █████ ", 8, 10, "position_8")]
#[case(" █████", 9, 10, "position_9")]
#[case(" █████", 100, 10, "position_out_of_bounds")]
#[case::position_0("█████ ", 0, 10)]
#[case::position_1(" █████ ", 1, 10)]
#[case::position_2(" █████ ", 2, 10)]
#[case::position_3(" █████ ", 3, 10)]
#[case::position_4(" █████ ", 4, 10)]
#[case::position_5(" █████ ", 5, 10)]
#[case::position_6(" █████ ", 6, 10)]
#[case::position_7(" █████ ", 7, 10)]
#[case::position_8(" █████ ", 8, 10)]
#[case::position_9(" █████", 9, 10)]
#[case::position_out_of_bounds(" █████", 100, 10)]
fn render_scrollbar_without_track_symbols(
#[case] expected: &str,
#[case] position: usize,
#[case] content_length: usize,
#[case] assertion_message: &str,
) {
let size = expected.width() as u16;
let mut buffer = Buffer::empty(Rect::new(0, 0, size, 1));
@@ -817,30 +811,25 @@ mod tests {
.begin_symbol(None)
.end_symbol(None)
.render(buffer.area, &mut buffer, &mut state);
assert_eq!(
buffer,
Buffer::with_lines(vec![expected]),
"{assertion_message}",
);
assert_eq!(buffer, Buffer::with_lines([expected]));
}
#[rstest]
#[case("█████-----", 0, 10, "position_0")]
#[case("-█████----", 1, 10, "position_1")]
#[case("-█████----", 2, 10, "position_2")]
#[case("--█████---", 3, 10, "position_3")]
#[case("--█████---", 4, 10, "position_4")]
#[case("---█████--", 5, 10, "position_5")]
#[case("---█████--", 6, 10, "position_6")]
#[case("----█████-", 7, 10, "position_7")]
#[case("----█████-", 8, 10, "position_8")]
#[case("-----█████", 9, 10, "position_9")]
#[case("-----█████", 100, 10, "position_out_of_bounds")]
#[case::position_0("█████-----", 0, 10)]
#[case::position_1("-█████----", 1, 10)]
#[case::position_2("-█████----", 2, 10)]
#[case::position_3("--█████---", 3, 10)]
#[case::position_4("--█████---", 4, 10)]
#[case::position_5("---█████--", 5, 10)]
#[case::position_6("---█████--", 6, 10)]
#[case::position_7("----█████-", 7, 10)]
#[case::position_8("----█████-", 8, 10)]
#[case::position_9("-----█████", 9, 10)]
#[case::position_out_of_bounds("-----█████", 100, 10)]
fn render_scrollbar_without_track_symbols_over_content(
#[case] expected: &str,
#[case] position: usize,
#[case] content_length: usize,
#[case] assertion_message: &str,
) {
let size = expected.width() as u16;
let mut buffer = Buffer::empty(Rect::new(0, 0, size, 1));
@@ -853,32 +842,27 @@ mod tests {
.begin_symbol(None)
.end_symbol(None)
.render(buffer.area, &mut buffer, &mut state);
assert_eq!(
buffer,
Buffer::with_lines(vec![expected]),
"{assertion_message}",
);
assert_eq!(buffer, Buffer::with_lines([expected]));
}
#[rstest]
#[case("<####---->", 0, 10, "position_0")]
#[case("<#####--->", 1, 10, "position_1")]
#[case("<-####--->", 2, 10, "position_2")]
#[case("<-####--->", 3, 10, "position_3")]
#[case("<--####-->", 4, 10, "position_4")]
#[case("<--####-->", 5, 10, "position_5")]
#[case("<---####->", 6, 10, "position_6")]
#[case("<---####->", 7, 10, "position_7")]
#[case("<---#####>", 8, 10, "position_8")]
#[case("<----####>", 9, 10, "position_9")]
#[case("<----####>", 10, 10, "position_one_out_of_bounds")]
#[case("<----####>", 15, 10, "position_few_out_of_bounds")]
#[case("<----####>", 500, 10, "position_very_many_out_of_bounds")]
#[case::position_0("<####---->", 0, 10)]
#[case::position_1("<#####--->", 1, 10)]
#[case::position_2("<-####--->", 2, 10)]
#[case::position_3("<-####--->", 3, 10)]
#[case::position_4("<--####-->", 4, 10)]
#[case::position_5("<--####-->", 5, 10)]
#[case::position_6("<---####->", 6, 10)]
#[case::position_7("<---####->", 7, 10)]
#[case::position_8("<---#####>", 8, 10)]
#[case::position_9("<----####>", 9, 10)]
#[case::position_one_out_of_bounds("<----####>", 10, 10)]
#[case::position_few_out_of_bounds("<----####>", 15, 10)]
#[case::position_very_many_out_of_bounds("<----####>", 500, 10)]
fn render_scrollbar_with_symbols(
#[case] expected: &str,
#[case] position: usize,
#[case] content_length: usize,
#[case] assertion_message: &str,
) {
let size = expected.width() as u16;
let mut buffer = Buffer::empty(Rect::new(0, 0, size, 1));
@@ -889,30 +873,25 @@ mod tests {
.track_symbol(Some("-"))
.thumb_symbol("#")
.render(buffer.area, &mut buffer, &mut state);
assert_eq!(
buffer,
Buffer::with_lines(vec![expected]),
"{assertion_message}",
);
assert_eq!(buffer, Buffer::with_lines([expected]));
}
#[rstest]
#[case("█████═════", 0, 10, "position_0")]
#[case("═█████════", 1, 10, "position_1")]
#[case("═█████════", 2, 10, "position_2")]
#[case("══█████═══", 3, 10, "position_3")]
#[case("══█████═══", 4, 10, "position_4")]
#[case("═══█████══", 5, 10, "position_5")]
#[case("═══█████══", 6, 10, "position_6")]
#[case("════█████═", 7, 10, "position_7")]
#[case("════█████═", 8, 10, "position_8")]
#[case("═════█████", 9, 10, "position_9")]
#[case("═════█████", 100, 10, "position_out_of_bounds")]
#[case::position_0("█████═════", 0, 10)]
#[case::position_1("═█████════", 1, 10)]
#[case::position_2("═█████════", 2, 10)]
#[case::position_3("══█████═══", 3, 10)]
#[case::position_4("══█████═══", 4, 10)]
#[case::position_5("═══█████══", 5, 10)]
#[case::position_6("═══█████══", 6, 10)]
#[case::position_7("════█████═", 7, 10)]
#[case::position_8("════█████═", 8, 10)]
#[case::position_9("═════█████", 9, 10)]
#[case::position_out_of_bounds("═════█████", 100, 10)]
fn render_scrollbar_horizontal_bottom(
#[case] expected: &str,
#[case] position: usize,
#[case] content_length: usize,
#[case] description: &str,
) {
let size = expected.width() as u16;
let mut buffer = Buffer::empty(Rect::new(0, 0, size, 2));
@@ -922,30 +901,25 @@ mod tests {
.end_symbol(None)
.render(buffer.area, &mut buffer, &mut state);
let empty_string = " ".repeat(size as usize);
assert_eq!(
buffer,
Buffer::with_lines(vec![&empty_string, expected]),
"{description}",
);
assert_eq!(buffer, Buffer::with_lines([&empty_string, expected]));
}
#[rstest]
#[case("█████═════", 0, 10, "position_0")]
#[case("═█████════", 1, 10, "position_1")]
#[case("═█████════", 2, 10, "position_2")]
#[case("══█████═══", 3, 10, "position_3")]
#[case("══█████═══", 4, 10, "position_4")]
#[case("═══█████══", 5, 10, "position_5")]
#[case("═══█████══", 6, 10, "position_6")]
#[case("════█████═", 7, 10, "position_7")]
#[case("════█████═", 8, 10, "position_8")]
#[case("═════█████", 9, 10, "position_9")]
#[case("═════█████", 100, 10, "position_out_of_bounds")]
#[case::position_0("█████═════", 0, 10)]
#[case::position_1("═█████════", 1, 10)]
#[case::position_2("═█████════", 2, 10)]
#[case::position_3("══█████═══", 3, 10)]
#[case::position_4("══█████═══", 4, 10)]
#[case::position_5("═══█████══", 5, 10)]
#[case::position_6("═══█████══", 6, 10)]
#[case::position_7("════█████═", 7, 10)]
#[case::position_8("════█████═", 8, 10)]
#[case::position_9("═════█████", 9, 10)]
#[case::position_out_of_bounds("═════█████", 100, 10)]
fn render_scrollbar_horizontal_top(
#[case] expected: &str,
#[case] position: usize,
#[case] content_length: usize,
#[case] description: &str,
) {
let size = expected.width() as u16;
let mut buffer = Buffer::empty(Rect::new(0, 0, size, 2));
@@ -955,30 +929,25 @@ mod tests {
.end_symbol(None)
.render(buffer.area, &mut buffer, &mut state);
let empty_string = " ".repeat(size as usize);
assert_eq!(
buffer,
Buffer::with_lines(vec![expected, &empty_string]),
"{description}",
);
assert_eq!(buffer, Buffer::with_lines([expected, &empty_string]));
}
#[rstest]
#[case("<####---->", 0, 10, "position_0")]
#[case("<#####--->", 1, 10, "position_1")]
#[case("<-####--->", 2, 10, "position_2")]
#[case("<-####--->", 3, 10, "position_3")]
#[case("<--####-->", 4, 10, "position_4")]
#[case("<--####-->", 5, 10, "position_5")]
#[case("<---####->", 6, 10, "position_6")]
#[case("<---####->", 7, 10, "position_7")]
#[case("<---#####>", 8, 10, "position_8")]
#[case("<----####>", 9, 10, "position_9")]
#[case("<----####>", 10, 10, "position_one_out_of_bounds")]
#[case::position_0("<####---->", 0, 10)]
#[case::position_1("<#####--->", 1, 10)]
#[case::position_2("<-####--->", 2, 10)]
#[case::position_3("<-####--->", 3, 10)]
#[case::position_4("<--####-->", 4, 10)]
#[case::position_5("<--####-->", 5, 10)]
#[case::position_6("<---####->", 6, 10)]
#[case::position_7("<---####->", 7, 10)]
#[case::position_8("<---#####>", 8, 10)]
#[case::position_9("<----####>", 9, 10)]
#[case::position_one_out_of_bounds("<----####>", 10, 10)]
fn render_scrollbar_vertical_left(
#[case] expected: &str,
#[case] position: usize,
#[case] content_length: usize,
#[case] description: &str,
) {
let size = expected.width() as u16;
let mut buffer = Buffer::empty(Rect::new(0, 0, 5, size));
@@ -990,26 +959,25 @@ mod tests {
.thumb_symbol("#")
.render(buffer.area, &mut buffer, &mut state);
let bar = expected.chars().map(|c| format!("{c} "));
assert_eq!(buffer, Buffer::with_lines(bar), "{description}");
assert_eq!(buffer, Buffer::with_lines(bar));
}
#[rstest]
#[case("<####---->", 0, 10, "position_0")]
#[case("<#####--->", 1, 10, "position_1")]
#[case("<-####--->", 2, 10, "position_2")]
#[case("<-####--->", 3, 10, "position_3")]
#[case("<--####-->", 4, 10, "position_4")]
#[case("<--####-->", 5, 10, "position_5")]
#[case("<---####->", 6, 10, "position_6")]
#[case("<---####->", 7, 10, "position_7")]
#[case("<---#####>", 8, 10, "position_8")]
#[case("<----####>", 9, 10, "position_9")]
#[case("<----####>", 10, 10, "position_one_out_of_bounds")]
#[case::position_0("<####---->", 0, 10)]
#[case::position_1("<#####--->", 1, 10)]
#[case::position_2("<-####--->", 2, 10)]
#[case::position_3("<-####--->", 3, 10)]
#[case::position_4("<--####-->", 4, 10)]
#[case::position_5("<--####-->", 5, 10)]
#[case::position_6("<---####->", 6, 10)]
#[case::position_7("<---####->", 7, 10)]
#[case::position_8("<---#####>", 8, 10)]
#[case::position_9("<----####>", 9, 10)]
#[case::position_one_out_of_bounds("<----####>", 10, 10)]
fn render_scrollbar_vertical_rightl(
#[case] expected: &str,
#[case] position: usize,
#[case] content_length: usize,
#[case] description: &str,
) {
let size = expected.width() as u16;
let mut buffer = Buffer::empty(Rect::new(0, 0, 5, size));
@@ -1021,26 +989,25 @@ mod tests {
.thumb_symbol("#")
.render(buffer.area, &mut buffer, &mut state);
let bar = expected.chars().map(|c| format!(" {c}"));
assert_eq!(buffer, Buffer::with_lines(bar), "{description}");
assert_eq!(buffer, Buffer::with_lines(bar));
}
#[rstest]
#[case("##--------", 0, 10, "position_0")]
#[case("-##-------", 1, 10, "position_1")]
#[case("--##------", 2, 10, "position_2")]
#[case("---##-----", 3, 10, "position_3")]
#[case("----#-----", 4, 10, "position_4")]
#[case("-----#----", 5, 10, "position_5")]
#[case("-----##---", 6, 10, "position_6")]
#[case("------##--", 7, 10, "position_7")]
#[case("-------##-", 8, 10, "position_8")]
#[case("--------##", 9, 10, "position_9")]
#[case("--------##", 10, 10, "position_one_out_of_bounds")]
#[case::position_0("##--------", 0, 10)]
#[case::position_1("-##-------", 1, 10)]
#[case::position_2("--##------", 2, 10)]
#[case::position_3("---##-----", 3, 10)]
#[case::position_4("----#-----", 4, 10)]
#[case::position_5("-----#----", 5, 10)]
#[case::position_6("-----##---", 6, 10)]
#[case::position_7("------##--", 7, 10)]
#[case::position_8("-------##-", 8, 10)]
#[case::position_9("--------##", 9, 10)]
#[case::position_one_out_of_bounds("--------##", 10, 10)]
fn custom_viewport_length(
#[case] expected: &str,
#[case] position: usize,
#[case] content_length: usize,
#[case] description: &str,
scrollbar_no_arrows: Scrollbar,
) {
let size = expected.width() as u16;
@@ -1049,28 +1016,27 @@ mod tests {
.position(position)
.viewport_content_length(2);
scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
assert_eq!(buffer, Buffer::with_lines(vec![expected]), "{description}");
assert_eq!(buffer, Buffer::with_lines([expected]));
}
/// Fixes <https://github.com/ratatui-org/ratatui/pull/959> which was a bug that would not
/// render a thumb when the viewport was very small in comparison to the content length.
#[rstest]
#[case("#----", 0, 100, "position_0")]
#[case("#----", 10, 100, "position_10")]
#[case("-#---", 20, 100, "position_20")]
#[case("-#---", 30, 100, "position_30")]
#[case("--#--", 40, 100, "position_40")]
#[case("--#--", 50, 100, "position_50")]
#[case("---#-", 60, 100, "position_60")]
#[case("---#-", 70, 100, "position_70")]
#[case("----#", 80, 100, "position_80")]
#[case("----#", 90, 100, "position_90")]
#[case("----#", 100, 100, "position_one_out_of_bounds")]
#[case::position_0("#----", 0, 100)]
#[case::position_10("#----", 10, 100)]
#[case::position_20("-#---", 20, 100)]
#[case::position_30("-#---", 30, 100)]
#[case::position_40("--#--", 40, 100)]
#[case::position_50("--#--", 50, 100)]
#[case::position_60("---#-", 60, 100)]
#[case::position_70("---#-", 70, 100)]
#[case::position_80("----#", 80, 100)]
#[case::position_90("----#", 90, 100)]
#[case::position_one_out_of_bounds("----#", 100, 100)]
fn thumb_visible_on_very_small_track(
#[case] expected: &str,
#[case] position: usize,
#[case] content_length: usize,
#[case] description: &str,
scrollbar_no_arrows: Scrollbar,
) {
let size = expected.width() as u16;
@@ -1079,6 +1045,6 @@ mod tests {
.position(position)
.viewport_content_length(2);
scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
assert_eq!(buffer, Buffer::with_lines(vec![expected]), "{description}");
assert_eq!(buffer, Buffer::with_lines([expected]));
}
}

View File

@@ -24,7 +24,7 @@ use crate::{prelude::*, widgets::Block};
/// use ratatui::{prelude::*, widgets::*};
///
/// Sparkline::default()
/// .block(Block::default().title("Sparkline").borders(Borders::ALL))
/// .block(Block::bordered().title("Sparkline"))
/// .data(&[0, 2, 3, 4, 1, 4, 10])
/// .max(5)
/// .direction(RenderDirection::RightToLeft)
@@ -225,7 +225,7 @@ mod tests {
use strum::ParseError;
use super::*;
use crate::{assert_buffer_eq, buffer::Cell};
use crate::buffer::Cell;
#[test]
fn render_direction_to_string() {
@@ -264,21 +264,21 @@ mod tests {
fn it_does_not_panic_if_max_is_zero() {
let widget = Sparkline::default().data(&[0, 0, 0]);
let buffer = render(widget, 6);
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" xxx"]));
assert_eq!(buffer, Buffer::with_lines([" xxx"]));
}
#[test]
fn it_does_not_panic_if_max_is_set_to_zero() {
let widget = Sparkline::default().data(&[0, 1, 2]).max(0);
let buffer = render(widget, 6);
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" xxx"]));
assert_eq!(buffer, Buffer::with_lines([" xxx"]));
}
#[test]
fn it_draws() {
let widget = Sparkline::default().data(&[0, 1, 2, 3, 4, 5, 6, 7, 8]);
let buffer = render(widget, 12);
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" ▁▂▃▄▅▆▇█xxx"]));
assert_eq!(buffer, Buffer::with_lines([" ▁▂▃▄▅▆▇█xxx"]));
}
#[test]
@@ -287,7 +287,7 @@ mod tests {
.data(&[0, 1, 2, 3, 4, 5, 6, 7, 8])
.direction(RenderDirection::LeftToRight);
let buffer = render(widget, 12);
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" ▁▂▃▄▅▆▇█xxx"]));
assert_eq!(buffer, Buffer::with_lines([" ▁▂▃▄▅▆▇█xxx"]));
}
#[test]
@@ -296,7 +296,7 @@ mod tests {
.data(&[0, 1, 2, 3, 4, 5, 6, 7, 8])
.direction(RenderDirection::RightToLeft);
let buffer = render(widget, 12);
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["xxx█▇▆▅▄▃▂▁ "]));
assert_eq!(buffer, Buffer::with_lines(["xxx█▇▆▅▄▃▂▁ "]));
}
#[test]

File diff suppressed because it is too large Load Diff

View File

@@ -49,6 +49,7 @@
pub struct TableState {
pub(crate) offset: usize,
pub(crate) selected: Option<usize>,
pub(crate) marked: Vec<usize>,
}
impl TableState {
@@ -60,8 +61,12 @@ impl TableState {
/// # use ratatui::{prelude::*, widgets::*};
/// let state = TableState::new();
/// ```
pub fn new() -> Self {
Self::default()
pub const fn new() -> Self {
Self {
offset: 0,
selected: None,
marked: vec![],
}
}
/// Sets the index of the first row to be displayed
@@ -172,6 +177,78 @@ impl TableState {
self.offset = 0;
}
}
/// Sets the index of the row as marked
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let mut state = TableState::default();
/// state.mark(1);
/// ```
pub fn mark(&mut self, index: usize) {
if !self.marked.contains(&index) {
self.marked.push(index);
}
}
/// Sets the index of the row as unmarked
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let mut state = TableState::default();
/// state.unmark(1);
/// ```
pub fn unmark(&mut self, index: usize) {
self.marked.retain(|i| *i != index);
}
/// Toggles the index of the row as marked or unmarked
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let mut state = TableState::default();
/// state.toggle_mark(1);
/// ```
pub fn toggle_mark(&mut self, index: usize) {
if self.marked.contains(&index) {
self.unmark(index);
} else {
self.mark(index);
}
}
/// Returns a iterator of all marked rows
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # use itertools::Itertools;
/// let mut state = TableState::default();
/// state.marked().contains(&1);
/// ```
pub fn marked(&self) -> std::slice::Iter<'_, usize> {
self.marked.iter()
}
/// Clears all marks from all rows
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let mut state = TableState::default();
/// state.clear_marks();
/// ```
pub fn clear_marks(&mut self) {
self.marked.drain(..);
}
}
#[cfg(test)]

View File

@@ -17,7 +17,7 @@ const DEFAULT_HIGHLIGHT_STYLE: Style = Style::new().add_modifier(Modifier::REVER
/// use ratatui::{prelude::*, widgets::*};
///
/// Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"])
/// .block(Block::default().title("Tabs").borders(Borders::ALL))
/// .block(Block::bordered().title("Tabs"))
/// .style(Style::default().white())
/// .highlight_style(Style::default().yellow())
/// .select(2)
@@ -333,7 +333,6 @@ where
#[cfg(test)]
mod tests {
use super::*;
use crate::{assert_buffer_eq, widgets::Borders};
#[test]
fn new() {
@@ -379,60 +378,61 @@ mod tests {
);
}
fn render(tabs: Tabs, area: Rect) -> Buffer {
#[track_caller]
fn test_case(tabs: Tabs, area: Rect, expected: &Buffer) {
let mut buffer = Buffer::empty(area);
tabs.render(area, &mut buffer);
buffer
assert_eq!(&buffer, expected);
}
#[test]
fn render_default() {
let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"]);
let mut expected = Buffer::with_lines(vec![" Tab1 │ Tab2 │ Tab3 │ Tab4 "]);
let mut expected = Buffer::with_lines([" Tab1 │ Tab2 │ Tab3 │ Tab4 "]);
// first tab selected
expected.set_style(Rect::new(1, 0, 4, 1), DEFAULT_HIGHLIGHT_STYLE);
assert_buffer_eq!(render(tabs, Rect::new(0, 0, 30, 1)), expected);
test_case(tabs, Rect::new(0, 0, 30, 1), &expected);
}
#[test]
fn render_no_padding() {
let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"]).padding("", "");
let mut expected = Buffer::with_lines(vec!["Tab1│Tab2│Tab3│Tab4 "]);
let mut expected = Buffer::with_lines(["Tab1│Tab2│Tab3│Tab4 "]);
// first tab selected
expected.set_style(Rect::new(0, 0, 4, 1), DEFAULT_HIGHLIGHT_STYLE);
assert_buffer_eq!(render(tabs, Rect::new(0, 0, 30, 1)), expected);
test_case(tabs, Rect::new(0, 0, 30, 1), &expected);
}
#[test]
fn render_more_padding() {
let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"]).padding("---", "++");
let mut expected = Buffer::with_lines(vec!["---Tab1++│---Tab2++│---Tab3++│"]);
let mut expected = Buffer::with_lines(["---Tab1++│---Tab2++│---Tab3++│"]);
// first tab selected
expected.set_style(Rect::new(3, 0, 4, 1), DEFAULT_HIGHLIGHT_STYLE);
assert_buffer_eq!(render(tabs, Rect::new(0, 0, 30, 1)), expected);
test_case(tabs, Rect::new(0, 0, 30, 1), &expected);
}
#[test]
fn render_with_block() {
let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"])
.block(Block::default().title("Tabs").borders(Borders::ALL));
let mut expected = Buffer::with_lines(vec![
let tabs =
Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"]).block(Block::bordered().title("Tabs"));
let mut expected = Buffer::with_lines([
"┌Tabs────────────────────────┐",
"│ Tab1 │ Tab2 │ Tab3 │ Tab4 │",
"└────────────────────────────┘",
]);
// first tab selected
expected.set_style(Rect::new(2, 1, 4, 1), DEFAULT_HIGHLIGHT_STYLE);
assert_buffer_eq!(render(tabs, Rect::new(0, 0, 30, 3)), expected);
test_case(tabs, Rect::new(0, 0, 30, 3), &expected);
}
#[test]
fn render_style() {
let tabs =
Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"]).style(Style::default().fg(Color::Red));
let mut expected = Buffer::with_lines(vec![" Tab1 │ Tab2 │ Tab3 │ Tab4 ".red()]);
let mut expected = Buffer::with_lines([" Tab1 │ Tab2 │ Tab3 │ Tab4 ".red()]);
expected.set_style(Rect::new(1, 0, 4, 1), DEFAULT_HIGHLIGHT_STYLE.red());
assert_buffer_eq!(render(tabs, Rect::new(0, 0, 30, 1)), expected);
test_case(tabs, Rect::new(0, 0, 30, 1), &expected);
}
#[test]
@@ -440,40 +440,32 @@ mod tests {
let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"]);
// first tab selected
assert_buffer_eq!(
render(tabs.clone().select(0), Rect::new(0, 0, 30, 1)),
Buffer::with_lines(vec![Line::from(vec![
" ".into(),
"Tab1".reversed(),
" │ Tab2 │ Tab3 │ Tab4 ".into(),
])])
);
let expected = Buffer::with_lines([Line::from(vec![
" ".into(),
"Tab1".reversed(),
" │ Tab2 │ Tab3 │ Tab4 ".into(),
])]);
test_case(tabs.clone().select(0), Rect::new(0, 0, 30, 1), &expected);
// second tab selected
assert_buffer_eq!(
render(tabs.clone().select(1), Rect::new(0, 0, 30, 1)),
Buffer::with_lines(vec![Line::from(vec![
" Tab1".into(),
"Tab2".reversed(),
" │ Tab3 │ Tab4 ".into(),
])])
);
let expected = Buffer::with_lines([Line::from(vec![
" Tab1 │ ".into(),
"Tab2".reversed(),
" Tab3 Tab4 ".into(),
])]);
test_case(tabs.clone().select(1), Rect::new(0, 0, 30, 1), &expected);
// last tab selected
assert_buffer_eq!(
render(tabs.clone().select(3), Rect::new(0, 0, 30, 1)),
Buffer::with_lines(vec![Line::from(vec![
" Tab1 │ Tab2 │ Tab3 │ ".into(),
"Tab4".reversed(),
" ".into(),
])])
);
let expected = Buffer::with_lines([Line::from(vec![
" Tab1 │ Tab2 │ Tab3 │ ".into(),
"Tab4".reversed(),
" ".into(),
])]);
test_case(tabs.clone().select(3), Rect::new(0, 0, 30, 1), &expected);
// out of bounds selects no tab
assert_buffer_eq!(
render(tabs.clone().select(4), Rect::new(0, 0, 30, 1)),
Buffer::with_lines(vec![" Tab1 │ Tab2 │ Tab3 │ Tab4 "])
);
let expected = Buffer::with_lines([" Tab1 │ Tab2 │ Tab3 │ Tab4 "]);
test_case(tabs.clone().select(4), Rect::new(0, 0, 30, 1), &expected);
}
#[test]
@@ -482,23 +474,21 @@ mod tests {
.style(Style::new().red())
.highlight_style(Style::new().underlined())
.select(0);
assert_buffer_eq!(
render(tabs, Rect::new(0, 0, 30, 1)),
Buffer::with_lines(vec![Line::from(vec![
" ".red(),
"Tab1".red().underlined(),
" │ Tab2 │ Tab3 │ Tab4 ".red(),
])])
);
let expected = Buffer::with_lines([Line::from(vec![
" ".red(),
"Tab1".red().underlined(),
" │ Tab2 │ Tab3 │ Tab4 ".red(),
])]);
test_case(tabs, Rect::new(0, 0, 30, 1), &expected);
}
#[test]
fn render_divider() {
let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"]).divider("--");
let mut expected = Buffer::with_lines(vec![" Tab1 -- Tab2 -- Tab3 -- Tab4 "]);
let mut expected = Buffer::with_lines([" Tab1 -- Tab2 -- Tab3 -- Tab4 "]);
// first tab selected
expected.set_style(Rect::new(1, 0, 4, 1), DEFAULT_HIGHLIGHT_STYLE);
assert_buffer_eq!(render(tabs, Rect::new(0, 0, 30, 1)), expected);
test_case(tabs, Rect::new(0, 0, 30, 1), &expected);
}
#[test]

View File

@@ -14,6 +14,7 @@
// not too happy about the redundancy in these tests,
// but if that helps readability then it's ok i guess /shrug
use pretty_assertions::assert_eq;
use ratatui::{backend::TestBackend, prelude::*, widgets::*};
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
@@ -42,7 +43,11 @@ impl AppState {
/// Renders the list to a `TestBackend` and asserts that the result matches the expected buffer.
#[track_caller]
fn assert_buffer(state: &mut AppState, expected: &Buffer) {
fn assert_buffer<'line, Lines>(state: &mut AppState, expected: Lines)
where
Lines: IntoIterator,
Lines::Item: Into<Line<'line>>,
{
let backend = TestBackend::new(21, 5);
let mut terminal = Terminal::new(backend).unwrap();
terminal
@@ -62,7 +67,7 @@ fn assert_buffer(state: &mut AppState, expected: &Buffer) {
.split(f.size());
let list = List::new(items)
.highlight_symbol(">>")
.block(Block::default().borders(Borders::RIGHT));
.block(Block::new().borders(Borders::RIGHT));
f.render_stateful_widget(list, layout[0], &mut state.list);
let table = Table::new(
@@ -76,7 +81,7 @@ fn assert_buffer(state: &mut AppState, expected: &Buffer) {
f.render_stateful_widget(scrollbar, layout[2], &mut state.scrollbar);
})
.unwrap();
terminal.backend().assert_buffer(expected);
terminal.backend().assert_buffer_lines(expected);
}
const DEFAULT_STATE_BUFFER: [&str; 5] = [
@@ -94,7 +99,8 @@ const DEFAULT_STATE_REPR: &str = r#"{
},
"table": {
"offset": 0,
"selected": null
"selected": null,
"marked": []
},
"scrollbar": {
"content_length": 10,
@@ -106,19 +112,15 @@ const DEFAULT_STATE_REPR: &str = r#"{
#[test]
fn default_state_serialize() {
let mut state = AppState::default();
let expected = Buffer::with_lines(DEFAULT_STATE_BUFFER);
assert_buffer(&mut state, &expected);
assert_buffer(&mut state, DEFAULT_STATE_BUFFER);
let state = serde_json::to_string_pretty(&state).unwrap();
assert_eq!(state, DEFAULT_STATE_REPR);
}
#[test]
fn default_state_deserialize() {
let expected = Buffer::with_lines(DEFAULT_STATE_BUFFER);
let mut state: AppState = serde_json::from_str(DEFAULT_STATE_REPR).unwrap();
assert_buffer(&mut state, &expected);
assert_buffer(&mut state, DEFAULT_STATE_BUFFER);
}
const SELECTED_STATE_BUFFER: [&str; 5] = [
@@ -135,7 +137,8 @@ const SELECTED_STATE_REPR: &str = r#"{
},
"table": {
"offset": 0,
"selected": 1
"selected": 1,
"marked": []
},
"scrollbar": {
"content_length": 10,
@@ -148,19 +151,15 @@ const SELECTED_STATE_REPR: &str = r#"{
fn selected_state_serialize() {
let mut state = AppState::default();
state.select(1);
let expected = Buffer::with_lines(SELECTED_STATE_BUFFER);
assert_buffer(&mut state, &expected);
assert_buffer(&mut state, SELECTED_STATE_BUFFER);
let state = serde_json::to_string_pretty(&state).unwrap();
assert_eq!(state, SELECTED_STATE_REPR);
}
#[test]
fn selected_state_deserialize() {
let expected = Buffer::with_lines(SELECTED_STATE_BUFFER);
let mut state: AppState = serde_json::from_str(SELECTED_STATE_REPR).unwrap();
assert_buffer(&mut state, &expected);
assert_buffer(&mut state, SELECTED_STATE_BUFFER);
}
const SCROLLED_STATE_BUFFER: [&str; 5] = [
@@ -178,7 +177,8 @@ const SCROLLED_STATE_REPR: &str = r#"{
},
"table": {
"offset": 4,
"selected": 8
"selected": 8,
"marked": []
},
"scrollbar": {
"content_length": 10,
@@ -191,17 +191,13 @@ const SCROLLED_STATE_REPR: &str = r#"{
fn scrolled_state_serialize() {
let mut state = AppState::default();
state.select(8);
let expected = Buffer::with_lines(SCROLLED_STATE_BUFFER);
assert_buffer(&mut state, &expected);
assert_buffer(&mut state, SCROLLED_STATE_BUFFER);
let state = serde_json::to_string_pretty(&state).unwrap();
assert_eq!(state, SCROLLED_STATE_REPR);
}
#[test]
fn scrolled_state_deserialize() {
let expected = Buffer::with_lines(SCROLLED_STATE_BUFFER);
let mut state: AppState = serde_json::from_str(SCROLLED_STATE_REPR).unwrap();
assert_buffer(&mut state, &expected);
assert_buffer(&mut state, SCROLLED_STATE_BUFFER);
}

View File

@@ -5,7 +5,7 @@ use ratatui::{
buffer::Buffer,
layout::Rect,
style::{Color, Style, Stylize},
widgets::{BarChart, Block, Borders, Paragraph},
widgets::{BarChart, Block, Paragraph},
Terminal,
};
@@ -28,7 +28,7 @@ fn barchart_can_be_stylized() {
})
.unwrap();
let mut expected = Buffer::with_lines(vec![
let mut expected = Buffer::with_lines([
" ██ ",
" ▅▅ ██ ",
"▂▂ ██ ██ ",
@@ -55,17 +55,15 @@ fn barchart_can_be_stylized() {
expected.get_mut(x * 3, 4).set_fg(Color::Blue);
expected.get_mut(x * 3 + 1, 4).set_fg(Color::Reset);
}
terminal.backend().assert_buffer(&expected);
}
#[test]
fn block_can_be_stylized() -> io::Result<()> {
let block = Block::default()
let block = Block::bordered()
.title("Title".light_blue())
.on_cyan()
.cyan()
.borders(Borders::ALL);
.cyan();
let area = Rect::new(0, 0, 8, 3);
let mut terminal = Terminal::new(TestBackend::new(11, 4))?;
@@ -73,7 +71,8 @@ fn block_can_be_stylized() -> io::Result<()> {
f.render_widget(block, area);
})?;
let mut expected = Buffer::with_lines(vec![
#[rustfmt::skip]
let mut expected = Buffer::with_lines([
"┌Title─┐ ",
"│ │ ",
"└──────┘ ",
@@ -90,7 +89,6 @@ fn block_can_be_stylized() -> io::Result<()> {
for x in 1..=5 {
expected.get_mut(x, 0).set_fg(Color::LightBlue);
}
terminal.backend().assert_buffer(&expected);
Ok(())
}
@@ -105,7 +103,7 @@ fn paragraph_can_be_stylized() -> io::Result<()> {
f.render_widget(paragraph, area);
})?;
let mut expected = Buffer::with_lines(vec!["Text "]);
let mut expected = Buffer::with_lines(["Text "]);
for x in 0..4 {
expected.get_mut(x, 0).set_fg(Color::Cyan);
}

View File

@@ -1,10 +1,8 @@
use std::error::Error;
use ratatui::{
assert_buffer_eq,
backend::{Backend, TestBackend},
layout::Rect,
prelude::Buffer,
widgets::{Paragraph, Widget},
Terminal, TerminalOptions, Viewport,
};
@@ -105,16 +103,13 @@ fn terminal_insert_before_moves_viewport() -> Result<(), Box<dyn Error>> {
f.render_widget(paragraph, f.size());
})?;
assert_buffer_eq!(
terminal.backend().buffer().clone(),
Buffer::with_lines(vec![
"------ Line 1 ------",
"------ Line 2 ------",
"[---- Viewport ----]",
" ",
" ",
])
);
terminal.backend().assert_buffer_lines([
"------ Line 1 ------",
"------ Line 2 ------",
"[---- Viewport ----]",
" ",
" ",
]);
Ok(())
}
@@ -150,16 +145,13 @@ fn terminal_insert_before_scrolls_on_large_input() -> Result<(), Box<dyn Error>>
f.render_widget(paragraph, f.size());
})?;
assert_buffer_eq!(
terminal.backend().buffer().clone(),
Buffer::with_lines(vec![
"------ Line 2 ------",
"------ Line 3 ------",
"------ Line 4 ------",
"------ Line 5 ------",
"[---- Viewport ----]",
])
);
terminal.backend().assert_buffer_lines([
"------ Line 2 ------",
"------ Line 3 ------",
"------ Line 4 ------",
"------ Line 5 ------",
"[---- Viewport ----]",
]);
Ok(())
}
@@ -205,16 +197,13 @@ fn terminal_insert_before_scrolls_on_many_inserts() -> Result<(), Box<dyn Error>
f.render_widget(paragraph, f.size());
})?;
assert_buffer_eq!(
terminal.backend().buffer().clone(),
Buffer::with_lines(vec![
"------ Line 2 ------",
"------ Line 3 ------",
"------ Line 4 ------",
"------ Line 5 ------",
"[---- Viewport ----]",
])
);
terminal.backend().assert_buffer_lines([
"------ Line 2 ------",
"------ Line 3 ------",
"------ Line 4 ------",
"------ Line 5 ------",
"[---- Viewport ----]",
]);
Ok(())
}

View File

@@ -2,33 +2,28 @@ use ratatui::{
backend::TestBackend,
buffer::Buffer,
style::{Color, Style},
widgets::{Bar, BarChart, BarGroup, Block, Borders},
widgets::{Bar, BarChart, BarGroup, Block},
Terminal,
};
// check that bars fill up correctly up to max value
#[test]
fn widgets_barchart_not_full_below_max_value() {
let test_case = |expected| {
let backend = TestBackend::new(30, 10);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let size = f.size();
let barchart = BarChart::default()
.block(Block::default().borders(Borders::ALL))
.data(&[("empty", 0), ("half", 50), ("almost", 99), ("full", 100)])
.max(100)
.bar_width(7)
.bar_gap(0);
f.render_widget(barchart, size);
})
.unwrap();
terminal.backend().assert_buffer(&expected);
};
// check that bars fill up correctly up to max value
test_case(Buffer::with_lines(vec![
let backend = TestBackend::new(30, 10);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let size = f.size();
let barchart = BarChart::default()
.block(Block::bordered())
.data(&[("empty", 0), ("half", 50), ("almost", 99), ("full", 100)])
.max(100)
.bar_width(7)
.bar_gap(0);
f.render_widget(barchart, size);
})
.unwrap();
terminal.backend().assert_buffer_lines([
"┌────────────────────────────┐",
"│ ▇▇▇▇▇▇▇███████│",
"│ ██████████████│",
@@ -39,47 +34,43 @@ fn widgets_barchart_not_full_below_max_value() {
"│ ██50█████99█████100██│",
"│ empty half almost full │",
"└────────────────────────────┘",
]));
]);
}
#[test]
fn widgets_barchart_group() {
const TERMINAL_HEIGHT: u16 = 11;
let test_case = |expected| {
let backend = TestBackend::new(35, TERMINAL_HEIGHT);
let mut terminal = Terminal::new(backend).unwrap();
let backend = TestBackend::new(35, TERMINAL_HEIGHT);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let size = f.size();
let barchart = BarChart::default()
.block(Block::bordered())
.data(
BarGroup::default().label("Mar".into()).bars(&[
Bar::default()
.value(10)
.label("C1".into())
.style(Style::default().fg(Color::Red))
.value_style(Style::default().fg(Color::Blue)),
Bar::default()
.value(20)
.style(Style::default().fg(Color::Green))
.text_value("20M".to_string()),
]),
)
.data(&vec![("C1", 50), ("C2", 40)])
.data(&[("C1", 60), ("C2", 90)])
.data(&[("xx", 10), ("xx", 10)])
.group_gap(2)
.bar_width(4)
.bar_gap(1);
f.render_widget(barchart, size);
})
.unwrap();
terminal
.draw(|f| {
let size = f.size();
let barchart = BarChart::default()
.block(Block::default().borders(Borders::ALL))
.data(
BarGroup::default().label("Mar".into()).bars(&[
Bar::default()
.value(10)
.label("C1".into())
.style(Style::default().fg(Color::Red))
.value_style(Style::default().fg(Color::Blue)),
Bar::default()
.value(20)
.style(Style::default().fg(Color::Green))
.text_value("20M".to_string()),
]),
)
.data(&vec![("C1", 50), ("C2", 40)])
.data(&[("C1", 60), ("C2", 90)])
.data(&[("xx", 10), ("xx", 10)])
.group_gap(2)
.bar_width(4)
.bar_gap(1);
f.render_widget(barchart, size);
})
.unwrap();
terminal.backend().assert_buffer(&expected);
};
let mut expected = Buffer::with_lines(vec![
let mut expected = Buffer::with_lines([
"┌─────────────────────────────────┐",
"│ ████│",
"│ ████│",
@@ -92,16 +83,13 @@ fn widgets_barchart_group() {
"│Mar │",
"└─────────────────────────────────┘",
]);
for y in 1..(TERMINAL_HEIGHT - 3) {
for x in 1..5 {
expected.get_mut(x, y).set_fg(Color::Red);
expected.get_mut(x + 5, y).set_fg(Color::Green);
}
}
expected.get_mut(2, 7).set_fg(Color::Blue);
expected.get_mut(3, 7).set_fg(Color::Blue);
test_case(expected);
terminal.backend().assert_buffer(&expected);
}

File diff suppressed because it is too large Load Diff

View File

@@ -12,10 +12,9 @@ use ratatui::{
use time::{Date, Month};
#[track_caller]
fn test_render<W: Widget>(widget: W, expected: &Buffer, width: u16, height: u16) {
fn test_render<W: Widget>(widget: W, width: u16, height: u16, expected: &Buffer) {
let backend = TestBackend::new(width, height);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| f.render_widget(widget, f.size()))
.unwrap();
@@ -28,14 +27,14 @@ fn days_layout() {
Date::from_calendar_date(2023, Month::January, 1).unwrap(),
CalendarEventStore::default(),
);
let expected = Buffer::with_lines(vec![
let expected = Buffer::with_lines([
" 1 2 3 4 5 6 7",
" 8 9 10 11 12 13 14",
" 15 16 17 18 19 20 21",
" 22 23 24 25 26 27 28",
" 29 30 31",
]);
test_render(c, &expected, 21, 5);
test_render(c, 21, 5, &expected);
}
#[test]
@@ -45,7 +44,7 @@ fn days_layout_show_surrounding() {
CalendarEventStore::default(),
)
.show_surrounding(Style::default());
let expected = Buffer::with_lines(vec![
let expected = Buffer::with_lines([
" 26 27 28 29 30 1 2",
" 3 4 5 6 7 8 9",
" 10 11 12 13 14 15 16",
@@ -53,7 +52,7 @@ fn days_layout_show_surrounding() {
" 24 25 26 27 28 29 30",
" 31 1 2 3 4 5 6",
]);
test_render(c, &expected, 21, 6);
test_render(c, 21, 6, &expected);
}
#[test]
@@ -63,7 +62,7 @@ fn show_month_header() {
CalendarEventStore::default(),
)
.show_month_header(Style::default());
let expected = Buffer::with_lines(vec![
let expected = Buffer::with_lines([
" January 2023 ",
" 1 2 3 4 5 6 7",
" 8 9 10 11 12 13 14",
@@ -71,7 +70,7 @@ fn show_month_header() {
" 22 23 24 25 26 27 28",
" 29 30 31",
]);
test_render(c, &expected, 21, 6);
test_render(c, 21, 6, &expected);
}
#[test]
@@ -81,7 +80,7 @@ fn show_weekdays_header() {
CalendarEventStore::default(),
)
.show_weekdays_header(Style::default());
let expected = Buffer::with_lines(vec![
let expected = Buffer::with_lines([
" Su Mo Tu We Th Fr Sa",
" 1 2 3 4 5 6 7",
" 8 9 10 11 12 13 14",
@@ -89,7 +88,7 @@ fn show_weekdays_header() {
" 22 23 24 25 26 27 28",
" 29 30 31",
]);
test_render(c, &expected, 21, 6);
test_render(c, 21, 6, &expected);
}
#[test]
@@ -101,7 +100,7 @@ fn show_combo() {
.show_weekdays_header(Style::default())
.show_month_header(Style::default())
.show_surrounding(Style::default());
let expected = Buffer::with_lines(vec![
let expected = Buffer::with_lines([
" January 2023 ",
" Su Mo Tu We Th Fr Sa",
" 1 2 3 4 5 6 7",
@@ -110,5 +109,5 @@ fn show_combo() {
" 22 23 24 25 26 27 28",
" 29 30 31 1 2 3 4",
]);
test_render(c, &expected, 21, 7);
test_render(c, 21, 7, &expected);
}

View File

@@ -29,7 +29,7 @@ fn widgets_canvas_draw_labels() {
})
.unwrap();
let mut expected = Buffer::with_lines(vec![" ", " ", " ", " ", "test "]);
let mut expected = Buffer::with_lines(["", "", "", "", "test "]);
for row in 0..5 {
for col in 0..5 {
expected.get_mut(col, row).set_bg(Color::Yellow);

View File

@@ -5,7 +5,7 @@ use ratatui::{
style::{Color, Style},
symbols,
text::{self, Span},
widgets::{Axis, Block, Borders, Chart, Dataset, GraphType::Line},
widgets::{Axis, Block, Chart, Dataset, GraphType::Line},
Terminal,
};
use rstest::rstest;
@@ -14,9 +14,16 @@ fn create_labels<'a>(labels: &'a [&'a str]) -> Vec<Span<'a>> {
labels.iter().map(|l| Span::from(*l)).collect()
}
fn axis_test_case<'a, S>(width: u16, height: u16, x_axis: Axis, y_axis: Axis, lines: Vec<S>)
where
S: Into<text::Line<'a>>,
#[track_caller]
fn axis_test_case<'line, Lines>(
width: u16,
height: u16,
x_axis: Axis,
y_axis: Axis,
expected: Lines,
) where
Lines: IntoIterator,
Lines::Item: Into<text::Line<'line>>,
{
let backend = TestBackend::new(width, height);
let mut terminal = Terminal::new(backend).unwrap();
@@ -26,8 +33,7 @@ where
f.render_widget(chart, f.size());
})
.unwrap();
let expected = Buffer::with_lines(lines);
terminal.backend().assert_buffer(&expected);
terminal.backend().assert_buffer_lines(expected);
}
#[rstest]
@@ -46,7 +52,7 @@ fn widgets_chart_can_render_on_small_areas(#[case] width: u16, #[case] height: u
.style(Style::default().fg(Color::Magenta))
.data(&[(0.0, 0.0)])];
let chart = Chart::new(datasets)
.block(Block::default().title("Plot").borders(Borders::ALL))
.block(Block::bordered().title("Plot"))
.x_axis(
Axis::default()
.bounds([0.0, 0.0])
@@ -62,195 +68,192 @@ fn widgets_chart_can_render_on_small_areas(#[case] width: u16, #[case] height: u
.unwrap();
}
#[test]
fn widgets_chart_handles_long_labels() {
let test_case = |x_labels, y_labels, x_alignment, lines| {
let mut x_axis = Axis::default().bounds([0.0, 1.0]);
if let Some((left_label, right_label)) = x_labels {
x_axis = x_axis
.labels(vec![Span::from(left_label), Span::from(right_label)])
.labels_alignment(x_alignment);
}
let mut y_axis = Axis::default().bounds([0.0, 1.0]);
if let Some((left_label, right_label)) = y_labels {
y_axis = y_axis.labels(vec![Span::from(left_label), Span::from(right_label)]);
}
axis_test_case(10, 5, x_axis, y_axis, lines);
};
test_case(
Some(("AAAA", "B")),
None,
Alignment::Left,
vec![
" ",
" ",
" ",
" ───────",
"AAA B",
],
);
test_case(
Some(("A", "BBBB")),
None,
Alignment::Left,
vec![
" ",
" ",
" ",
" ─────────",
"A BBBB",
],
);
test_case(
Some(("AAAAAAAAAAA", "B")),
None,
Alignment::Left,
vec![
" ",
" ",
" ",
" ───────",
"AAA B",
],
);
test_case(
Some(("A", "B")),
Some(("CCCCCCC", "D")),
Alignment::Left,
vec![
"D │ ",
"",
"CCC│ ",
" └──────",
" A B",
],
);
test_case(
Some(("AAAAAAAAAA", "B")),
Some(("C", "D")),
Alignment::Center,
vec![
"D │ ",
"",
"C │ ",
" └──────",
"AAAAAAA B",
],
);
test_case(
Some(("AAAAAAA", "B")),
Some(("C", "D")),
Alignment::Right,
vec![
"D│ ",
"",
"C│ ",
" └────────",
" AAAAA B",
],
);
test_case(
Some(("AAAAAAA", "BBBBBBB")),
Some(("C", "D")),
Alignment::Right,
vec![
"D│ ",
"",
"C│ ",
" └────────",
" AAAAABBBB",
],
);
#[rstest]
#[case(
Some(("AAAA", "B")),
None,
Alignment::Left,
vec![
" ",
" ",
" ",
" ───────",
"AAA B",
],
)]
#[case(
Some(("A", "BBBB")),
None,
Alignment::Left,
vec![
" ",
" ",
" ",
" ─────────",
"A BBBB",
],
)]
#[case(
Some(("AAAAAAAAAAA", "B")),
None,
Alignment::Left,
vec![
" ",
" ",
" ",
" ───────",
"AAA B",
],
)]
#[case(
Some(("A", "B")),
Some(("CCCCCCC", "D")),
Alignment::Left,
vec![
"D │ ",
"",
"CCC│ ",
" └──────",
" A B",
],
)]
#[case(
Some(("AAAAAAAAAA", "B")),
Some(("C", "D")),
Alignment::Center,
vec![
"D │ ",
"",
"C │ ",
" └──────",
"AAAAAAA B",
],
)]
#[case(
Some(("AAAAAAA", "B")),
Some(("C", "D")),
Alignment::Right,
vec![
"D│ ",
"",
"C│ ",
" └────────",
" AAAAA B",
],
)]
#[case(
Some(("AAAAAAA", "BBBBBBB")),
Some(("C", "D")),
Alignment::Right,
vec![
"D│ ",
"",
"C│ ",
" └────────",
" AAAAABBBB",
],
)]
fn widgets_chart_handles_long_labels<'line, Lines>(
#[case] x_labels: Option<(&str, &str)>,
#[case] y_labels: Option<(&str, &str)>,
#[case] x_alignment: Alignment,
#[case] expected: Lines,
) where
Lines: IntoIterator,
Lines::Item: Into<text::Line<'line>>,
{
let mut x_axis = Axis::default().bounds([0.0, 1.0]);
if let Some((left_label, right_label)) = x_labels {
x_axis = x_axis
.labels(vec![Span::from(left_label), Span::from(right_label)])
.labels_alignment(x_alignment);
}
let mut y_axis = Axis::default().bounds([0.0, 1.0]);
if let Some((left_label, right_label)) = y_labels {
y_axis = y_axis.labels(vec![Span::from(left_label), Span::from(right_label)]);
}
axis_test_case(10, 5, x_axis, y_axis, expected);
}
#[test]
fn widgets_chart_handles_x_axis_labels_alignments() {
let test_case = |y_alignment, lines| {
let x_axis = Axis::default()
.labels(vec![Span::from("AAAA"), Span::from("B"), Span::from("C")])
.labels_alignment(y_alignment);
let y_axis = Axis::default();
axis_test_case(10, 5, x_axis, y_axis, lines);
};
test_case(
Alignment::Left,
vec![
" ",
" ",
" ",
" ───────",
"AAA B C",
],
);
test_case(
Alignment::Center,
vec![
" ",
" ",
" ",
" ────────",
"AAAA B C",
],
);
test_case(
Alignment::Right,
vec![
" ",
" ",
" ",
"──────────",
"AAA B C",
],
);
#[rstest]
#[case::left(
Alignment::Left,
vec![
" ",
" ",
" ",
" ───────",
"AAA B C",
],
)]
#[case::center(
Alignment::Center,
vec![
" ",
" ",
" ",
" ────────",
"AAAA B C",
],
)]
#[case::right(
Alignment::Right,
vec![
" ",
" ",
" ",
"──────────",
"AAA B C",
],
)]
fn widgets_chart_handles_x_axis_labels_alignments<'line, Lines>(
#[case] y_alignment: Alignment,
#[case] expected: Lines,
) where
Lines: IntoIterator,
Lines::Item: Into<text::Line<'line>>,
{
let x_axis = Axis::default()
.labels(vec![Span::from("AAAA"), Span::from("B"), Span::from("C")])
.labels_alignment(y_alignment);
let y_axis = Axis::default();
axis_test_case(10, 5, x_axis, y_axis, expected);
}
#[test]
fn widgets_chart_handles_y_axis_labels_alignments() {
let test_case = |y_alignment, lines| {
let x_axis = Axis::default().labels(create_labels(&["AAAAA", "B"]));
let y_axis = Axis::default()
.labels(create_labels(&["C", "D"]))
.labels_alignment(y_alignment);
axis_test_case(20, 5, x_axis, y_axis, lines);
};
test_case(
Alignment::Left,
vec![
"D │ ",
"",
"C",
" └───────────────",
"AAAAA B",
],
);
test_case(
Alignment::Center,
vec![
" D │ ",
"",
" C │ ",
" └───────────────",
"AAAAA B",
],
);
test_case(
Alignment::Right,
vec![
" D│ ",
"",
" C│ ",
" └───────────────",
"AAAAA B",
],
);
#[rstest]
#[case::left(Alignment::Left, [
"D │ ",
" ",
"C │ ",
" └───────────────",
"AAAAA B",
])]
#[case::center(Alignment::Center, [
" D │ ",
"",
" C │ ",
" └───────────────",
"AAAAA B",
])]
#[case::right(Alignment::Right, [
" D",
"",
" C│ ",
" └───────────────",
"AAAAA B",
])]
fn widgets_chart_handles_y_axis_labels_alignments<'line, Lines>(
#[case] y_alignment: Alignment,
#[case] expected: Lines,
) where
Lines: IntoIterator,
Lines::Item: Into<text::Line<'line>>,
{
let x_axis = Axis::default().labels(create_labels(&["AAAAA", "B"]));
let y_axis = Axis::default()
.labels(create_labels(&["C", "D"]))
.labels_alignment(y_alignment);
axis_test_case(20, 5, x_axis, y_axis, expected);
}
#[test]
@@ -265,7 +268,7 @@ fn widgets_chart_can_have_axis_with_zero_length_bounds() {
.style(Style::default().fg(Color::Magenta))
.data(&[(0.0, 0.0)])];
let chart = Chart::new(datasets)
.block(Block::default().title("Plot").borders(Borders::ALL))
.block(Block::bordered().title("Plot"))
.x_axis(
Axis::default()
.bounds([0.0, 0.0])
@@ -305,7 +308,7 @@ fn widgets_chart_handles_overflows() {
(1_588_298_496.0, 1.0),
])];
let chart = Chart::new(datasets)
.block(Block::default().title("Plot").borders(Borders::ALL))
.block(Block::bordered().title("Plot"))
.x_axis(
Axis::default()
.bounds([1_588_298_471.0, 1_588_992_600.0])
@@ -338,11 +341,7 @@ fn widgets_chart_can_have_empty_datasets() {
.draw(|f| {
let datasets = vec![Dataset::default().data(&[]).graph_type(Line)];
let chart = Chart::new(datasets)
.block(
Block::default()
.title("Empty Dataset With Line")
.borders(Borders::ALL),
)
.block(Block::bordered().title("Empty Dataset With Line"))
.x_axis(
Axis::default()
.bounds([0.0, 0.0])
@@ -411,7 +410,7 @@ fn widgets_chart_can_have_a_legend() {
];
let chart = Chart::new(datasets)
.style(Style::default().bg(Color::White))
.block(Block::default().title("Chart Test").borders(Borders::ALL))
.block(Block::bordered().title("Chart Test"))
.x_axis(
Axis::default()
.bounds([0.0, 100.0])
@@ -435,7 +434,7 @@ fn widgets_chart_can_have_a_legend() {
);
})
.unwrap();
let mut expected = Buffer::with_lines(vec![
let mut expected = Buffer::with_lines([
"┌Chart Test────────────────────────────────────────────────┐",
"│10.0│Y Axis ┌─────────┐│",
"│ │ •• │Dataset 1││",
@@ -615,7 +614,6 @@ fn widgets_chart_can_have_a_legend() {
for (col, row) in x_axis_title {
expected.get_mut(col, row).set_fg(Color::Yellow);
}
terminal.backend().assert_buffer(&expected);
}
@@ -645,7 +643,7 @@ fn widgets_chart_top_line_styling_is_correct() {
})
.unwrap();
let mut expected = Buffer::with_lines(vec![
let mut expected = Buffer::with_lines([
"b│abc••••",
"",
"",

View File

@@ -5,7 +5,7 @@ use ratatui::{
style::{Color, Modifier, Style, Stylize},
symbols,
text::Span,
widgets::{Block, Borders, Gauge, LineGauge},
widgets::{Block, Gauge, LineGauge},
Terminal,
};
@@ -22,20 +22,20 @@ fn widgets_gauge_renders() {
.split(f.size());
let gauge = Gauge::default()
.block(Block::default().title("Percentage").borders(Borders::ALL))
.block(Block::bordered().title("Percentage"))
.gauge_style(Style::default().bg(Color::Blue).fg(Color::Red))
.use_unicode(true)
.percent(43);
f.render_widget(gauge, chunks[0]);
let gauge = Gauge::default()
.block(Block::default().title("Ratio").borders(Borders::ALL))
.block(Block::bordered().title("Ratio"))
.gauge_style(Style::default().bg(Color::Blue).fg(Color::Red))
.use_unicode(true)
.ratio(0.511_313_934_313_1);
f.render_widget(gauge, chunks[1]);
})
.unwrap();
let mut expected = Buffer::with_lines(vec![
let mut expected = Buffer::with_lines([
" ",
" ",
" ┌Percentage────────────────────────┐ ",
@@ -71,18 +71,18 @@ fn widgets_gauge_renders_no_unicode() {
.split(f.size());
let gauge = Gauge::default()
.block(Block::default().title("Percentage").borders(Borders::ALL))
.block(Block::bordered().title("Percentage"))
.percent(43)
.use_unicode(false);
f.render_widget(gauge, chunks[0]);
let gauge = Gauge::default()
.block(Block::default().title("Ratio").borders(Borders::ALL))
.block(Block::bordered().title("Ratio"))
.ratio(0.211_313_934_313_1)
.use_unicode(false);
f.render_widget(gauge, chunks[1]);
})
.unwrap();
let expected = Buffer::with_lines(vec![
terminal.backend().assert_buffer_lines([
" ",
" ",
" ┌Percentage────────────────────────┐ ",
@@ -94,7 +94,6 @@ fn widgets_gauge_renders_no_unicode() {
" ",
" ",
]);
terminal.backend().assert_buffer(&expected);
}
#[test]
@@ -106,9 +105,7 @@ fn widgets_gauge_applies_styles() {
.draw(|f| {
let gauge = Gauge::default()
.block(
Block::default()
.title(Span::styled("Test", Style::default().fg(Color::Red)))
.borders(Borders::ALL),
Block::bordered().title(Span::styled("Test", Style::default().fg(Color::Red))),
)
.gauge_style(Style::default().fg(Color::Blue).bg(Color::Red))
.percent(43)
@@ -121,7 +118,7 @@ fn widgets_gauge_applies_styles() {
f.render_widget(gauge, f.size());
})
.unwrap();
let mut expected = Buffer::with_lines(vec![
let mut expected = Buffer::with_lines([
"┌Test──────┐",
"│████ │",
"│███43% │",
@@ -173,8 +170,7 @@ fn widgets_gauge_supports_large_labels() {
f.render_widget(gauge, f.size());
})
.unwrap();
let expected = Buffer::with_lines(vec!["4333333333"]);
terminal.backend().assert_buffer(&expected);
terminal.backend().assert_buffer_lines(["4333333333"]);
}
#[test]
@@ -196,7 +192,7 @@ fn widgets_line_gauge_renders() {
},
);
let gauge = LineGauge::default()
.block(Block::default().title("Gauge 2").borders(Borders::ALL))
.block(Block::bordered().title("Gauge 2"))
.gauge_style(Style::default().fg(Color::Green))
.line_set(symbols::line::THICK)
.ratio(0.211_313_934_313_1);
@@ -211,7 +207,7 @@ fn widgets_line_gauge_renders() {
);
})
.unwrap();
let mut expected = Buffer::with_lines(vec![
let mut expected = Buffer::with_lines([
"43% ────────────────",
"┌Gauge 2───────────┐",
"│21% ━━━━━━━━━━━━━━│",

View File

@@ -8,6 +8,7 @@ use ratatui::{
widgets::{Block, Borders, HighlightSpacing, List, ListItem, ListState},
Terminal,
};
use rstest::rstest;
#[test]
fn list_should_shows_the_length() {
@@ -45,7 +46,12 @@ fn widgets_list_should_highlight_the_selected_item() {
f.render_stateful_widget(list, size, &mut state);
})
.unwrap();
let mut expected = Buffer::with_lines(vec![" Item 1 ", ">> Item 2 ", " Item 3 "]);
#[rustfmt::skip]
let mut expected = Buffer::with_lines([
" Item 1 ",
">> Item 2 ",
" Item 3 ",
]);
for x in 0..10 {
expected.get_mut(x, 1).set_bg(Color::Yellow);
}
@@ -75,9 +81,12 @@ fn widgets_list_should_highlight_the_selected_item_wide_symbol() {
f.render_stateful_widget(list, size, &mut state);
})
.unwrap();
let mut expected = Buffer::with_lines(vec![" Item 1 ", "▶ Item 2 ", " Item 3 "]);
#[rustfmt::skip]
let mut expected = Buffer::with_lines([
" Item 1 ",
"▶ Item 2 ",
" Item 3 ",
]);
for x in 0..10 {
expected.get_mut(x, 1).set_bg(Color::Yellow);
}
@@ -95,7 +104,7 @@ fn widgets_list_should_truncate_items() {
let backend = TestBackend::new(10, 2);
let mut terminal = Terminal::new(backend).unwrap();
let cases = vec![
let cases = [
// An item is selected
TruncateTestCase {
selected: Some(0),
@@ -103,7 +112,7 @@ fn widgets_list_should_truncate_items() {
ListItem::new("A very long line"),
ListItem::new("A very long line"),
],
expected: Buffer::with_lines(vec![
expected: Buffer::with_lines([
format!(">> A ve{} ", symbols::line::VERTICAL),
format!(" A ve{} ", symbols::line::VERTICAL),
]),
@@ -115,7 +124,7 @@ fn widgets_list_should_truncate_items() {
ListItem::new("A very long line"),
ListItem::new("A very long line"),
],
expected: Buffer::with_lines(vec![
expected: Buffer::with_lines([
format!("A very {} ", symbols::line::VERTICAL),
format!("A very {} ", symbols::line::VERTICAL),
]),
@@ -127,7 +136,7 @@ fn widgets_list_should_truncate_items() {
terminal
.draw(|f| {
let list = List::new(case.items.clone())
.block(Block::default().borders(Borders::RIGHT))
.block(Block::new().borders(Borders::RIGHT))
.highlight_symbol(">> ");
f.render_stateful_widget(list, Rect::new(0, 0, 8, 2), &mut state);
})
@@ -159,8 +168,12 @@ fn widgets_list_should_clamp_offset_if_items_are_removed() {
f.render_stateful_widget(list, size, &mut state);
})
.unwrap();
let expected = Buffer::with_lines(vec![" Item 2 ", " Item 3 ", " Item 4 ", ">> Item 5 "]);
terminal.backend().assert_buffer(&expected);
terminal.backend().assert_buffer_lines([
" Item 2 ",
" Item 3 ",
" Item 4 ",
">> Item 5 ",
]);
// render again with 1 items => check offset is clamped to 1
state.select(Some(1));
@@ -172,8 +185,12 @@ fn widgets_list_should_clamp_offset_if_items_are_removed() {
f.render_stateful_widget(list, size, &mut state);
})
.unwrap();
let expected = Buffer::with_lines(vec![" Item 3 ", " ", " ", " "]);
terminal.backend().assert_buffer(&expected);
terminal.backend().assert_buffer_lines([
" Item 3 ",
" ",
" ",
" ",
]);
}
#[test]
@@ -196,7 +213,7 @@ fn widgets_list_should_display_multiline_items() {
f.render_stateful_widget(list, size, &mut state);
})
.unwrap();
let mut expected = Buffer::with_lines(vec![
let mut expected = Buffer::with_lines([
" Item 1 ",
" Item 1a",
">> Item 2 ",
@@ -232,7 +249,7 @@ fn widgets_list_should_repeat_highlight_symbol() {
f.render_stateful_widget(list, size, &mut state);
})
.unwrap();
let mut expected = Buffer::with_lines(vec![
let mut expected = Buffer::with_lines([
" Item 1 ",
" Item 1a",
">> Item 2 ",
@@ -251,7 +268,6 @@ fn widgets_list_should_repeat_highlight_symbol() {
fn widget_list_should_not_ignore_empty_string_items() {
let backend = TestBackend::new(6, 4);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let items = vec![
@@ -268,134 +284,98 @@ fn widget_list_should_not_ignore_empty_string_items() {
f.render_widget(list, f.size());
})
.unwrap();
let expected = Buffer::with_lines(vec!["Item 1", "", "", "Item 4"]);
terminal.backend().assert_buffer(&expected);
terminal
.backend()
.assert_buffer_lines(["Item 1", "", "", "Item 4"]);
}
#[allow(clippy::too_many_lines)]
#[test]
fn widgets_list_enable_always_highlight_spacing() {
let test_case = |state: &mut ListState, space: HighlightSpacing, expected: Buffer| {
let backend = TestBackend::new(30, 8);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let size = f.size();
let table = List::new(vec![
ListItem::new(vec![Line::from("Item 1"), Line::from("Item 1a")]),
ListItem::new(vec![Line::from("Item 2"), Line::from("Item 2b")]),
ListItem::new(vec![Line::from("Item 3"), Line::from("Item 3c")]),
])
.block(Block::default().borders(Borders::ALL))
.highlight_symbol(">> ")
.highlight_spacing(space);
f.render_stateful_widget(table, size, state);
})
.unwrap();
terminal.backend().assert_buffer(&expected);
};
assert_eq!(HighlightSpacing::default(), HighlightSpacing::WhenSelected);
let mut state = ListState::default();
// no selection, "WhenSelected" should only allocate if selected
test_case(
&mut state,
HighlightSpacing::default(),
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│Item 1 │",
"│Item 1a ",
"│Item 2 ",
"│Item 2b ",
"│Item 3 ",
"│Item 3c ",
"└────────────────────────────┘",
]),
);
// no selection, "Always" should allocate regardless if selected or not
test_case(
&mut state,
HighlightSpacing::Always,
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│ Item 1 ",
"│ Item 1a │",
"│ Item 2 │",
"│ Item 2b │",
"│ Item 3 │",
" Item 3c ",
"└────────────────────────────┘",
]),
);
// no selection, "Never" should never allocate regadless if selected or not
test_case(
&mut state,
HighlightSpacing::Never,
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│Item 1 │",
"│Item 1a │",
"│Item 2 │",
"│Item 2b │",
"│Item 3 │",
"│Item 3c │",
"└────────────────────────────┘",
]),
);
// select first, "WhenSelected" should only allocate if selected
state.select(Some(0));
test_case(
&mut state,
HighlightSpacing::default(),
Buffer::with_lines(vec![
"┌────────────────────────────┐",
">> Item 1 │",
"│ Item 1a │",
"│ Item 2 │",
"│ Item 2b │",
"│ Item 3 │",
"│ Item 3c │",
"└────────────────────────────┘",
]),
);
// select first, "Always" should allocate regardless if selected or not
state.select(Some(0));
test_case(
&mut state,
HighlightSpacing::Always,
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│>> Item 1 │",
"│ Item 1a │",
"│ Item 2 │",
"│ Item 2b │",
"│ Item 3 │",
"│ Item 3c │",
"└────────────────────────────┘",
]),
);
// select first, "Never" should never allocate regadless if selected or not
state.select(Some(0));
test_case(
&mut state,
HighlightSpacing::Never,
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│Item 1 │",
"│Item 1a │",
"│Item 2 │",
"│Item 2b │",
"│Item 3 │",
"│Item 3c │",
"└────────────────────────────┘",
]),
);
#[rstest]
#[case::none_when_selected(None, HighlightSpacing::WhenSelected, [
"┌─────────────┐",
"│Item 1 │",
"│Item 1a │",
"│Item 2 │",
"│Item 2b │",
"│Item 3 │",
"│Item 3c │",
"└─────────────┘",
])]
#[case::none_always(None, HighlightSpacing::Always, [
"┌─────────────┐",
"│ Item 1 │",
"│ Item 1a │",
"│ Item 2 │",
"│ Item 2b │",
"│ Item 3 │",
"│ Item 3c │",
"└─────────────┘",
])]
#[case::none_never(None, HighlightSpacing::Never, [
"┌─────────────┐",
"│Item 1 │",
"│Item 1a │",
"│Item 2 │",
"│Item 2b │",
"│Item 3 │",
"│Item 3c │",
"└─────────────┘",
])]
#[case::first_when_selected(Some(0), HighlightSpacing::WhenSelected, [
"┌─────────────┐",
">> Item 1 │",
" Item 1a",
" Item 2 │",
" Item 2b",
" Item 3 │",
"│ Item 3c ",
"└─────────────┘",
])]
#[case::first_always(Some(0), HighlightSpacing::Always, [
"┌─────────────┐",
"│>> Item 1 │",
"│ Item 1a │",
"│ Item 2 │",
"│ Item 2b │",
"│ Item 3 ",
"│ Item 3c",
"└─────────────┘",
])]
#[case::first_never(Some(0), HighlightSpacing::Never, [
"┌─────────────┐",
"│Item 1",
"│Item 1a ",
"│Item 2 │",
"│Item 2b │",
"│Item 3 │",
"│Item 3c │",
"└─────────────┘",
])]
fn widgets_list_enable_always_highlight_spacing<'line, Lines>(
#[case] selected: Option<usize>,
#[case] space: HighlightSpacing,
#[case] expected: Lines,
) where
Lines: IntoIterator,
Lines::Item: Into<Line<'line>>,
{
let mut state = ListState::default().with_selected(selected);
let backend = TestBackend::new(15, 8);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let size = f.size();
let table = List::new(vec![
ListItem::new(vec![Line::from("Item 1"), Line::from("Item 1a")]),
ListItem::new(vec![Line::from("Item 2"), Line::from("Item 2b")]),
ListItem::new(vec![Line::from("Item 3"), Line::from("Item 3c")]),
])
.block(Block::bordered())
.highlight_symbol(">> ")
.highlight_spacing(space);
f.render_stateful_widget(table, size, &mut state);
})
.unwrap();
terminal
.backend()
.assert_buffer(&Buffer::with_lines(expected));
}

View File

@@ -3,25 +3,23 @@ use ratatui::{
buffer::Buffer,
layout::Alignment,
text::{Line, Span, Text},
widgets::{Block, Borders, Padding, Paragraph, Wrap},
widgets::{Block, Padding, Paragraph, Wrap},
Terminal,
};
/// Tests the [`Paragraph`] widget against the expected [`Buffer`] by rendering it onto an equal
/// area and comparing the rendered and expected content.
#[allow(clippy::needless_pass_by_value)]
fn test_case(paragraph: Paragraph, expected: Buffer) {
#[track_caller]
fn test_case(paragraph: Paragraph, expected: &Buffer) {
let backend = TestBackend::new(expected.area.width, expected.area.height);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let size = f.size();
f.render_widget(paragraph, size);
})
.unwrap();
terminal.backend().assert_buffer(&expected);
terminal.backend().assert_buffer(expected);
}
#[test]
@@ -30,12 +28,12 @@ fn widgets_paragraph_renders_double_width_graphemes() {
let text = vec![Line::from(s)];
let paragraph = Paragraph::new(text)
.block(Block::default().borders(Borders::ALL))
.block(Block::bordered())
.wrap(Wrap { trim: true });
test_case(
paragraph,
Buffer::with_lines(vec![
&Buffer::with_lines([
"┌────────┐",
"│コンピュ│",
"│ータ上で│",
@@ -61,13 +59,12 @@ fn widgets_paragraph_renders_mixed_width_graphemes() {
let size = f.size();
let text = vec![Line::from(s)];
let paragraph = Paragraph::new(text)
.block(Block::default().borders(Borders::ALL))
.block(Block::bordered())
.wrap(Wrap { trim: true });
f.render_widget(paragraph, size);
})
.unwrap();
let expected = Buffer::with_lines(vec![
terminal.backend().assert_buffer_lines([
// The internal width is 8 so only 4 slots for double-width characters.
"┌────────┐",
"│aコンピ │", // Here we have 1 latin character so only 3 double-width ones can fit.
@@ -77,18 +74,17 @@ fn widgets_paragraph_renders_mixed_width_graphemes() {
"│、 │",
"└────────┘",
]);
terminal.backend().assert_buffer(&expected);
}
#[test]
fn widgets_paragraph_can_wrap_with_a_trailing_nbsp() {
let nbsp = "\u{00a0}";
let line = Line::from(vec![Span::raw("NBSP"), Span::raw(nbsp)]);
let paragraph = Paragraph::new(line).block(Block::default().borders(Borders::ALL));
let paragraph = Paragraph::new(line).block(Block::bordered());
test_case(
paragraph,
Buffer::with_lines(vec![
&Buffer::with_lines([
"┌──────────────────┐",
"│NBSP\u{00a0}",
"└──────────────────┘",
@@ -99,16 +95,16 @@ fn widgets_paragraph_can_wrap_with_a_trailing_nbsp() {
#[test]
fn widgets_paragraph_can_scroll_horizontally() {
let text =
Text::from("段落现在可以水平滚动了!\nParagraph can scroll horizontally!\nShort line");
let paragraph = Paragraph::new(text).block(Block::default().borders(Borders::ALL));
Text::from("段落现在可以水平滚动了!\nParagraph can scroll horizontally!\nLittle line");
let paragraph = Paragraph::new(text).block(Block::bordered());
test_case(
paragraph.clone().alignment(Alignment::Left).scroll((0, 7)),
Buffer::with_lines(vec![
&Buffer::with_lines([
"┌──────────────────┐",
"│在可以水平滚动了!│",
"│ph can scroll hori│",
"│ine ",
"line │",
"│ │",
"│ │",
"│ │",
@@ -120,11 +116,11 @@ fn widgets_paragraph_can_scroll_horizontally() {
// only support Alignment::Left
test_case(
paragraph.clone().alignment(Alignment::Right).scroll((0, 7)),
Buffer::with_lines(vec![
&Buffer::with_lines([
"┌──────────────────┐",
"│段落现在可以水平滚│",
"│Paragraph can scro│",
" Short line│",
"Little line│",
"│ │",
"│ │",
"│ │",
@@ -144,12 +140,12 @@ const SAMPLE_STRING: &str = "The library is based on the principle of immediate
fn widgets_paragraph_can_wrap_its_content() {
let text = vec![Line::from(SAMPLE_STRING)];
let paragraph = Paragraph::new(text)
.block(Block::default().borders(Borders::ALL))
.block(Block::bordered())
.wrap(Wrap { trim: true });
test_case(
paragraph.clone().alignment(Alignment::Left),
Buffer::with_lines(vec![
&Buffer::with_lines([
"┌──────────────────┐",
"│The library is │",
"│based on the │",
@@ -164,7 +160,7 @@ fn widgets_paragraph_can_wrap_its_content() {
);
test_case(
paragraph.clone().alignment(Alignment::Center),
Buffer::with_lines(vec![
&Buffer::with_lines([
"┌──────────────────┐",
"│ The library is │",
"│ based on the │",
@@ -179,7 +175,7 @@ fn widgets_paragraph_can_wrap_its_content() {
);
test_case(
paragraph.clone().alignment(Alignment::Right),
Buffer::with_lines(vec![
&Buffer::with_lines([
"┌──────────────────┐",
"│ The library is│",
"│ based on the│",
@@ -196,19 +192,19 @@ fn widgets_paragraph_can_wrap_its_content() {
#[test]
fn widgets_paragraph_works_with_padding() {
let text = vec![Line::from(SAMPLE_STRING)];
let paragraph = Paragraph::new(text)
.block(Block::default().borders(Borders::ALL).padding(Padding {
left: 2,
right: 2,
top: 1,
bottom: 1,
}))
let block = Block::bordered().padding(Padding {
left: 2,
right: 2,
top: 1,
bottom: 1,
});
let paragraph = Paragraph::new(vec![Line::from(SAMPLE_STRING)])
.block(block.clone())
.wrap(Wrap { trim: true });
test_case(
paragraph.clone().alignment(Alignment::Left),
Buffer::with_lines(vec![
&Buffer::with_lines([
"┌────────────────────┐",
"│ │",
"│ The library is │",
@@ -225,7 +221,7 @@ fn widgets_paragraph_works_with_padding() {
);
test_case(
paragraph.clone().alignment(Alignment::Right),
Buffer::with_lines(vec![
&Buffer::with_lines([
"┌────────────────────┐",
"│ │",
"│ The library is │",
@@ -241,20 +237,16 @@ fn widgets_paragraph_works_with_padding() {
]),
);
let mut text = vec![Line::from("This is always centered.").alignment(Alignment::Center)];
text.push(Line::from(SAMPLE_STRING));
let paragraph = Paragraph::new(text)
.block(Block::default().borders(Borders::ALL).padding(Padding {
left: 2,
right: 2,
top: 1,
bottom: 1,
}))
.wrap(Wrap { trim: true });
let paragraph = Paragraph::new(vec![
Line::from("This is always centered.").alignment(Alignment::Center),
Line::from(SAMPLE_STRING),
])
.block(block)
.wrap(Wrap { trim: true });
test_case(
paragraph.alignment(Alignment::Right),
Buffer::with_lines(vec![
&Buffer::with_lines([
"┌────────────────────┐",
"│ │",
"│ This is always │",
@@ -283,12 +275,12 @@ fn widgets_paragraph_can_align_spans() {
Line::from(default_s),
];
let paragraph = Paragraph::new(text)
.block(Block::default().borders(Borders::ALL))
.block(Block::bordered())
.wrap(Wrap { trim: true });
test_case(
paragraph.clone().alignment(Alignment::Left),
Buffer::with_lines(vec![
&Buffer::with_lines([
"┌──────────────────┐",
"│ This string will│",
"│ override the│",
@@ -303,7 +295,7 @@ fn widgets_paragraph_can_align_spans() {
);
test_case(
paragraph.alignment(Alignment::Center),
Buffer::with_lines(vec![
&Buffer::with_lines([
"┌──────────────────┐",
"│ This string will│",
"│ override the│",
@@ -333,11 +325,11 @@ fn widgets_paragraph_can_align_spans() {
let mut text = left_lines.clone();
text.append(&mut lines);
let paragraph = Paragraph::new(text).block(Block::default().borders(Borders::ALL));
let paragraph = Paragraph::new(text).block(Block::bordered());
test_case(
paragraph.clone().alignment(Alignment::Right),
Buffer::with_lines(vec![
&Buffer::with_lines([
"┌──────────────────┐",
"│This string │",
"│will override the │",
@@ -352,7 +344,7 @@ fn widgets_paragraph_can_align_spans() {
);
test_case(
paragraph.alignment(Alignment::Left),
Buffer::with_lines(vec![
&Buffer::with_lines([
"┌──────────────────┐",
"│This string │",
"│will override the │",

File diff suppressed because it is too large Load Diff

View File

@@ -26,8 +26,7 @@ fn widgets_tabs_should_not_panic_on_narrow_areas() {
);
})
.unwrap();
let expected = Buffer::with_lines(vec![" "]);
terminal.backend().assert_buffer(&expected);
terminal.backend().assert_buffer_lines([" "]);
}
#[test]
@@ -48,7 +47,7 @@ fn widgets_tabs_should_truncate_the_last_item() {
);
})
.unwrap();
let mut expected = Buffer::with_lines(vec![format!(" Tab1 {} T ", symbols::line::VERTICAL)]);
let mut expected = Buffer::with_lines([format!(" Tab1 {} T ", symbols::line::VERTICAL)]);
expected.set_style(Rect::new(1, 0, 4, 1), Style::new().reversed());
terminal.backend().assert_buffer(&expected);
}